深入解析锁和条件变量

锁的概念:在CPU运行过程中,不会单一的取执行一个事件,而是通过线程,或者进程来进行执行,这样CPU的利用率才得以提高,但是在不同的线程之间,由于互相独立,那么对于资源的访问来说,就可能同时进行,假如A进程获取一个临时变量temp的值,但是在获取的同时,B进程却将temp的值改变了,这时就会出现资源访问的冲突,为了更好的解决这个问题,就有了锁的概念,说的明白些,他就像现实中的锁一样,我们程序中所有的资源,包括变量,内存等都存放在一个房子里,开始时,锁处于开锁状态,如果某一个线程需要访问资源时,就需要拿到这把锁,进到房子里,把门锁上,这样就不会有其他人来干扰你,等你对资源访问结束后,在把锁打开,放下锁,这样别人就可以进入,这样就保证了对资源访问的顺序,这种锁叫互斥锁,这种机制是我们所说的避免竞争。

条件变量:条件变量是线程同步的一种机制。它给多个线程提供一个回合场所。所谓的条件变量就是需要满足这个条件,才可以继续进行操作。

互斥锁

初始化:

1)动态方式

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
mutex 出参,互斥锁
attr 互斥锁的属性,NULL表示默认/缺省的属性

2)静态的方式
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //一般都选择静态方式

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
给mutex互斥锁加锁
1.互斥锁没有被锁:加锁
2.互斥锁已经被锁:阻塞/等待,直到被解锁,然后再对mutex互斥锁加锁

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

条件变量

什么是条件变量

条件变量是线程的另外一种同步机制,这些同步对象为线程提供了会合的场所,理解起来就是两个(或者多个)线程需要碰头(或者说进行交互-一个线程给另外的一个或者多个线程发送消息),我们指定在条件变量这个地方发生,一个线程用于修改这个变量使其满足其它线程继续往下执行的条件,其它线程则接收条件已经发生改变的信号。

(核心作用)条件变量同锁一起使用使得线程可以以一种无竞争的方式等待任意条件的发生。所谓无竞争就是,条件改变这个信号会发送到所有等待这个信号的线程。而不是说一个线程接受到这个消息而其它线程就接收不到了。

函数介绍

初始化:

动态方式:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
静态的方式:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

等待条件变量

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

原子性的解锁并阻塞/等待条件变量。

唤醒条件变量

int pthread_cond_signal(pthread_cond_t *cond);

实例分析

下例实现了生产者和消费者模型,生产者向队列中插入数据,消费者则在生产者发出队列准备好(有数据了)后接收消息,然后取出数据进行处理。实现的关键点在以下几个方面:

  • 生产者和消费者都对条件变量的使用加了锁
  • 消费者调用pthread_cond_wait,等待队列是否准备好的信息,注意参数有两个,一个是pthread_cond_t,另外一个是pthread_mutex_t.

代码:

#include "pthread.h"
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void
process_msg(void)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/* now process the message mp */
}
}
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}

配合

上面的是函数的介绍,为了方便查看,接下来说一下为什么cond和mutex要配合使用。

首先:在pthread_cond_wait()函数之前,要人为获取一把锁,在pthread_cond_wait()执行时,会自动释放这个锁,并且处于等待/阻塞条件,等待信号来临,一但信号来临,那么在pthread_cond_wait()函数调用返回之前,自动将指定的互斥量重新锁住,所以必须在pthread_cond_wait()之后再人为释放锁。其结实际用法为下:

pthread_mutex_lock(&mutex); //加锁

pthread_cond_wait(&flag,&mutex); //等待,清除标记flag

pthread_mutex_unlock(&mutex); //解锁
其次:对于pthread_cond_signal()函数有两种用法,第一种在加锁与解锁之间,第二种是在加锁解锁之后:

pthread_mutex_lock(&mutex);

pthread_cond_signal(&flag);

pthread_mutex_unlock(&mutex);

这种方式的缺点是:在某线程中,会遭虫等待线程从内核中唤醒(cond_signal是有内核发起的),然后又回到内核空间(cond_wait返回后会有原子加锁的行为),所以这一来一回会产生性能的问题,但是在Linux下或者NPTL里面不会,因为Linux线程中有两个队列即cond_wait和cond_signal两个队列,pthread_cond_signal()只是让线程在从wait队列移动到cond队列,不会再永和空间和内核之间往返,不会有应能损耗。

pthread_mutex_lock(&mutex);

pthread_mutex_unlock(&mutex);

pthread_cond_signal(&flag);
这种方式的缺点是:如果在unlock和signal之前有一个优先级更低的线程正在等待mutex的话,那么他就会抢占高优先级的线程,而上面的情况则不会出现。

优点是:不会产生性能的损耗,因为在signal之前就已经解锁了。

cond和mutex两个组合使用是为了避免,在一个线程中,如果在wait之前,另一个函数已经完成signal了,那么这个线程将阻塞在这里,从而错过了signal,所以用mutex来实现两个进程的同步,当我等待前上锁,等待后释放锁,然后另一个线程获取锁,产生signal信号,在释放锁。

线程结束时发生死锁

现象

媒控在红外的回调函数中,对ISP内部线程进行了关闭。在关闭函数中使用了pthread_cancel发送进程终止信号,之后用join等待线程的关闭。
经过检查,需要在线程中加入pthread_testcancel(),对线程终止信号进行处理,退出线程。然而并没有解决问题。
它的回调也在此线程之中,这就带来了一个问题。

线程死锁的原因

线程A控制着线程B的运行和销毁。线程B中执行一个回调函数,会调用到线程A中对线程B的释放。线程A发送了一个线程销毁的信号并进行等待,但是必须等到线程B对这个销毁信号进行处理才能推出。而线程B又需要等待A执行完这个释放函数,才能去对销毁信号进行处理。因此发生了死锁。

解决方案

单独创建一个回调函数的线程C,和线程AB分离开来,在线程C中调用就没有什么问题了