首先理解下: 资源管理——线程同步的意思
因为进程内的线程分享相同的资源,所以需要在系统级别上设置控制机制来保证数据的完整性。当一个线程修改了某个变量,而另一个线程试图读取它时,或者两个线程同时修改同一变量,就会影响到数据的完整性,为防止这个问题,操作系统提供了一种相互排斥对象,简写为mutex。在多线程程序中,mutex是通过编程来实现的,可防止多个线程在同一时间访问同一资源。当一个线程需要访问某一资源时,它必须先请求一个mutex,一旦线程得到一个mutex,其他想获取同一mutex的线程被阻塞,并处于低CPU占用的等待状态;一旦这个线程完成了数据访问,它会释放对应的mutex,这就允许其他线程获取以访问相关的数据。
如果mutex实现得不好,将会导致资源饥饿,也就是常说的死锁。资源饥饿发生在当一个或多个线程竞争同一资源时,如果一个线程请求一个mutex两次,也可能会发生死锁
互斥器(mutex)是加锁原语,用来排他性地访问共享数据,它不是等待原语。在使用mutex的时候,我们一搬都会期望加锁不要阻塞,总是能立刻拿到加锁。然后尽快访问数据,用完之后尽快解锁,这样才不影响并发性和性能,如果需要等待某个条件成立,我们就应该使用如下的条件变量。
与mutex类似,Unix中的条件变量,也是一种同步机制。条件变量允许线程会合,可让一个线程在有变化时通知另一个线程,这在Windows中,被称为events。。条件变量(condition variable)是在多线程程序中用来实现"等待--》唤醒"逻辑常用的方法。
条件变量利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
一个线程等待"条件变量的条件成立"而挂起;
另一个线程使“条件成立”。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量,函数pthread_cond_wait把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。
条件变量的使用简化如下:
条件变量只有一种正确的使用方式,几乎不可能用错。
对于wait端:
1、必须与mutex一起使用,该布尔表达式的读写需要受此mutex保护。
2、在mutex已上锁的时候才能调用wait()
3、把布尔表达条件和wait()放到while循环中
对于signal或broadcast端
1、修改布尔表达式通常要用mutex来保护
2、在signal之前一般要修改布尔表达
3、注意区分signal和broadcast的区别:
signal通常用于表示资源可用,broadcast通常用于表明状态变化
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
#define CONSUMERS_COUNT 1
#define PRODUCERS_COUNT 1
unsigned short in = 0;
unsigned short out = 0;
unsigned short produce_id = 0;
unsigned short consume_id = 0;
pthread_mutex_t g_mutex;
pthread_cond_t g_cond;
pthread_t g_thread[CONSUMERS_COUNT+PRODUCERS_COUNT];
int nready=0;
void* consume(void *arg)
{
int i=0;
int num = (int)arg;
while (1)
{
pthread_mutex_lock(&g_mutex);
while(nready==0)
//if(nready==0)
{
printf("%d.....begin wait a condtion..\n",num);
pthread_cond_wait(&g_cond,&g_mutex);
}
printf("%d.....end wait a condtion..\n",num);
printf("%d.....begin consume produce..\n",num);
--nready;
printf("%d.......end consume produce......\n",num);
pthread_mutex_unlock(&g_mutex);
sleep(1);
}
return NULL;
}
void* produce(void *arg)
{
int num = (int)arg;
int i;
while (1)
{
pthread_mutex_lock(&g_mutex);
printf("%d begin produce product....\n",num);
++nready;
printf("%d end produce product.....\n",num);
// pthread_mutex_unlock(&g_mutex);
// sleep(1);
if(nready>0)
pthread_cond_signal(&g_cond);
printf("%d.....signal......\n",num);
pthread_mutex_unlock(&g_mutex);
sleep(1);
}
return NULL;
}
int main(void)
{
int i=0;
pthread_mutex_init(&g_mutex,NULL);
pthread_cond_init(&g_cond,NULL);
for (i=0; i<CONSUMERS_COUNT; i++)
pthread_create(&g_thread[i], NULL, consume, (void*)i);
sleep(1);
for (i=0; i<PRODUCERS_COUNT; i++)
pthread_create(&g_thread[CONSUMERS_COUNT+i], NULL, produce, (void*)i);
for (i=0; i<CONSUMERS_COUNT+PRODUCERS_COUNT; i++)
pthread_join(g_thread[i], NULL);
pthread_mutex_destroy(&g_mutex);
pthread_cond_destroy(&g_cond);
return 0;
}
(一、)pthread_cond_wait
1、对g_mutex进行解锁2、等待条件,知道有线程向它发起通知
3、重新对g_mutex进行加锁操作
(二、)pthread_cond_signal
向第一个等待条件的线程发起通知,如果没有任何一个线程处理等待条件的状态,这个通知将被忽略
(三、)pthread_cond_brocadcase向所有等待线程发起通知
(四、)为什么用等待线程中用while循环?
这是因为可能会存在虚假唤醒”spurious wakeup”的情况。也就是说,即使没有线程调用pthread_cond_signal, 原先调用
pthread_cond_wait的函数也可能会返回。此时线程被唤醒了,但是条件并不满足,这个时候如果不对条件进行检查而往下执行,就
可能会导致后续的处理出现错误。虚假唤醒在linux的多处理器系统中/在程序接收到信号时可能回发生。在Windows系统和JAVA虚拟
机上也存在。在系统设计时应该可以避免虚假唤醒,但是这会影响条件变量的执行效率,而既然通过while循环就能避免虚假唤醒造
成的错误,因此程序的逻辑就变成了while循环的情况。注意:即使是虚假唤醒的情况,线程也是在成功锁住mutex后才能从
pthread_cond_wait()中返回。即使存在多个线程被虚假唤醒,但是也只能是一个线程一个线程的顺序执行,也即:
pthread_mutex_lock 检查/处理 pthread_condition_wai()或者pthread_mutex_unlock(mutex)来解锁.
(五、)解锁和等待转移(wait morphing) 解锁互斥量mutex和发出唤醒信号pthread_cond_signal是两个单独的操作,那么就存在一个顺序的问题。谁先随后可能会产生不同的结果。
如下:
(1) 按照 unlock(mutex); pthread_cond_signal()顺序, 当等待的线程被唤醒时,因为mutex已经解锁,因此被唤醒的线程很容易就锁住了mutex然后从pthread_cond_wait()中返回了。
(2) 按照 pthread_cond_signal(); unlock(mutext)顺序,当等待线程被唤醒时,它试图锁住mutex,但是如果此时mutex还未解锁,则线程又进入睡眠,mutex成功解锁后,此线程在再次被唤醒并锁住mutex,从而从pthread_cond_wait()中返回。
可以看到,按照(2)的顺序,对等待线程可能会发生2次的上下文切换,严重影响性能。因此在后来的实现中,对(2)的情况,如果线程被唤醒但是不能锁住mutex,则线程被转移(morphing)到互斥量mutex的等待队列中,避免了上下文的切换造成的开销。
--wait morphing 编程时,推荐采用(1)的顺序解锁和发唤醒信号。而Java编程只能按照(2)的顺序,否则发生异常
Linux系统中说明了这两种顺序不管采用哪种,其实现效果都是一样的。