15.线程同步的几种方法

2023-11-03

一、为什么需要线程同步

        线程同步通常是出现在多线程环境下的问题,对于多个线程同时访问的共享内存中的变量,如果不进行保护,就会导致一些列数据出错问题。以下图为例:

        假设线程A在第一次读取变量的值为10,每次写周期会将变量A加5,理论上当线程A完成其任务的时候,变量的值变为20,但是由于线程B是在两个写周期间读取的变量,结果为15,因此会导致数据出错。

二、互斥锁

        互斥锁,是进行线程同步的一种方式。顾名思义,当线程A对共享内存进行访问的时候,对其进行上锁,在访问结束后解锁。在加锁期间,如果线程B想要申请访问共享内存资源的话,会被阻塞,直到线程A释放互斥锁。

1、互斥锁初始化

pthread_mutex_init()函数

        初始化互斥锁,其函数原型如下所示:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

使用该函数需要包含头文件<pthread.h>。

mutex: 参数 mutex 是一个 pthread_mutex_t 类型指针, 指向需要进行初始化操作的互斥锁对象;
attr: 参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性
返回值: 成功返回 0;失败将返回一个非 0 的错误码

调用函数 pthread_mutex_lock()可以对互斥锁加锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);

而调用函数pthread_mutex_unlock()可以对互斥锁解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

当不需要使用互斥锁的时候需要对其进行销毁: 

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

不能销毁还没有解锁的互斥锁,否则将会出现错误;
没有初始化的互斥锁也不能销毁

测试代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_mutex_t mutex;   //定义自旋锁
static int count = 0;
static int loops;


static void *new_pthread(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;

    for (j = 0; j < loops; j++) {
        //pthread_mutex_lock(&mutex); //互斥锁上锁
        l_count = count;
        l_count++;
        count = l_count;
        //pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}


int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;   //定义两个线程
    int ret;
    
    loops = atoi(argv[1]);  //保存循环次数

    /* 初始化互斥锁 */
    //pthread_mutex_init(&mutex, NULL);

    //创建第一个线程
    ret = pthread_create(&tid1, NULL, new_pthread, &loops);
    if(ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);        
    }

    //创建第二个线程
    ret = pthread_create(&tid2, NULL, new_pthread, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid2, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("count = %d\n", count);

    pthread_mutex_destroy(&mutex);

    exit(0);
}

        创建两个线程分别对变量count加上loops次数,使用互斥锁保护变量count,防止数据读取错误。运行结果如下:

        如果把互斥锁注释点,可以发现运行结果如下,会导致数据不一致:

 

pthread_mutex_trylock()函数
        在线程A持有互斥锁期间,如果线程B调用pthread_mutex_lock()函数获取互斥锁,会持续阻塞,直到线程A释放。

        针对不想被阻塞的情况,Linux提供了pthread_mutex_trylock()函数,其函数原型如下:

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

参数 mutex 指向目标互斥锁,成功返回 0,失败返回一个非 0 值的错误码,如果目标互斥锁已经被其它
线程锁住,则调用失败返回 EBUSY。

  函数测试如下:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>


static pthread_mutex_t mutex;
static int count = 0;
static int loops;


static void *new_pthread(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {

    while(pthread_mutex_trylock(&mutex)); //以非阻塞方式上锁
        l_count = count;
        l_count++;
        count = l_count;
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;   //定义两个线程
    int ret;
    
    loops = atoi(argv[1]);  //保存循环次数

    /* 初始化互斥锁 */
    //pthread_mutex_init(&mutex, NULL);

    //创建第一个线程
    ret = pthread_create(&tid1, NULL, new_pthread, &loops);
    if(ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);        
    }

    //创建第二个线程
    ret = pthread_create(&tid2, NULL, new_pthread, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    ret = pthread_join(tid2, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("count = %d\n", count);

    pthread_mutex_destroy(&mutex);
    exit(0);
}

运行结果如下,和使用 pthread_mutex_lock()效果一样:

三、条件变量 

        条件变量和互斥锁很相似,不过条件变量通常和互斥锁搭配使用。        

        条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止。条件变量是和互斥锁一起搭配使用的。 使用条件变量主要包括两个动作:
        ① 一个线程等待某个条件满足而被阻塞;
        ②另一个线程中,条件满足时发出“信号”。

条件变量的初始化:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

参数 cond 指向 pthread_cond_t 条件变量对象
函数调用成功返回 0,失败将返回一个非 0 值的错误码

通知条件变量:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal()和 pthread_cond_broadcast()的区别在于:二者对阻塞于 pthread_cond_wait()
的多个线程对应的处理方式不同, pthread_cond_signal()函数至少能唤醒一个线程,而
pthread_cond_broadcast()函数则能唤醒所有线程。 

等待条件变量:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

cond: 指向需要等待的条件变量,目标条件变量;
mutex: 参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象;前面开头便给大家介绍
了,条件变量通常是和互斥锁一起使用。

返回值: 调用成功返回 0;失败将返回一个非 0 值的错误码

测试代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>


static pthread_mutex_t mutex; //定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_avail = 0; //全局共享资源


/* 消费者线程 */
static void *consumer_thread(void *arg)
{
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁

    while (0 >= g_avail)
        pthread_cond_wait(&cond, &mutex);//等待条件满足

    while (0 < g_avail)
        g_avail--; //消费

    pthread_mutex_unlock(&mutex);//解锁
    }
    return (void *)0;
}


/* 主线程(生产者) */
int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret;
    /* 初始化互斥锁和条件变量 */
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        g_avail++; //生产
        pthread_mutex_unlock(&mutex);//解锁
        pthread_cond_signal(&cond);//向条件变量发送信号
    }
    exit(0);
}

