关于锁的总结(二)条件变量
条件变量
互斥器
用于互斥
,条件变量
用于等待
如果上例子中消费者和生产者同时并发,那么需要消费者需要轮询
(polling
)或者轮转
(spinning
)
查询buf
状态,看是否满足消费条件
void comsume_wait(int i) {
for( ; ; ) {
pthread_mutex_lock(&shared.mutex);
if (i < shared.sum) {
pthread_mutex_unlock(&shared.mutex);
return;
}
pthread_mutex_unlock(&shared.mutex);
}
}
void *comsume(void *arg) {
(void) arg;
for (int i = 0;i < MAXBUFLEN;i++) {
comsume_wait(i) ;
if (i != shared.buf[i]) {
printf("buf[%d] = %d\n", i, shared.buf[i]);
}
}
return (NULL);
}
这种方式并不友好,实际是对CPU时间
的一种浪费
如果使用条件变量的方式,那么上面的代码即可写成如下
struct Nread {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nread;
} nread = {
PTHREAD_MUTEX_INITIALIZER,
PTHREAD_COND_INITIALIZER,
0
};
void *produce(void *arg) {
for( ; ; ) {
pthread_mutex_lock(&shared.mutex);
if (shared.sum >= MAXBUFLEN) {
pthread_mutex_unlock(&shared.mutex);
return(NULL);
}
shared.buf[shared.sum] = shared.sum; // produce data 1 in shared.buf[shared.sum]
shared.sum++;
pthread_mutex_unlock(&shared.mutex);
pthread_mutex_lock(&nread.mutex);
if (0 == nread.nread) {
pthread_cond_signal(&nread.cond);
}
nread.nread += 1;
pthread_mutex_unlock(&nread.mutex);
*((int *)arg) += 1;
sleep(0);
}
}
void *comsume(void *arg) {
(void) arg;
for (int i = 0;i < MAXBUFLEN;i++) {
pthread_mutex_lock(&nread.mutex);
while (0 == nread.nread) {
pthread_cond_wait(&nread.cond, &nread.mutex);
}
if (i != shared.buf[i]) {
printf("buf[%d] = %d\n", i, shared.buf[i]);
}
nread.nread -= 1;
pthread_mutex_unlock(&nread.mutex);
}
return (NULL);
}
这里面有个问题关于条件变量
的正确做法讨论
我是这么理解的,pthread_cond_wait
、pthread_cond_signal
这个本身就是观察者模式
如果消费者
任务在生产者
任务生产之前注册事件cond
,那么消息就会完全被消费,线程能被唤醒
但如果消费者
任务在生产者
任务生产之后注册事件cond
,那么这个消息就不知道被消费,线程一直在沉睡
所以需要引入0 === nread.nread
这样一条来保证,消费者
是否有必要睡眠
,保证状态上消息
是"逻辑未丢"的(实际还是丢了)
事例可以看陈硕的代码
下面贴链接中一种正确的做法分析
class Waiter5 : public Waiter
{
public:
void wait() override
{
pthread_mutex_lock(&mutex_);
while (!signaled_)
{
pthread_cond_wait(&cond_, &mutex_);
}
pthread_mutex_unlock(&mutex_);
}
void signal() override
{
pthread_mutex_lock(&mutex_);
signaled_ = true;
pthread_mutex_unlock(&mutex_);
pthread_cond_signal(&cond_);
}
private:
bool signaled_ = false;
};
重点理解下面几点:
1.首先一点要引入可以判断是否应该进入休眠状态的条件(变量),避免"逻辑上丢失信号",状态持久化保存能力
2.毕竟cond_是临界资源,代码块是临界区,需要 mutex
保护
需要mutext其实为了保证数据一致性(串行),见爆栈网
Process A Process B
pthread_mutex_lock(&mutex);
while (condition == FALSE)
condition = TRUE;
pthread_cond_signal(&cond);
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
这样任务A永远都是睡眠
这里可以仔细思考一下,当ProcessB被mutex lock保护
1.
condition = TRUE;
pthread_cond_signal(&cond);
最小只需保证condition = TRUE被mutex lock保护就行
2.
pthread_cond_signal(&cond);
condition = TRUE;
那么两行需要都得被保护
3.pthread_cond_wait
等待会释放锁,唤醒时会上锁(很重要,不然会死锁)
4.当被唤醒时,需要使用while
判断,因为可能是spurious wakeups
唤醒
基于上面第3条,多个消费者被唤醒时,会去争抢
锁
,没有抢到锁的线程就属于虚假唤醒了
5.睡眠
跟IO阻塞
一样是阻塞
状态,不占用cpu
资源
(对于挂起
这个词,是有争议的,暂时不使用挂起
,见下文,理解为等待阻塞
)
线程状态
关于线程状态,个人认为java
里面是理解最清晰的
1. 新建(new):新创建了一个线程对象。
2. 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
3. 运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4. 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5. 死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
阻塞
看一下wiki上的定义
Blocked
A process transitions to a blocked state when it cannot carry on without an external change in state or event occurring.
也就是说如果你写了一个while(1)
,占用了cpu
,是不建议使用阻塞
去描述,而应该是running