关于锁的总结(二)条件变量

条件变量

互斥器用于互斥条件变量用于等待
如果上例子中消费者和生产者同时并发,那么需要消费者需要轮询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_waitpthread_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