全局变量 g_avail 作为主线程和新线程之间的共享资源,两个线程在访问它们之间首先会对互斥锁进行上锁,消费者线程中,当判断没有产品可被消费时(g_avail <= 0),调用pthread_cond_wait()使得线程陷入等待状态,等待条件变量,等待生产者制造产品;调用pthread_cond_wait()后线程阻塞并解锁互斥锁;而在生产者线程中,它的任务是生产产品(使用g_avail++来模拟),产品生产完成之后,调用pthread_mutex_unlock()将互斥锁解锁,并调用 pthread_cond_signal()向条件变量发送信号;这将会唤醒处于等待该条件变量的消费者线程,唤醒之后再次自动获取互斥锁,然后再对产品进行消费(g_avai--模拟)。

 四、自旋锁

        自旋锁的使用方法和互斥锁很相似。

自旋锁与互斥锁之间的区别:

        实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
        开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠) ,直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁; 休眠与唤醒开销是很大的, 所以互斥锁的开销要远高于自旋锁、 自旋锁的效率远高于互斥锁; 但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
        使用场景的区别: 自旋锁在用户态应用程序中使用的比较少, 通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占) , 一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁。

自旋锁初始化

#include <pthread.h>

int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

参数 lock 指向了需要进行初始化或销毁的自旋锁对象
参数 pshared 表示自旋锁的进程共享属性,可以
取值如下:
    PTHREAD_PROCESS_SHARED: 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
    PTHREAD_PROCESS_PRIVATE: 私有自旋锁。只有本进程内的线程才能够使用该自旋锁

自旋锁加锁和解锁 

        pthread_spin_lock()函数或 pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为 EBUSY。

#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

五、信号量 

        信号量本质上是一个计数器,用于多进程对共享数据对象的读取,,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

        信号量有关函数:

头文件:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> 

创建信号量: 

int semget(key_t key,int nsems,int flags);

(1)第一个参数key是长整型(唯一非零)。

(2)第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1。

(3)第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。一般我们会还或上一个文件权限

删除和初始化信号量 :

int semctl(int semid, int semnum, int cmd, ...);

(1)sem_id是由semget返回的信号量标识符

(2)semnum当前信号量集的哪一个信号量

(3)cmd通常是下面两个值中的其中一个
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符,删除的话就不需要缺省参数,只需要三个参数即可。

第四个参数一般设置为union semnu arg;定义如下:
union semun
{ 
    int val;  //使用的值
    struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区
    unsigned short *arry;  //GETALL,、SETALL 使用的数组
    struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};

改变信号量的值 :

int semop(int semid, struct sembuf *sops, size_t nops);

(1)nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作

(2)sembuf的定义如下:
struct sembuf{ 
    short sem_num;   //除非使用一组信号量,否则它为0 
    short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,                                        
                    //一个是-1,即P(等待)操作, 
                    //一个是+1,即V(发送信号)操作。 
    short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量, 
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量 
}; 
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

