unix网络编程--锁(一)

  阅读了unix网络编程的卷二之后,看着里面的实例并且理解其原理算法,就将里面的C语言的锁API进行C++封装以供以后使用。实现的锁接口以及一些算法会封装到我的TimePass库中。我觉得应该就锁的问题写一个系列的博客。锁按照其作用域可以分为线程锁和进程锁; 按照其工作方式:又可以分为互斥锁,读写锁,文件锁。读写锁也是互斥,只是相对于读写锁来说更加精细,其分为读和写,读与读不会互斥,读和写会互斥,写与写也会互斥。文件锁有相对于读写锁来说更加精细了,对整个文件,可以分区段进行加锁。

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 消费者和生产者的原理

  想象一下,在一条生产线上,多台机器来生产物品,一台机器来打包物品;这其中生产线就是一个队列,生产物品的机器就是生产者,打包物品的机器就是消费者。在计算机算法中,就生产者和消费者而言,生产物品的机器就是生产者,消费物品的机器是消费者。每个生产者分别生产完一次物品,停止下来,让消费者去消费物品直到消费者消费完所有物品,停下来;生产者再去继续生产,并且第一个生产者通知生产线上已经有物品了,让消费者继续消费。就这样不停的往返反复,生产者和消费者共同去维护一个队列,在这过程中,生产者和消费者能够达到并发。

unix网络编程--锁(一)_第1张图片

  为什么不是多个生产者和多个消费者,这样子速度不是更快么?

  为了维持队列的顺序,生产者和生产者是互斥的,消费者与消费者是互斥的,唯有生产者和消费不互斥。多个生产者确实有它的优势,当只有一个生产者的时候,生产线上有了一个产品之后,生产线停止,让消费者去消费,这样子就完全没有达到消费者和生产者的并发,只是串行的。当生产线上的物品都消耗完了,一个消费者会解锁,并且进入等待队列,这时候可能是另一个在等待队列中的消费者获取到锁资源,造成这个消费者饥饿消费;其次当生产者生产了物品,去通知消费者消费,要一个一个的去通知,即使采用群体唤醒,消费者们是互斥的;况且当消费过程是一个简短的过程,一个线程进行消费和多线程加锁进行消费比起来并不会慢,反而加锁和解锁会造成性能消耗的。注意只有当消费过程是一个比较耗时的过程,才会考虑有没有加锁的必要。

   为什么多个生产者分别生产一次物品,就会停下来?

   毕竟生产者消费者问题并不会像富士康生产线那样,流水线上两端的工人步调一致,生产线两头的工人不需要互相通知。一个生产者生产玩一次物品之后,会有一个通知的过程,为了防止多个线程同时通知,造成紊乱,就对通知过程也加锁了,而这个锁就是消费用的锁。当通知过去的时候,消费者会消费完当前物品,直到释放锁(消费者消费的过程中一直加锁解锁,可以在解锁的时候,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   }
View Code

  这是生产线程,在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   }
View Code

  等待生产者通知之所以放到while里面,是为了防止consume_wait调用失败,造成饥饿消费.

  以上是用互斥锁实现生产者和消费者的思路。在这里为了方便使用,我实现了一个四个类,类图如下:

  unix网络编程--锁(一)_第2张图片

  以上这个UML图,是用linux下面的Dia画,其完全可以替代visio,而且支持将UML图片转化为各种文件,比如png。

  这个生产者和消费者的框架使用起来很简单,只需要实现一个类来继承ProduceConsume,实现其中的纯虚函数。然后启用线程,调用ThreadFuncLoader里面的Produce和Consume就OK了,使用者不用纠结锁的问题。

  在这里使用者所要实现的函数有:

  Produce对应上面的begin_produce

  Consume对应上面的begin_consume

  ProduceComplete对应上面的“生产结束”

  ConsumeComplete对应上面你的“生产线上没有了数据"

  Condition 对应上面的“通知条件”“等待条件”

 

你可能感兴趣的:(unix)