天天看点

【Linux】线程同步

在介绍线程同步之前,我们需要先了解一下死锁的相关概念。

死锁:在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占有用不会释放的资源而处于一种永久等待的状态。

无论线程还是进程都可能出现死锁

死锁的四个必要条件
  • 互斥:每个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源在未使用完之前不能被强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁的方法
  • 破坏死锁的四个必要条件中的2,3,4条
  • 加锁的顺序保持一致即同步性
  • 避免未释放锁的场景
  • 资源一次性分配

线程同步

上一篇我们讲了Linux线程的互斥,利用锁的机制保证了线程的安全。本篇博客我们将为大家讲解Linux下的同步机制,因为时序问题,而导致程序异常,我们称之为竞争状态。而为了避免发生竞争状态,我们需要在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源称为同步。

在OS中,我们提供互斥机制是为了保证数据安全性,同步机制为了保证合理性。

想要保证线程同步的合理性和高效性,我们就需要用到条件变量。当一个线程互斥地访问某个变量,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量,下面我们来看看条件变量相关的函数,这些函数真的非常像互斥锁。

  1. 初始化条件变量:一般定义全局变量并初始化

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

//参数一:要初始化的条件变量 参数二:一般设置为NULL

  1. 销毁
int pthread_cond_destroy(pthread_cond_t *cond)
  1. 在条件队列中等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

//参数一为要等待的条件变量,参数二为一把互斥锁,这把锁的用途后面讲解

  1. 唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有的条件变量,惊群问题

int pthread_cond_signal(pthread_cond_t *cond); //唤醒某一个线程

使用条件变量例子:

此程序r2中的signal函数每隔1秒唤醒一次r1函数,所以每秒打印一次活动。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1( void *arg )
{
	while ( 1 ){
	pthread_cond_wait(&cond, &mutex);
	printf("活动\n");
	}
} 
void *r2(void *arg )
{
	while ( 1 ) {
	pthread_cond_signal(&cond);
	sleep(1);
	}
}
 int main( void )
{
	pthread_t t1, t2;
	pthread_cond_init(&cond, NULL);
	pthread_mutex_init(&mutex, NULL);
	
	pthread_create(&t1, NULL, r1, NULL);
	pthread_create(&t2, NULL, r2, NULL);
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
} 
           
【Linux】线程同步
为什么pthread_cond_wait函数参数需要传互斥锁:
pthread_mutex_lock(&mutex);
//进入临界区访问临界资源
while (condition_is_false) {
	pthread_cond_wait(&cond);
}
 pthread_mutex_unlock(&mutex);

           
  • 从上面代码中我们发现,当一个线程(可能是读线程)进入临界区时发现此时临界资源不具备读的条件,所以此时就需要另外一个线程对临界资源进行写操作,但是由于我们现在的读线程手中拿着锁,所以写线程拿不到锁,也就无法进入临界区。为了解决这一问题pthread_cond_wait函数在进行等待时会将锁自动释放,然后挂起。 值得注意的是,释放锁和挂起等待这俩步操作合成了一个原子操作。当满足条件被唤醒时,此时读线程同时又获得了互斥锁。
  • 那么释放锁和等待能不能不是原子的操作呢?

    大家先看下面的代码:

pthread_mutex_lock(&mutex);
while (condition_is_false) {
	pthread_mutex_unlock(&mutex);
	//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
	pthread_cond_wait(&cond);
	pthread_mutex_lock(&mutex);
} 
pthread_mutex_unlock(&mutex);

           

由于解锁和等待不是原子操作。调用解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_ cond_ wait。所以解锁和等待必须是一个原子操作。

条件变量使用规范
  1. 等待条件即线程等待
pthread_mutex_lock(&mutex);
while(条件为假)
	pthread_cond_wait(cond,&mutex);
	修改条件
	pthread_mutex_unlock(&mutex);
	
           
  1. 给条件发送信号即唤醒
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
           

下篇博客我们就要使用条件变量来实现经典的生产者消费者模型。