15.线程同步的几种方法 的相关文章

  • 无法加载 JavaHL 库。- linux/eclipse

    在尝试安装 Subversion 插件时 当 Eclipse 启动时出现此错误 Failed to load JavaHL Library These are the errors that were encountered no libs
  • 强制卸载 NFS 安装目录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Locked 这个问题及其答案是locked help locked posts因为这个问题是题外话 但却具有历史意义 目前不接受新的答案
  • 如何使用 bash 锁定文件

    我有一个任务从远程服务器同步目录 rsync av email protected cdn cgi l email protection srv data srv data 为了使其定期运行并避免脚本 reEnter 问题 我使用 rsyn
  • Linux TUN/TAP:无法从 TAP 设备读回数据

    问题是关于如何正确配置想要使用 Tun Tap 模块的 Linux 主机 My Goal 利用现有的路由软件 以下为APP1和APP2 但拦截并修改其发送和接收的所有消息 由Mediator完成 我的场景 Ubuntu 10 04 Mach
  • Linux中的CONFIG_OF是什么?

    我看到它在很多地方被广泛使用 但不明白在什么场景下我需要使用它 What is 配置 OF OF 的全名是什么 打开固件 这是很久以前发明的 当时苹果公司正在生产基于 PowerPC CPU 的笔记本电脑 而 Sun Microsystem
  • 我可以从命令行打印 html 文件(带有图像、css)吗?

    我想从脚本中打印带有图像的样式化 html 页面 谁能建议一个开源解决方案 我使用的是 Linux Ubuntu 8 04 但也对其他操作系统的解决方案感兴趣 你可以给html2ps http user it uu se jan html2
  • chown:不允许操作

    我有问题 我需要通过 php 脚本为系统中的不同用户设置文件所有者权限 所以我通过以下命令执行此操作 其中 1002 是系统的用户 ID file put contents filename content system chown 100
  • Linux 中的动态环境变量?

    Linux 中是否可以通过某种方式拥有动态环境变量 我有一个网络服务器 网站遵循以下布局 site qa production 我想要一个环境变量 例如 APPLICATION ENV 当我在 qa 目录中时设置为 qa 当我在生产目录中时
  • 所有平台上的java

    如果您想用 java 为 Windows Mac 和 Linux 编写桌面应用程序 那么所有这些代码都相同吗 您只需更改 GUI 即可使 Windows 应用程序更像 Windows 等等 如果不深入细节 它是如何工作的 Java 的卖点之
  • 添加要在给定命令中运行的 .env 变量

    我有一个 env 文件 其中包含如下变量 HELLO world SOMETHING nothing 前几天我发现了这个很棒的脚本 它将这些变量放入当前会话中 所以当我运行这样的东西时 cat env grep v xargs node t
  • arm64和armhf有什么区别?

    Raspberry Pi Type 3 具有 64 位 CPU 但其架构不是arm64 but armhf 有什么区别arm64 and armhf armhf代表 arm hard float 是给定的名称Debian 端口 https
  • Linux 内核标识符中前导和尾随下划线的含义是什么?

    我不断遇到一些小约定 比如 KERNEL Are the 在这种情况下 是内核开发人员使用的命名约定 还是以这种方式命名宏的语法特定原因 整个代码中有很多这样的例子 例如 某些函数和变量以 甚至 这有什么具体原因吗 它似乎被广泛使用 我只需
  • 在 Linux 上使用多处理时,TKinter 窗口不会出现

    我想生成另一个进程来异步显示错误消息 同时应用程序的其余部分继续 我正在使用multiprocessingPython 2 6 中的模块来创建进程 我试图用以下命令显示窗口TKinter 这段代码在Windows上运行良好 但在Linux上
  • 使用 sh 运行 bash 脚本

    我有 bash 脚本 它需要 bash 另一个人尝试运行它 sh script name sh 它失败了 因为 sh 是他的发行版中 dash 的符号链接 ls la bin sh lrwxrwxrwx 1 root root 4 Aug
  • iptables通过注释删除特定规则

    我需要删除一些具有相同评论的规则 例如 我有带有 comment test it 的规则 所以我可以像这样获得它们的列表 sudo iptables t nat L grep test it 但是我怎样才能删除所有带有注释 测试它 的 PR
  • Linux 中什么处理 ping?

    我想覆盖 更改 linux 处理 ping icmp echo 请求数据包的方式 这意味着我想运行自己的服务器来回复传入的 icmp 回显请求或其他 数据包 但为了使其正常工作 我想我需要禁用 Linux 的默认 ping icmp 数据包
  • ftrace:仅打印trace_printk()的输出

    是否可以只转储trace printk 输出于trace文件 我的意思是过滤掉函数跟踪器 或任何其他跟踪器 中的所有函数 一般来说 您可以在选项目录中关闭选项 sys kernel debug tracing options Use ls显
  • 如何通过ssh检查ubuntu服务器上是否存在php和apache

    如何通过ssh检查Ubuntu服务器上apache是 否安装了php和mysql 另外如果安装的话在哪个目录 如果安装了其他软件包 例如 lighttpd 那么它在哪里 确定程序是否已安装的另一种方法是使用which命令 它将显示您正在搜索
  • 如何确保应用程序在 Linux 上持续运行

    我试图确保脚本在开发服务器上保持运行 它会整理统计数据并提供网络服务 因此它应该会持续存在 但一天中有几次 它会因未知原因而消失 当我们注意到时 我们只需再次启动它 但这很麻烦 并且某些用户没有权限 或专有技术 来启动它 作为一名程序员 我
  • 如何使用 GOPATH 的 Samba 服务器位置?

    我正在尝试将 GOPATH 设置为共享网络文件夹 当我进入 export GOPATH smb path to shared folder I get go GOPATH entry is relative must be absolute

随机推荐