线程同步(上)

作者: iamsonormal2333 | 来源:发表于2017-02-28 23:33 被阅读22次

    寒假期间我尽尽是看了UNP和APUE之后了解了线程的概念,并没有使用框架实现这些功能。所以源码部分只能使用Stevens书上的源码。

    互斥锁(互斥量)

    在UNP和APUE两本书中用互斥锁和互斥量两个不同的词描述了pthread_mutex_t这种变量类型。我倾向于互斥锁这种表述,因为pthread_mutex_t这种变量实际上可以理解成一个锁,可以被上锁和解锁。

          int pthread_mutex_lock(pthread_mutex_t *mutex);
          int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

    当我们在某一线程中进行某一操作,并不想其他线程进行相同操作时,可以使用线程锁。线程锁在未上锁状态时调用pthread_mutex_lock函数会被锁住,在被锁住状态上锁会导致尝试进行上锁状态的其他线程阻塞。

          s=0;
          for(i=0;i<5;i++)
              pthread_create(&ntid[i],NULL,add,NULL);
          void add() {
              int t = s+1;
              sleep(2);
              s=t;
          }
    

    例如这个程序,按照期望s最终的值应为5,但是实际上s的值可能为0-5间的任意一个数。在第一个线程执行睡眠时,其他线程可能已经完成了对t的赋值。如果希望正确运行可以使用这样的方法:

          s=0;
          pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
          for(i=0;i<5;i++) {
              pthread_lock(&mutex);
              pthread_create(&ntid[i],NULL,add,NULL);
              pthread_unlock(&mutex);
          }
          void add() {
              int t = s+1;
              sleep(2);
              s=t;
          }
    

    在进行加法之前先将互斥锁锁住,再进行add函数调用。当一个互斥锁被解锁后,其他由于互斥锁而阻塞的线程会变成可运行状态。而第一个变为运行状态的线程可以再次给互斥锁上锁。

    可能下面一个例子才让你意识到互斥锁的重要性,这里将add函数换为向链表尾部加入元素的函数。

          struct node{
                int data;
                node *next;
          };
          node *head,*now;
          now = head = new node;
          i=0;
          head->data = i++;
          void add() {
              node *l;
              l = new node;
              l->data = i++;
              now->next = l;
              now = l;
          }
    

    如果add函数变为这样,不使用互斥锁可能会导致很严重的后果。比如当第一个线程执行now->next = l和now = l之间时,另一个线程执行了now->next = l,之前的一个线程的now->next = l 便会被覆盖。之后可能会导致数据缺失。所以可见互斥锁的重要性。

    避免死锁

    死锁是使用互斥锁时可能出现的问题。假设线程1将互斥锁1锁住,线程2将互斥锁2锁住,而如果这时线程1试图锁住互斥锁2,便会导致死锁问题。

    当然我们可以通过合理设计互斥锁之间的关系,但是有时会由于程序过于复杂而使得这样十分困难。所以这里提供另一种解决方法

          int pthread_mutex_trylock(pthread_mutex_t *mutex);
    

    调用该函数会尝试对互斥锁上锁。若上锁成功则返回0并将互斥锁锁住,否则返回EBUSY,此时线程并不会被阻塞。

    同时也可以通过调用pthread_mutex_timedlock()函数来设定线程的阻塞的时间

          int pthread_mutex_timedlock(pthread_mutex_t *mutex,
                const struct timespec *restrict tsptr);
    

    条件变量

    条件变量是线程可用的另一种同步机制。之所以将条件变量与互斥锁写在一篇简书里是因为通常互斥锁和条件变量是一起使用的。

    条件变量使用变量类型为pthread_cond_t类型,还是用一个例子来说明条件变量的用法。下面的程序通过轮询的方法来检查是否有线程空闲出来以执行文件操作。

          while(n>0) {
              filefind(&file_flag);//find a file to read
              pthread_mutex_lock(&mutex);
              if(s_pthreads>0)
                  for(i=0;i<nfiles;i++)
                      if(!file[i].flags) {
                            pthread_join(file[i].tid);
                            ...//update file[i] and other data
                      }
              pthread_mutex_unlock(&mutex);
          }
    

    每次循环中程序都会检查是否有空闲线程,如果有就加入这个线程,对文件进行处理。这样是没有问题的,但是这样程序在空闲时无法进入休眠,这样相当消耗CPU时间。使用条件变量可以避免这样的问题。

          while(n>0) {
              filefind(&file_flag);//find a file to read
              pthread_mutex_lock(&mutex);
              while(!s_pthreads)
                    pthread_cont_wait(&cond,&mutex);
              for(i=0;i<nfiles;i++)
                  if(!file[i].flags) {
                        pthread_join(file[i].tid);
                        ...//update file[i] and other data
                      }
              pthread_mutex_unlock(&mutex);
          }
    

    在每次有线程执行完这次操作并且即将进入空闲时,应当执行下述操作

          pthread_mutex_lock(&mutex);
          s_threads++;
          pthread_cond_signal(&cond);
          pthread_mutex_unlock(&mutex);
    

    当s_threads==0时,无空闲线程,pthread_cond_wait()函数将线程投入睡眠,并且释放互斥锁mutex。当被pthread_cond_signal()函数唤醒后,该线程将重新持有该互斥锁。

    为什么一定需要互斥锁呢?假如去掉互斥锁,那么可能出现以下情况,当线程被唤醒后,可能因为意外s_pthreads重新变为0,该信号会丢失。

    如果有必要唤醒所有线程,可以使用函数

          int pthread_cond_broadcast(pthread_cond_t *ptr);
    

    相关文章

      网友评论

        本文标题:线程同步(上)

        本文链接:https://www.haomeiwen.com/subject/vcytgttx.html