线程同步学习

概念

有A、B、C三个线程,A线程负责输入数据,B线程负责处理数据、C线程负责输出数据,这三个线程之间就存在着同步关系,即A必须先执行,B次之,C最后执行,否则不能得到正确的结果。

那么所谓线程同步,是指多个线程在发生的事件存在着某种时序关系,它们必须按规定时间执行,以共同完成一项任务。多个线程在访问共享资源时,通过一定的机制来协调它们的执行顺序,以确保共享资源在任何时刻都能被正确地访问和修改。

为什么需要线程同步呢?

在多线程编程中,不同的线程可能会同时访问和修改同一个共享资源。如果没有适当的同步机制,可能会导致数据不一致、资源竞争等问题。例如,两个线程同时对一个计数器进行递增操作,如果不进行同步,可能会导致计数器的值不正确。

还记得上一篇博客中讲过的一个知识点就是线程的竞争能力会有不同,竞争能力强的线程可能从头到尾都占用一个锁,所以线程同步是来解决这种情况的。实现线程同步的方法主要有三种,分别是互斥锁、信号量和条件变量。本博客主要是讲解使用条件变量的方法来实现线程同步。

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。条件变量通常与互斥锁一起使用,用于线程之间的等待和通知。

当一个线程需要等待某个条件满足时,它可以使用条件变量进行等待。在等待之前,线程必须先获取互斥锁,以确保对共享资源的访问是安全的。

条件变量的函数接口跟互斥锁的基本一样,定义方式也一样,可以局部或者全局定义。它存在于<pthread.h>头文件中,类型是pthread_cond_t
全局定义

函数原型:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

变量名可以自己定义,全局的条件变量不需要手动销毁。


局部定义

条件变量的局部定义需要自己手动创建和销毁,这些都需要函数接口来实现。我们接下来就来学习这些接口。
pthread_cond_init

功能:用于初始化一个条件变量。

函数原型:int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

参数:

  • cond:要初始化的条件变量,类型是pthread_cond_t *的指针。
  • attr:条件变量的属性,一般设置为nullptr即可。

返回值:成功返回0,失败返回错误码。


pthread_cond_destory

功能:用于销毁一个条件变量。

函数原型:int pthread_cond_destroy(pthread_cond_t *cond);

返回值:成功返回0,失败返回错误码。


pthread_cond_wait

功能:用于让一个线程进入等待队列等待,直到被唤醒。

函数原型:int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

参数:

  • cond:指向条件变量的指针。
  • mutex:指向互斥锁的指针。在调用pthread_cond_wait之前,线程必须已经持有这个互斥锁。传入锁的指针是为了帮助这个线程释放该锁,从而让其他线程也能够轻松申请锁进入等待队列等待。

返回值:成功返回0,失败返回错误码。


进入到等待队列之后,可以有2种方式唤醒队列中的线程。分别是唤醒一个线程和唤醒所有线程。
pthread_cond_signal

功能:唤醒等待队列中的第一个线程。

函数原型:int pthread_cond_signal(pthread_cond_t *cond);

返回值:成功返回0,失败返回错误码。

pthread_cond_broadcast

功能:唤醒等待队列中的所有线程。

函数原型:int pthread_cond_broadcast(pthread_cond_t *cond);


接下来,我将用图片的方式来帮助大家理解条件变量函数接口的使用方法。

如图所示,有三个线程和一个临界资源,现在线程都要去争夺临界资源。

线程1先申请到了锁,但是我们给临界资源添加了条件变量,所以线程1不能直接进入到临界资源,而是要先到等待队列当中去,并且要释放锁。

线程2和线程3也是一样,都需要先申请锁,然后发现我们给临界资源添加了条件变量,所以要先去等待队列当中去,并且释放锁。

现在所有的线程都进入到队列当中了,先加入到等待队列是为了保持线程同步 。也就是谁先访问的临界资源,最后谁就第一个访问。还记得上一篇博客中举的VIP自习室的例子吗

绿色代表的是已经在等待队列当中的线程了,红色代表的是新来的线程,他要先到自习室的门口,发现自习室里面有人,于是他需要排到队列的尾部去,直到轮到他进入到临界资源当中。而自习室当中的人要是临时有事,需要离开,那么他要先把钥匙归还并且还需要摇响铃铛,告诉下一个人该到他自习了,当解决完事情之后,需要重新排队才行。

如果使用pthread_cond_signal接口唤醒队列的第一个线程,那么线程就可以重新获得之前的锁,进入到临界资源当中去。

假设线程1执行完临界区的代码之后,又重新申请了锁,由于条件变量的存在,线程1不能直接访问临界资源,必须要到等待队列的尾部才行。这样就能避免一个线程一直访问临界资源。

如果使用pthread_cond_broadcast接口唤醒所有的线程,那么就会让所有的线程再次竞争同一把锁,谁先竞争到谁就访问临界资源,当访问完毕后,剩下的线程继续竞争,再访问临界资源。

现在通过代码来理解这些接口有什么作用。

cpp 复制代码
int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *Count(void *args)
{
    pthread_detach(pthread_self());
    uint64_t number = (uint64_t)args;
    cout << "pthread: " << number << "create sucess" << endl;
    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        cout << "pthread: " << number << ", cnt: " << cnt++ << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    for (uint32_t i = 0; i < 5; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, Count, (void *)&i);
        usleep(1000);
    }
    sleep(3);
    cout << "main thread ctrl begin: " << endl;
    while (1)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        // pthread_cond_broadcast(&cond);
        cout << "signal one thread..." << endl;
    }
    return 0;
}

先创建一个全局变量cnt用来观察每个线程的变化,然后再全局初始化mutexcond。在主函数当中,创建了5个线程,休眠3秒后,接着在循环当中每过一秒使用signal接口唤醒队列当中的每一个线程,而线程执行的函数Count,进入while循环后,通过pthread_mutex_lock加锁,试图访问临界资源。但是我们使用了条件变量进行限制,需要先通过pthread_cont_wait进入等待队列,并且释放掉申请的锁,唤醒线程则是到主线程当中的pthread_cond_signal接口来唤醒,每过一秒钟唤醒一次。

然后我们来看代码结果:

这分别是用2个唤醒接口运行出来的结果,我们可以看到signal接口,每秒钟唤醒一个接口,5个线程都唤醒后,又重新开始;而broadcast接口,一次唤醒所有的线程,也可以看到第一次时3号线程的竞争能力强,第二次时0号线程的竞争能力强,但可以确定的是,每个轮次中,每个线程都获得了一次临界资源,这也算是线程同步。

相关推荐
正经教主几秒前
【基础】Windows开发设置入门4:Windows、Python、Linux和Node.js包管理器的作用和区别(AI整理)
linux·windows·python·包管理器
Zfox_7 分钟前
RPM 包制作备查 &SRPM 包编译
linux·rpm·srpm
MaCa .BaKa30 分钟前
38-日语学习小程序
java·vue.js·spring boot·学习·mysql·小程序·maven
贺函不是涵43 分钟前
【沉浸式求职学习day41】【Servlet】
java·学习·servlet·maven
Excuse_lighttime43 分钟前
JVM 机制
java·linux·jvm
YOYO--小天1 小时前
4G和5G模块的使用
linux·嵌入式硬件·5g
愚润求学1 小时前
【Linux】进程间通信(一):认识管道
linux·运维·服务器·开发语言·c++·笔记
霸王蟹1 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
持之以恒的天秤1 小时前
多线程与线程互斥
linux