【Linux】多线程4——线程同步/条件变量

1.Linux线程同步

1.1.同步概念与线程饥饿问题

先来理解同步的概念

  • 什么是线程同步

        在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

“同”字从字面上容易理解为一起动作,但其实不是,“同”字应是指协同、协助、互相配合。

        如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

        所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。

  • 同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
  • 竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。
  • 线程饥饿问题

        首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题。

        单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源。

        现在我们增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。

        增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问。

        例如,现在有两个线程访问一块临界区,一个线程往临界区写入数据,另一个线程从临界区读取数据,但负责数据写入的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就一直在执行写入操作,直到临界区被写满,此后该线程就一直在进行申请锁和释放锁。而负责数据读取的线程由于竞争力太弱,每次都申请不到锁,因此无法进行数据的读取,引入同步后该问题就能很好的解决。

1.2.条件变量

我们怎么实现线程同步呢?这需要学习Linux的条件变量。

  • 什么是条件变量?该不会真就是1个变量吧!!!

千万不要被误导了,条件变量可不是变量, 条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

条件变量通常需要配合互斥锁一起使用。

        互斥量可以防止多个线程同时访问临界资源,而条件变量允许一个线程将某个临界资源的状态变化通知其他线程,在共享资源设定一个条件变量,如果共享资源条件不满足,则让线程到该条件变量下阻塞等待,当条件满足时,其他线程可以唤醒条件变量阻塞等待的线程。

        在线程之间有一种情况:线程A需要某个条件才能继续往下执行,如果该条件不成立,此时线程A进行阻塞等待,当线程B运行后使该条件成立后,则唤醒该线程A继续往下执行。

        在pthread库中,可以通过条件变量中,可以设定一个阻塞等待的条件,或者唤醒等待条件的线程。

        条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

条件变量是一种等待机制,每一个条件变量对应一个等待原因与等待队列。一般对于条件变量会有两种操作:

每个条件变量都有属于自己的一个等待队列

  1. wait操作 : 将自己阻塞在等待队列里,唤醒一个等待者或者开放锁的互斥访问
  2. singal 操作 : 唤醒一个等待的线程(等待队列为空的话什么也不做)

1.3.条件变量函数

1.3.1.初始化条件变量

POSIX提供了两种初始化条件变量的方法。

  • 第一种方法

初始化条件变量的函数叫做pthread_cond_init,该函数的函数原型如下:

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

参数说明:

  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为NULL即可。

返回值说明:

  • 条件变量初始化成功返回0,失败返回错误码。
  • 第二种方法

调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

这个相当于调用函数pthread_cond_init()初始化,并且参数attr为NULL。 

1.3.2.销毁条件变量

销毁条件变量的函数叫做pthread_cond_destroy,该函数的函数原型如下:

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • cond:需要销毁的条件变量。

返回值说明:

  • 条件变量销毁成功返回0,失败返回错误码。

销毁条件变量需要注意:

  • 使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁。

1.3.3.等待条件变量满足

        条件变量就是为了与某个条件关联起来使用的,如果条件不满足,就等待(pthread_cond_wait) ,或者等待一段有限的时间(pthread_cond_timedwait) 。

POSIX提供了如下条件变量的等待接口:

        函数描述:这两个函数都是让指定的条件变量进入等待状态,其工作机制是先解锁传入的互斥量,再让条件变量等待,从而使所在线程处于阻塞状态。这两个函数返回时,系统会确保该线程再次持有互斥量(加锁)。

两个函数的区别:

  1. pthread_cond_wait函数调用成功后,会一直阻塞等待,直到条件变量被唤醒。
  2. 而 pthread_cond_timedwait 函数只会等待指定的时间,时间到了之后,条件变量仍未被唤醒的话,会返回一个错误码ETIMEDOUT,该错误码定义在<errno.h>头文件。

参数说明:

  • cond:需要等待的条件变量。
  • mutex:当前线程所处临界区对应的互斥锁。

返回值说明:

函数调用成功返回0,失败返回错误码。

1.3.4.唤醒等待

上面说完了条件等待,接下来介绍条件变量的唤醒。

调用完条件变量等待函数的线程处于阻塞状态,若要被唤醒,必须是其他线程来唤醒。

唤醒等待的函数有以下两个:

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

必须在互斥锁的保护下使用相应的条件变量。否则对条件变量的解锁有可能发生在锁定条件变量之前,从而造成死锁。 

        pthread_cond_signal 负责唤醒等待在条件变量上的一个线程,如果有多个线程等待,是唤醒哪一个呢?Linux内核会为每个条件变量维护一个等待队列,调用了 pthread_cond_wait 或 pthread_cond_timedwait 的线程会按照调用时间先后添加到该队列中。pthread_cond_signal会唤醒该队列的第一个。

        如果没有线程被阻塞在条件变量上,那么调用pthread_cond_signal()将没有作用。

        pthread_cond_broadcast,就是同时唤醒等待在条件变量上的所有线程。前面说过,条件等待的两个函数返回时,系统会确保该线程再次持有互斥量(加锁),所有,这里被唤醒的所有线程都会去争夺互斥锁,没抢到的线程会继续等待,拿到锁后同样会从条件等待函数返回。所以,被唤醒的线程第一件事就是再次判断条件是否满足!

        由于pthread_cond_broadcast函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast函数。

