前言:
生产者消费者模型是老生常谈的话题,实现手段也是各种各样,不同的手段的 运行效率也是天壤之别。代码简洁度,数据安全性,运行稳定性,运行性能等等要素很难做到兼顾。
最基本的模型 -> 大粒度锁 + 忙等(循环check / busy check)
组件:
mutex
代码:
#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>std::list<long> FIFO;
std::mutex lock;
long consumer_v = -1;
long producer_v = 9999999;void consumer(){static long times=0;while(consumer_v!=0){std::unique_lock<std::mutex> ul(lock);if(!FIFO.empty()){consumer_v =std::move(FIFO.front());FIFO.pop_front();times++;}else{//usleep(1); //降低轮询次数以节省cputimes++;}}printf("consumer times : %ld\n" , times);
}void producer(){static long times=0;while(producer_v--!=0){std::unique_lock<std::mutex> ul(lock);FIFO.push_back(producer_v);times++;}printf("producer times : %ld\n" , times);
}int main()
{std::thread cons(consumer);std::thread prod(producer);cons.join();prod.join();
}
以上代码中,cpu通常会达到200%,原因是 consumer 中需要判断FIFO 中是否有数据,如果没有数据要再次加锁和判断,因此这数据 busy check 代码结构,这个过程会非常耗费 cpu 。
通过top命令查看:
%CPU
200.0
可以通过usleep来降低轮询频率从而降低cpu ,但是弊端代码就是执行时间会变长。
$ time ./1
producer times : 9999999
consumer times : 10002394real 0m16.661s
user 0m19.614s
sys 0m13.541s
每次运行上述代码都会发现输出结果中,consumer times 的值会有很大波动,有时比 producer times 大几百,有时大几千,这些就是无用轮询的次数。
优缺点:
优点:代码简洁易懂,方便阅读和修改,逻辑清晰。
缺点:
1)cpu和运行效率无法兼得,要么cpu忙(这往往是绝对无法接收的);
2)要么运行效率无法得到保障(sleep间隔长了则效率低,短了则cpu忙);
3)竞争数据的加锁粒度大,一次性把整个list都锁住了。不过这一点不是太大的问题,而且优化起来难度较高,一般属于无锁编程范畴。不属于严重的缺点。
改善CPU的模型 -> 大粒度锁 + 休眠唤醒
为了改善 cpu 忙等问题,可以使用休眠唤醒机制。把唤醒工作交给内核,达到在不进行 busy check 的前提下,还可以来提升等待线程的响应效率的目的。
组件:
conditional variable / semaphore
其他:
当我们锁住列表的时候,释放锁的时机要控制好,建议通过 std::move 把需要处理的数据从 FIFO 中拿出来,或者 通过拷贝的方式拷贝拿出来,然后立刻就把锁释放掉,这样不会影响其他线程加锁。不可以在锁住状态中执行耗时操作,除非你有充分的理由或者知道自己在干啥。