阅读了unix网络编程的卷二之后,看着里面的实例并且理解其原理算法,就将里面的C语言的锁API进行C++封装以供以后使用。实现的锁接口以及一些算法会封装到我的TimePass库中。我觉得应该就锁的问题写一个系列的博客。锁按照其作用域可以分为线程锁和进程锁; 按照其工作方式:又可以分为互斥锁,读写锁,文件锁。读写锁也是互斥,只是相对于读写锁来说更加精细,其分为读和写,读与读不会互斥,读和写会互斥,写与写也会互斥。文件锁有相对于读写锁来说更加精细了,对整个文件,可以分区段进行加锁。
Table of Contents
1 互斥锁的API
(1) int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr *mutexattr);
初始化mutex; pthreadmutexattrt里面的shared字段设置成PTHREAD_PROCESS_SHARE,可以通过在共享内存里面创建锁,实现进程间的互斥锁。
(2) int pthread_mutex_destroy(pthread_mutext *mutex);
销毁锁资源
(3) int pthread_mutex_lock(pthread_mutext *mutex);
获取锁的资源,如果已经有进程或者线程获取到了就阻塞掉
(4) int pthread_mutex_unlock(pthread_mutext *mutex);
释放所资源,在阻塞的进程中或者线程中,按照阻塞的队列顺序获取锁资源
(5) int pthread_mutex_trylock(pthread_mutext *mutex);
尝试获取所资源,如果恰巧所资源被占用,就会返回EBUSY的错误码; 其不会像pthread_mutex_lock阻塞掉。这个函数可以用来查看锁的占用状况。
以上如果仅仅是实现互斥锁的封装已经够用了,互斥锁的类名为Mutex,其数据成员如下:
class Mutex {
public: //成员函数 …. private: pthread_mutex_t mutex_; }
(7) int pthread_cond_init(pthread_condt *cond, pthread_cond_attrt * condattr);
初始化条件资源
(8) int pthread_cond_destroy(pthreadcondt *cond);
销毁条件资源
(9) int pthread_cond_wait(pthreadcondt *cond, pthreadmutext *mutex);
将mutex与cond进行绑定,当执行wait的时候,首先对mutex进行解锁,然后再进入阻塞队列,直到pthreadcondsignal通知,才唤醒; 紧接着占用锁资源,才向下执行。
(10)int pthreadcondsignal(pthreadcondt *cond);
通过条件资源唤醒互斥锁
以上通过这些函数可以实现条件锁:
class CondMutex:public Mutex{
public: //成员函数 …. private: pthread_cond_t cond_; }
2 消费者和生产者的原理
想象一下,在一条生产线上,多台机器来生产物品,一台机器来打包物品;这其中生产线就是一个队列,生产物品的机器就是生产者,打包物品的机器就是消费者。在计算机算法中,就生产者和消费者而言,生产物品的机器就是生产者,消费物品的机器是消费者。每个生产者分别生产完一次物品,停止下来,让消费者去消费物品直到消费者消费完所有物品,停下来;生产者再去继续生产,并且第一个生产者通知生产线上已经有物品了,让消费者继续消费。就这样不停的往返反复,生产者和消费者共同去维护一个队列,在这过程中,生产者和消费者能够达到并发。
为什么不是多个生产者和多个消费者,这样子速度不是更快么?
为了维持队列的顺序,生产者和生产者是互斥的,消费者与消费者是互斥的,唯有生产者和消费不互斥。多个生产者确实有它的优势,当只有一个生产者的时候,生产线上有了一个产品之后,生产线停止,让消费者去消费,这样子就完全没有达到消费者和生产者的并发,只是串行的。当生产线上的物品都消耗完了,一个消费者会解锁,并且进入等待队列,这时候可能是另一个在等待队列中的消费者获取到锁资源,造成这个消费者饥饿消费;其次当生产者生产了物品,去通知消费者消费,要一个一个的去通知,即使采用群体唤醒,消费者们是互斥的;况且当消费过程是一个简短的过程,一个线程进行消费和多线程加锁进行消费比起来并不会慢,反而加锁和解锁会造成性能消耗的。注意只有当消费过程是一个比较耗时的过程,才会考虑有没有加锁的必要。
为什么多个生产者分别生产一次物品,就会停下来?
毕竟生产者消费者问题并不会像富士康生产线那样,流水线上两端的工人步调一致,生产线两头的工人不需要互相通知。一个生产者生产玩一次物品之后,会有一个通知的过程,为了防止多个线程同时通知,造成紊乱,就对通知过程也加锁了,而这个锁就是消费用的锁。当通知过去的时候,消费者会消费完当前物品,直到释放锁(消费者消费的过程中一直加锁解锁,可以在解锁的时候,sleep一下,同时也在生产者的过程中sleep一下,可以防止过度的消费或者过度的生产,尽量让消费者和生产者步调一致,不过sleep的时间不容易把握)。
3 消费者和生产者的实现
1 //生产者线程函数 2 void* produce (void* arg) { 3 while (生产没结束) { 4 produce_lock() 5 if (生产结束) { 6 produce_unlock() 7 return NULL; 8 } 9 //往生产线上放数据 10 begin_produce(); 11 produce_unlock(); 12 13 //这里的锁运行速度很快 14 consume_lock(); 15 if (满足通知条件) { 16 //通知消费者去生产 17 pthread_cond_signal(); 18 consume_unlock() 19 } 20 21 }
这是生产线程,在producelock里面,我们还需要判断生产是否结束,是为了防止这样的情况发生,当多个生产者线程被阻塞了,有一个线程完成了所有的生产任务,如果不判断,其他的生产线程继续往下执行,直到这个此次循环结束为止,会超出生产的量。
1 //消费者线程函数 2 void* consume(void* arg) { 3 while (生产没结束 || 生产线上还有数据) { 4 consume_lock(); 5 while (等待条件) { //注意这个条件与上面的通知条件是相反的 6 //等待生产者的通知 7 consume_wait(); 8 } 9 begin_consume() 10 consume_unlock() 11 } 12 }
等待生产者通知之所以放到while里面,是为了防止consume_wait调用失败,造成饥饿消费.
以上是用互斥锁实现生产者和消费者的思路。在这里为了方便使用,我实现了一个四个类,类图如下:
以上这个UML图,是用linux下面的Dia画,其完全可以替代visio,而且支持将UML图片转化为各种文件,比如png。
这个生产者和消费者的框架使用起来很简单,只需要实现一个类来继承ProduceConsume,实现其中的纯虚函数。然后启用线程,调用ThreadFuncLoader里面的Produce和Consume就OK了,使用者不用纠结锁的问题。
在这里使用者所要实现的函数有:
Produce对应上面的begin_produce
Consume对应上面的begin_consume
ProduceComplete对应上面的“生产结束”
ConsumeComplete对应上面你的“生产线上没有了数据"
Condition 对应上面的“通知条件”“等待条件”