参数说明:

  • cond:唤醒在cond条件变量下等待的线程。

返回值说明:

  • 函数调用成功返回0,失败返回错误码。

使用示例:

我们先下面这样子的

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;

int cnt=0;

void* Count(void*args)
{
	pthread_detach(pthread_self());//分离线程
	long long number=(long long)args;
	while(1)
	{
		cout<<"pthread: "<<number<<endl;
		sleep(3);
	}
}

int main()
{
	for(long long i=0;i<5;i++)
	{
		pthread_t tid;
		pthread_create(&tid,nullptr,Count,(void*)i);
	}
	while(1)
	sleep(1);
}

特别注意:64位平台下面,int类型是4字节,不能和void*指针类型(8字节)进行相互转换 ,所以这里使用long long

        多个执行流向显示器打印,就是往文件里写入,多线程或多进程往同一个文件写入,这个文件就是一种临界资源,不加保护的话,非常容易出现信息干扰。

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;

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());//分离线程
	long long number=(long long)args;
	while(1)
	{
		pthread_mutex_lock(&mutex);//加锁
		//先不管临界资源的情况
		cout<<"pthread: "<<number<<" ,cnt :"<<cnt++<<endl;
		pthread_mutex_unlock(&mutex);//解锁
		sleep(1);
	}
}

int main()
{
	for(long long i=0;i<5;i++)
	{
		pthread_t tid;
		pthread_create(&tid,nullptr,Count,(void*)i);
	}
	while(1)
	sleep(1);
}

我们给打印这条语句加了锁,打印出来的结果也自然不会混乱了

好了,我今天想说的主角可不是屏幕,而是我们的++操作

我们接下来用上我们的条件变量

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;

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());//分离线程
	long long number=(long long)args;

	cout<<"pthread: "<<number<<" creat success !"<<endl;
	while(1)
	{
		pthread_mutex_lock(&mutex);//加锁
		pthread_cond_wait(&cond,&mutex);//先让自己这个线程去条件变量cond的等待队列  
                                        //为什么填在这里?pthread_cond_wait让线程等待的时候会自动释放掉
		//先不管临界资源的情况
		cout<<"pthread: "<<number<<" ,cnt :"<<cnt++<<endl;
		pthread_mutex_unlock(&mutex);//解锁
		sleep(1);
	}
}

int main()
{
	for(long long 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);//每过1秒就唤醒1次
	pthread_cond_signal(&cond);//唤醒在cond的等待队列的1个线程,默认都是第1个
	cout<<"signal one thread..."<<endl;
	}
}

      此时我们会发现唤醒这4个线程时具有明显的顺序性,根本原因是当这若干个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的头部线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行wait,所以我们能够看到一个周转的现象。

我们可以唤醒所有线程

#include <stdio.h>
#include <pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;

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());//分离线程
	long long number=(long long)args;

	cout<<"pthread: "<<number<<" creat success !"<<endl;
	while(1)
	{
		pthread_mutex_lock(&mutex);//加锁
		pthread_cond_wait(&cond,&mutex);//先让别的线程去等待队列  //为什么填在这里?pthread_cond_wait让线程等待的时候会自动释放掉
		//先不管临界资源的情况
		cout<<"pthread: "<<number<<" ,cnt :"<<cnt++<<endl;
		pthread_mutex_unlock(&mutex);//解锁
		sleep(1);
	}
}

int main()
{
	for(long long 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_broadcast(&cond);//唤醒在cond的等待队列的1个线程,默认都是第1个
	cout<<"signal one thread..."<<endl;
	}
}

我为什么要让一个线程去休眠?

一定是临界资源没有就绪,没错,临界资源也是有状态的

你怎么知道临界资源是就绪还是不就绪的?你判断出来的!那判断是访问临界资源吗? 是的,必须是的

        我们需要判断临界资源状态,就得访问临界资源,而我们的线程对临界资源是会修改的,这就注定了这个判断一定要在加锁和解锁之间,这样子别的线程就不能修改我们的临界资源,我们的判断结果也会是正确的

也就是必须是下面这种结构

void* Count(void*args)
{

	while(1)
	{
		pthread_mutex_lock(&mutex);//加锁
		
        pthread_cond_wait(&cond,&mutex);//判断资源情况,
		

		pthread_mutex_unlock(&mutex);//解锁

	}
}

这也是我们为什么需要互斥量的原因 

  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值