026 UNIX再学习 -- 线程同步

六月ゝ 毕业季﹏ 提交于 2019-12-18 22:05:52
                                          
                                                                                   
                                                                                
                                           

一、为什么要线程同步

当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题。同样,如果变量时只读的,每个线程同时读取该变量也不会有一致性问题。但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值
当一个线程修改变量时,其他线程在读取这个变量时可能会看到一个不一致的值。在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。当然,这种行为是与处理器体系结构相关的,但是可移植的程序并不能对使用何种处理器体系结构做出任何假设。


上图中描述了两个线程读写相同变量的假设例子。在这个例子中,线程 A 读取变量然后给这个变量赋予一个新的数值,但写操作需要两个存储器周期。当线程 B 在这两个存储器写周期中间读取这个变量时,它就会得到不一致的值。
为了解决这个问题,线程不得不使用锁,同一时间只允许一个线程访问该变量。
如果线程 B 希望读取变量,它首先要获取锁。同样,当线程 A 更新变量时,也需要获取同样的这把锁。这样,线程 B 在线程 A 释放锁以前就不能读取变量。

总结一下,多线程共享进程中的资源,多个线程同时访问相同的共享资源时,需要相互协调,以避免出现数据的不一致和混乱问题而线程之间的协调和通信即线程的同步。

线程同步方式有多种,接下来我们先看互斥量。

一、互斥量

线程中提供了互斥量(互斥锁)的机制来实现线程的同步。

1、什么是互斥量?

互斥锁,是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。可以保证以下三点:
原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量。
唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。
非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
从以上三点,我们看出可以用互斥量来保证对变量(关键的代码段)的排他性访问。

互斥量从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。
对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。

互斥量是用 pthread_mutex_t 数据类型表示的。在使用互斥量以前,必须首先对它进行初始化,可以把它设置为常量 PTHREAD_MUTEX_INITIALIZER只适用于静态分配的互斥量),也可以通过调用 pthread_mutex_init 函数初始化。

2、互斥量使用步骤

(1)定义互斥量

pthread_mutex_t mutex

(2)初始化互斥锁

静态初始化
pthread_mutex_t mtx = PTHERAD_MUTEX_INITIALIZER
动态初始化
int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  1. #include <pthread.h>
  2. int pthread_mutex_init(pthread_mutex_t *mutex,
  3.     const pthread_mutexattr_t *attr);
  4. 返回值:成功返回 0;失败返回错误编号
要用默认的属性初始化互斥量,只需把 attr 设为 NULL。
初始化互斥锁之前,必须将其所在的内存清零。
如果互斥锁已初始化,则它会处于未锁定状态。互斥锁可以位于进程之间共享的内存中或者某个进程的专用内存中。

(3)使用互斥量进行加锁

  1. #include <pthread.h>
  2. int pthread_mutex_lock(pthread_mutex_t *mutex);
  3. int pthread_mutex_trylock(pthread_mutex_t *mutex);
  4. 返回值:若成功返回 0,;失败返回错误编号
对互斥量进行加锁,需要调用 pthread_mutex_lock。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。
如果线程不希望被阻塞,它可以使用 pthread_mutex_trylock 尝试对互斥量进行加锁。如果调用 pthread_mutex_trylock 时互斥量处于未锁住状态,那么 pthread_mutex_trylock 将锁住互斥量,不会出现阻塞直接返回 0,否则 pthread_mutex_trylock 就会失败,不能锁住互斥量,返回 EBUSY。

(4)使用互斥量进行解锁

对互斥量解锁,需要调用 pthread_mutex_unlock
  1. #include <pthread.h>
  2. int pthread_mutex_unlock(pthread_mutex_t *mutex);
  3. 返回值:若成功返回 0,;失败返回错误编号

(5)如果不再使用,则销毁互斥量

  1. #include <pthread.h>
  2. int pthread_mutex_destroy(pthread_mutex_t *mutex);
  3. 返回值:成功返回 0;失败返回错误码

3、示例说明

  1. //使用互斥量解决多线程抢占资源的问题
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <pthread.h>
  6. char* buf[5]; //字符指针数组  全局变量
  7. int pos; //用于指定上面数组的下标
  8. //1.定义互斥量
  9. pthread_mutex_t mutex;
  10. void* task(void* p)
  11. {
  12.  //3.使用互斥量进行加锁
  13.  pthread_mutex_lock(&mutex);
  14.  buf[pos] = p;
  15.  sleep(1);
  16.  pos++;
  17.  //4.使用互斥量进行解锁
  18.  pthread_mutex_unlock(&mutex);
  19. }
  20. int main(void)
  21. {
  22.  //2.初始化互斥量
  23.  pthread_mutex_init(&mutex,0);
  24.  //1.启动一个线程 向数组中存储内容
  25.  pthread_t tid,tid2;
  26.  pthread_create(&tid,NULL,task,"zhangfei");
  27.  pthread_create(&tid2,NULL,task,"guanyu");
  28.  //2.主线程进程等待,并且打印最终的结果
  29.  pthread_join(tid,NULL);
  30.  pthread_join(tid2,NULL);
  31.  //5.销毁互斥量
  32.  pthread_mutex_destroy(&mutex);
  33.  int i = 0;
  34.  printf("字符指针数组中的内容是:");
  35.  for(i = 0; i < pos; i++)
  36.  {
  37.   printf("%s ",buf[i]);
  38.  }
  39.  printf("\n");
  40.  return 0;
  41. }
  42. 编译:# gcc test.c -lpthread
  43. 输出结果:
  44. 字符指针数组中的内容是:guanyu zhangfei

4、示例解析

多线程抢占资源,zhangfei、guanyu 线程阻塞不能同时给数组赋值的,所以用到互斥锁。
就跟上厕所一样,一个一个排队来。
注意,加锁解锁是需要时间的,所以互斥锁应尽量少。

三、避免死锁

1、什么是死锁呢?

所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
举个例子:
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。
  1. //使用互斥量解决多线程抢占资源的问题
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <pthread.h>
  6. char* buf[5]; //字符指针数组  全局变量
  7. int pos; //用于指定上面数组的下标
  8. //1.定义互斥量
  9. pthread_mutex_t mutex;
  10. void* task(void* p)
  11. {
  12.  //3.使用互斥量进行加锁
  13.  pthread_mutex_lock(&mutex);
  14.  pthread_mutex_lock(&mutex);
  15.  buf[pos] = p;
  16.  sleep(1);
  17.  pos++;
  18.  //4.使用互斥量进行解锁
  19.  pthread_mutex_unlock(&mutex);
  20.  pthread_mutex_unlock(&mutex);
  21. }
  22. int main(void)
  23. {
  24.  //2.初始化互斥量
  25.  pthread_mutex_init(&mutex,0);
  26.  //1.启动一个线程 向数组中存储内容
  27.  pthread_t tid,tid2;
  28.  pthread_create(&tid,NULL,task,"zhangfei");
  29.  pthread_create(&tid2,NULL,task,"guanyu");
  30.  //2.主线程进程等待,并且打印最终的结果
  31.  pthread_join(tid,NULL);
  32.  pthread_join(tid2,NULL);
  33.  //5.销毁互斥量
  34.  pthread_mutex_destroy(&mutex);
  35.  int i = 0;
  36.  printf("字符指针数组中的内容是:");
  37.  for(i = 0; i < pos; i++)
  38.  {
  39.   printf("%s ",buf[i]);
  40.  }
  41.  printf("\n");
  42.  return 0;
  43. }
  44. 两个嵌套的互斥锁会产生死锁

2、产生条件

虽然进程在运行过程中,可能发生死锁,但死锁的发生必须具备一定的条件,死锁的发生必须具有以下四个必要条件。

(1)互斥条件

指进程对所分配的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。

(2)请求和保持条件

只进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

(3)不剥夺条件

只进程已获得的资源,在未使用之前,不能被剥夺,只能在使用完时由自己释放。

(4)环路等待条件

只在发生死锁时,必然在一个进程 -- 资源的环形链,即进程集合{P0, P1, P2 ..., Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,....,Pn正在等待已被 P0 占用的资源。

3、处理方法

在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。

(1)预防死锁

这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。

(2)避免死锁

该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

(3)检测和解除死锁

先检测:这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源。检测方法包括定时检测、效率低时检测、进程等待时检测等。然后解除死锁:采取适当措施,从系统中将已发生的死锁清除掉。

这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

四、函数 pthread_mutex_timedlock

  1. #include <pthread.h>
  2. #include <time.h>
  3. int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
  4. const struct timespec *restrict abs_timeout);
  5. 返回值:成功返回 0;失败返回错误编号

1、函数功能

当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量原语允许绑定线程阻塞时间。pthread_mutex_timedlock 函数与 pthread_mutex_lock 是基本等价的,但是在达到超时时间时,pthread_mutex_timedlock 不会对互斥量进行加锁,而是返回错误码 ETIMEDOUT

超时指定愿意等待的绝对时间(与相对时间对比而言,指定在时间 X 之前可以阻塞等待,而不是说愿意阻塞 Y 秒)。
这个超时时间是用 timespec 结构来表示的,它用秒和纳秒来描述时间。

2、示例说明

  1. #include <stdio.h>
  2. #include <pthread.h>
  3. #include <time.h>
  4. #include "apue.h"
  5. int main (void)
  6. {
  7.  int err;
  8.  struct timespec tout;
  9.  struct tm *tmp;
  10.  char buf[64];
  11.  pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  12.  
  13.  pthread_mutex_lock (&lock);
  14.  printf ("mutex is locked\n");
  15.  clock_gettime (CLOCK_REALTIME, &tout);
  16.  tmp = localtime (&tout.tv_sec);
  17.  strftime (buf, sizeof (buf), "%r", tmp);
  18.  printf ("current time is %s\n", buf);
  19.  tout.tv_sec += 10;
  20.  err = pthread_mutex_timedlock (&lock, &tout);
  21.  clock_gettime (CLOCK_REALTIME, &tout);
  22.  tmp = localtime (&tout.tv_sec);
  23.  strftime (buf, sizeof (buf), "%r", tmp);
  24.  printf ("the time is now %s\n", buf);
  25.  if (err == 0)
  26.   printf ("mutex locked again\n");
  27.  else
  28.   printf ("can`t lock mutex again:%s\n", strerror (err));
  29.  return 0;
  30. }
  31. 编译:# gcc test.c  -lpthread -lrt
  32. 输出结果:
  33. mutex is locked
  34. current time is 03:11:30 PM
  35. the time is now 03:11:40 PM
  36. can`t lock mutex again:Connection timed out

3、示例解析

时间函数可查看:

首先,编译时,需要加上选项 -lrt。否则,会出现错误:undefined reference to `clock_gettime'
再有,pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 是静态初始化

这个程序故意对它已有的互斥量进行加锁,目的是演示 pthread_mutex_timedlock 是如何工作的。不推荐在实际中使用这种策略,因为它会导致死锁。
注意,阻塞的时间可能会有所不同,造成不同的原因有多种:开始时间可能再某秒的中间位置,系统时钟的精度可能不足以精确到支持我们指定的超时时间值,或者在程序继续运行前,调度延迟可能会增加时间值。

五、读写锁

读写锁与互斥量类似,不过读写锁允许更改的并行性。互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。
读写锁可以有 3 中状态:读模式下加锁状态、写模式加锁状态、不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有度模式的读写锁。
当读写锁在写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权限。
但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

读写锁也叫做共享互斥锁。当读写锁时读模式锁住时,就可以说成为以共享模式锁住的。当它是写模式锁住的时候,就可以说成是互斥模式锁住的。
与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。

1、读写锁初始化和销毁

POSIX 定义的读写锁的数据类型是: pthread_rwlock_t
  1. #include <pthread.h>
  2. int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
  3.     const pthread_rwlockattr_t *restrict attr);
  4. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  5. 两个函数返回值:若成功,返回 0;否则,返回错误编号
读写锁通过调用 pthread_rwlock_init 进行动态初始化如果希望读写锁有默认的属性,可以传一个 NULL 指针给 attr。可以调用常量 PTHREAD_RWLOCK_INITIALIZER 进行静态初始化
在释放读写锁占用的内存之前,需要调用 phtread_rwlock_destroy 做清理工作。
如果 pthread_rwlock_init 为读写锁分配了资源,pthread_rwlock_destroy 将释放这些资源。
如果在调用 pthread_rwlock_destroy 之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失。

2、读写锁解锁

  1. #include <pthread.h>
  2. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  3. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  4. int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  5. 成功则返回0, 出错则返回错误编号.
要在读模式下锁定读写锁,需要调用 pthread_rwlock_rdlock
要在写模式下锁定读写锁,需要调用 pthread_rwlock_wrlock
不管以何种方式锁住读写锁,都可以调用 pthread_rwlcok_unlock 进行解锁

3、示例说明
参看:线程同步与互斥:读写锁

  1. #include<stdio.h> 
  2. #include<unistd.h> 
  3. #include<pthread.h> 
  4.  
  5. pthread_rwlock_t rwlock; //读写锁 
  6. int num = 1
  7.  
  8. //读操作,其他线程允许读操作,却不允许写操作 
  9. void *fun1(void *arg) 
  10.     while(1
  11.     { 
  12.         pthread_rwlock_rdlock(&rwlock); 
  13.         printf("read num first===%d\n",num); 
  14.         pthread_rwlock_unlock(&rwlock); 
  15.         sleep(1); 
  16.     } 
  17.  
  18. //读操作,其他线程允许读操作,却不允许写操作 
  19. void *fun2(void *arg) 
  20.     while(1
  21.     { 
  22.         pthread_rwlock_rdlock(&rwlock); 
  23.         printf("read num second===%d\n",num); 
  24.         pthread_rwlock_unlock(&rwlock); 
  25.         sleep(2); 
  26.     } 
  27.  
  28. //写操作,其它线程都不允许读或写操作 
  29. void *fun3(void *arg) 
  30.     while(1
  31.     { 
  32.          
  33.         pthread_rwlock_wrlock(&rwlock); 
  34.         num++; 
  35.         printf("write thread first\n"); 
  36.         pthread_rwlock_unlock(&rwlock); 
  37.         sleep(2); 
  38.     } 
  39.  
  40. //写操作,其它线程都不允许读或写操作 
  41. void *fun4(void *arg) 
  42.     while(1
  43.     { 
  44.          
  45.         pthread_rwlock_wrlock(&rwlock); 
  46.         num++; 
  47.         printf("write thread second\n"); 
  48.         pthread_rwlock_unlock(&rwlock); 
  49.         sleep(1); 
  50.     } 
  51.  
  52. int main() 
  53.     pthread_t ptd1, ptd2, ptd3, ptd4; 
  54.      
  55.     pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁 
  56.      
  57.     //创建线程 
  58.     pthread_create(&ptd1, NULL, fun1, NULL); 
  59.     pthread_create(&ptd2, NULL, fun2, NULL); 
  60.     pthread_create(&ptd3, NULL, fun3, NULL); 
  61.     pthread_create(&ptd4, NULL, fun4, NULL); 
  62.      
  63.     //等待线程结束,回收其资源 
  64.     pthread_join(ptd1,NULL); 
  65.     pthread_join(ptd2,NULL); 
  66.     pthread_join(ptd3,NULL); 
  67.     pthread_join(ptd4,NULL); 
  68.      
  69.     pthread_rwlock_destroy(&rwlock);//销毁读写锁 
  70.      
  71.     return 0
  72. 编译:# gcc test.c -lpthread
  73. 输出结果:
  74. write thread second
  75. write thread first
  76. read num second===3
  77. read num first===3
  78. write thread second
  79. read num first===4
  80. write thread first
  81. read num second===5
  82. write thread second
  83. read num first===6
  84. .....

4、示例解析

在此示例程序中,共创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。
当某个线程读操作时,其他线程允许读操作,却不允许写操作;
当某个线程写操作时,其他线程都不允许读或写操作。

六、条件变量

1、条件变量概念

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。
在使用条件变量之前,必须先对它进行初始化。由 pthread_cond_t 数据类型表示的条件变量可以用两种方式进行初始化,可以把常量 PTHREAD_COND_INITIALIZER 赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用 pthread_cond_init 函数对它进行初始化。
在释放条件变量底层的内存空间之前,可以使用 pthread_cond_destroy 函数对条件变量进行反初始化。

2、条件变量初始化和销毁

  1. #include <pthread.h>
  2. int pthread_cond_destroy(pthread_cond_t *cond);
  3. int pthread_cond_init(pthread_cond_t *restrict cond,
  4. const pthread_condattr_t *restrict attr);
  5. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  6. 两个函数的返回值:若成功,返回 0;否则,返回错误编号

(1)参数解析

当参数 attr 为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由 attr 中的属性值来决定。调用 pthread_cond_init 函数时,参数 attr 为空指针等价于 attr 中的属性为缺省属性,只是前者不需要 attr 所占用的内存开销。这个函数返回时,条件变量被存放在参数 cond 指向的内存中。
可以用宏 PTHREAD_COND_INITIALIZER 来初始化静态定义的条件变量,使其具有缺省属性。这和用pthread_cond_init 函数动态分配的效果是一样的。初始化时不进行错误检查。
如:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

3、条件变量等待

  1. #include <pthread.h>
  2. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  3. int pthread_cond_timedwait(pthread_cond_t *cond,
  4.     pthread_mutex_t *mutex, const struct timespec *abstime);
  5. 两个函数的返回值:若陈宫,返回 0;否则,返回错误编号

(1)参数解析

函数将解锁 mutex 参数指向的互斥锁,并使当前线程阻塞在 cond 参数指向的条件变量上。被阻塞的线程可以被pthread_cond_signal 函数,pthread_cond_broadcast 函数唤醒,也可能在被信号中断后被唤醒。pthread_cond_wait 函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。pthread_cond_wait 函数返回时,相应的互斥锁将被当前线程锁定,即使是函数出错返回。
一般一个条件表达式都是在一个互斥锁的保护下被检查。当条件表达式未被满足时,线程将仍然阻塞在这个条件变量上。当另一个线程改变了条件的值并向条件变量发出信号时,等待在这个条件变量上的一个线程或所有线程被唤醒,接着都试图再次占有相应的互斥锁。阻塞在条件变量上的线程被唤醒以后,直到 pthread_cond_wait() 函数返回之前条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥锁之前,必须重新测试条件值。最好的测试方法是循环调用 pthread_cond_wait 函数,并把满足条件的表达式置为循环的终止条件。如:
  1. pthread_mutex_lock();
  2. while (condition_is_false)
  3.     pthread_cond_wait(); 
  4. pthread_mutex_unlock();
阻塞在同一个条件变量上的不同线程被释放的次序是不一定的。
注意:pthread_cond_wait() 函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条件变量相关的互斥锁仍将处在锁定状态。


pthread_cond_timedwait 函数的功能与 pthread_cond_wait 函数相似,只是多了一个超时(tsptr)。超时值指定了我们愿意等待多长时间,它是通过 timespec 结构指定的。

4、条件变量唤醒

  1. #include <pthread.h>
  2. int pthread_cond_broadcast(pthread_cond_t *cond);
  3. int pthread_cond_signal(pthread_cond_t *cond);
  4. 两个函数的返回值:若成功,返回 0;否则,返回错误编号

(1)函数解析

pthread_cond_signal 函数被用来释放被阻塞在指定条件变量上的一个线程。至少能唤醒一个等待该条件的线程。必须在互斥锁的保护下使用相应的条件变量。否则对条件变量的解锁有可能发生在锁定条件变量之前,从而造成死锁。
唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是 SCHED_OTHER 类型的,系统将根据线程的优先级唤醒线程。如果没有线程被阻塞在条件变量上,那么调用 pthread_cond_signal() 将没有作用。

pthread_cond_broadcast 函数唤醒所有被 pthread_cond_wait 函数阻塞在某个条件变量上的线程,参数 cond 被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,pthread_cond_broadcast 函数无效。由于 pthread_cond_broadcast 函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用 pthread_cond_broadcast 函数。

5、示例说明

  1. //示例一
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4. #include <unistd.h> 
  5. #include <pthread.h> 
  6.  
  7. pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER; 
  8. pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; 
  9.  
  10. void *traveler_arrive(void *name) 
  11.     char *p = (char *)name; 
  12.  
  13.     printf ("Travelr: %s need a taxi now!\n", p); 
  14.     pthread_mutex_lock(&taximutex); 
  15.     pthread_cond_wait(&taxicond, &taximutex); 
  16.     pthread_mutex_unlock(&taximutex); 
  17.     printf ("traveler: %s now got a taxi!\n", p); 
  18.     pthread_exit(NULL); 
  19.  
  20. void *taxi_arrive(void *name) 
  21.     char *p = (char *)name; 
  22.     printf ("Taxi: %s arrives.\n", p); 
  23.     pthread_cond_signal(&taxicond); 
  24.     pthread_exit(NULL); 
  25.  
  26. int main (int argc, char **argv) 
  27.     char *name; 
  28.     pthread_t thread; 
  29.     pthread_attr_t threadattr; 
  30.     pthread_attr_init(&threadattr); 
  31.  
  32.     name = "Jack"; 
  33.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  34.     sleep(1); 
  35.     name = "Susan"; 
  36.     pthread_create(&thread, &threadattr, traveler_arrive, name); 
  37.     sleep(1); 
  38.     name = "Mike"; 
  39.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  40.     sleep(1); 
  41.  
  42.     return 0; 
  43. }
  44. 编译:# gcc test.c  -lpthread
  45. 输出结果:
  46. Taxi: Jack arrives.
  47. Travelr: Susan need a taxi now!
  48. Taxi: Mike arrives.
  49. traveler: Susan now got a taxi!
  1. //示例二
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4. #include <unistd.h> 
  5. #include <pthread.h> 
  6.  
  7. int travelercount = 0; 
  8. pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER; 
  9. pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; 
  10.  
  11. void *traveler_arrive(void *name) 
  12.     char *p = (char *)name; 
  13.  
  14.     pthread_mutex_lock(&taximutex); 
  15.  
  16.     printf ("traveler: %s need a taxi now!\n", p); 
  17.     travelercount++; 
  18.     pthread_cond_wait(&taxicond, &taximutex); 
  19.              
  20.     pthread_mutex_unlock(&taximutex); 
  21.     printf ("traveler: %s now got a taxi!\n", p); 
  22.     pthread_exit(NULL); 
  23.  
  24. void *taxi_arrive(void *name) 
  25.     char *p = (char *)name; 
  26.     printf ("Taxi: %s arrives.\n", p); 
  27.     for(;;){ 
  28.         if(travelercount){ 
  29.             pthread_cond_signal(&taxicond); 
  30.             travelercount--; 
  31.             break; 
  32.         } 
  33.     } 
  34.  
  35.     pthread_exit(NULL); 
  36.  
  37. int main (int argc, char **argv) 
  38.     char *name; 
  39.     pthread_t thread; 
  40.     pthread_attr_t threadattr; 
  41.     pthread_attr_init(&threadattr); 
  42.  
  43.     name = "Jack"; 
  44.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  45.     sleep(1); 
  46.     name = "Susan"; 
  47.     pthread_create(&thread, &threadattr, traveler_arrive, name); 
  48.     sleep(3); 
  49.     name = "Mike"; 
  50.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  51.     sleep(4); 
  52.  
  53.     return 0; 
  54. }
  55. 编译:# gcc test.c  -lpthread
  56. 输出结果:
  57. Taxi: Jack arrives.
  58. traveler: Susan need a taxi now!
  59. traveler: Susan now got a taxi!
  60. Taxi: Mike arrives.

七、自旋锁

自旋锁与互斥量功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁!!!
自旋锁在用户态使用的比较少,在内核使用的比较多!自旋锁的使用场景:锁的持有时间比较短,或者说小于2次上下文切换的时间。
自旋锁在用户态的函数接口和互斥量一样,把pthread_mutex_xxx()中mutex换成spin,如:pthread_spin_init()。

八、总结

APUE 第 11 章,线程部分,之前没有怎么深入研究过,而且就用到了互斥量。其他的像读写锁、条件变量、屏障、自旋锁都不熟悉。
如果想继续深入研究,参看:随笔分类 - linux编程-线程
人家写的不错,而且讲的不我清楚多了。
                                   
                                   
               
                   
                                           

一、为什么要线程同步

当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题。同样,如果变量时只读的,每个线程同时读取该变量也不会有一致性问题。但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值
当一个线程修改变量时,其他线程在读取这个变量时可能会看到一个不一致的值。在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。当然,这种行为是与处理器体系结构相关的,但是可移植的程序并不能对使用何种处理器体系结构做出任何假设。


上图中描述了两个线程读写相同变量的假设例子。在这个例子中,线程 A 读取变量然后给这个变量赋予一个新的数值,但写操作需要两个存储器周期。当线程 B 在这两个存储器写周期中间读取这个变量时,它就会得到不一致的值。
为了解决这个问题,线程不得不使用锁,同一时间只允许一个线程访问该变量。
如果线程 B 希望读取变量,它首先要获取锁。同样,当线程 A 更新变量时,也需要获取同样的这把锁。这样,线程 B 在线程 A 释放锁以前就不能读取变量。

总结一下,多线程共享进程中的资源,多个线程同时访问相同的共享资源时,需要相互协调,以避免出现数据的不一致和混乱问题而线程之间的协调和通信即线程的同步。

线程同步方式有多种,接下来我们先看互斥量。

一、互斥量

线程中提供了互斥量(互斥锁)的机制来实现线程的同步。

1、什么是互斥量?

互斥锁,是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。可以保证以下三点:
原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量。
唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。
非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
从以上三点,我们看出可以用互斥量来保证对变量(关键的代码段)的排他性访问。

互斥量从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。
对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。

互斥量是用 pthread_mutex_t 数据类型表示的。在使用互斥量以前,必须首先对它进行初始化,可以把它设置为常量 PTHREAD_MUTEX_INITIALIZER只适用于静态分配的互斥量),也可以通过调用 pthread_mutex_init 函数初始化。

2、互斥量使用步骤

(1)定义互斥量

pthread_mutex_t mutex

(2)初始化互斥锁

静态初始化
pthread_mutex_t mtx = PTHERAD_MUTEX_INITIALIZER
动态初始化
int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  1. #include <pthread.h>
  2. int pthread_mutex_init(pthread_mutex_t *mutex,
  3.     const pthread_mutexattr_t *attr);
  4. 返回值:成功返回 0;失败返回错误编号
要用默认的属性初始化互斥量,只需把 attr 设为 NULL。
初始化互斥锁之前,必须将其所在的内存清零。
如果互斥锁已初始化,则它会处于未锁定状态。互斥锁可以位于进程之间共享的内存中或者某个进程的专用内存中。

(3)使用互斥量进行加锁

  1. #include <pthread.h>
  2. int pthread_mutex_lock(pthread_mutex_t *mutex);
  3. int pthread_mutex_trylock(pthread_mutex_t *mutex);
  4. 返回值:若成功返回 0,;失败返回错误编号
对互斥量进行加锁,需要调用 pthread_mutex_lock。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。
如果线程不希望被阻塞,它可以使用 pthread_mutex_trylock 尝试对互斥量进行加锁。如果调用 pthread_mutex_trylock 时互斥量处于未锁住状态,那么 pthread_mutex_trylock 将锁住互斥量,不会出现阻塞直接返回 0,否则 pthread_mutex_trylock 就会失败,不能锁住互斥量,返回 EBUSY。

(4)使用互斥量进行解锁

对互斥量解锁,需要调用 pthread_mutex_unlock
  1. #include <pthread.h>
  2. int pthread_mutex_unlock(pthread_mutex_t *mutex);
  3. 返回值:若成功返回 0,;失败返回错误编号

(5)如果不再使用,则销毁互斥量

  1. #include <pthread.h>
  2. int pthread_mutex_destroy(pthread_mutex_t *mutex);
  3. 返回值:成功返回 0;失败返回错误码

3、示例说明

  1. //使用互斥量解决多线程抢占资源的问题
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <pthread.h>
  6. char* buf[5]; //字符指针数组  全局变量
  7. int pos; //用于指定上面数组的下标
  8. //1.定义互斥量
  9. pthread_mutex_t mutex;
  10. void* task(void* p)
  11. {
  12.  //3.使用互斥量进行加锁
  13.  pthread_mutex_lock(&mutex);
  14.  buf[pos] = p;
  15.  sleep(1);
  16.  pos++;
  17.  //4.使用互斥量进行解锁
  18.  pthread_mutex_unlock(&mutex);
  19. }
  20. int main(void)
  21. {
  22.  //2.初始化互斥量
  23.  pthread_mutex_init(&mutex,0);
  24.  //1.启动一个线程 向数组中存储内容
  25.  pthread_t tid,tid2;
  26.  pthread_create(&tid,NULL,task,"zhangfei");
  27.  pthread_create(&tid2,NULL,task,"guanyu");
  28.  //2.主线程进程等待,并且打印最终的结果
  29.  pthread_join(tid,NULL);
  30.  pthread_join(tid2,NULL);
  31.  //5.销毁互斥量
  32.  pthread_mutex_destroy(&mutex);
  33.  int i = 0;
  34.  printf("字符指针数组中的内容是:");
  35.  for(i = 0; i < pos; i++)
  36.  {
  37.   printf("%s ",buf[i]);
  38.  }
  39.  printf("\n");
  40.  return 0;
  41. }
  42. 编译:# gcc test.c -lpthread
  43. 输出结果:
  44. 字符指针数组中的内容是:guanyu zhangfei

4、示例解析

多线程抢占资源,zhangfei、guanyu 线程阻塞不能同时给数组赋值的,所以用到互斥锁。
就跟上厕所一样,一个一个排队来。
注意,加锁解锁是需要时间的,所以互斥锁应尽量少。

三、避免死锁

1、什么是死锁呢?

所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
举个例子:
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。
  1. //使用互斥量解决多线程抢占资源的问题
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <pthread.h>
  6. char* buf[5]; //字符指针数组  全局变量
  7. int pos; //用于指定上面数组的下标
  8. //1.定义互斥量
  9. pthread_mutex_t mutex;
  10. void* task(void* p)
  11. {
  12.  //3.使用互斥量进行加锁
  13.  pthread_mutex_lock(&mutex);
  14.  pthread_mutex_lock(&mutex);
  15.  buf[pos] = p;
  16.  sleep(1);
  17.  pos++;
  18.  //4.使用互斥量进行解锁
  19.  pthread_mutex_unlock(&mutex);
  20.  pthread_mutex_unlock(&mutex);
  21. }
  22. int main(void)
  23. {
  24.  //2.初始化互斥量
  25.  pthread_mutex_init(&mutex,0);
  26.  //1.启动一个线程 向数组中存储内容
  27.  pthread_t tid,tid2;
  28.  pthread_create(&tid,NULL,task,"zhangfei");
  29.  pthread_create(&tid2,NULL,task,"guanyu");
  30.  //2.主线程进程等待,并且打印最终的结果
  31.  pthread_join(tid,NULL);
  32.  pthread_join(tid2,NULL);
  33.  //5.销毁互斥量
  34.  pthread_mutex_destroy(&mutex);
  35.  int i = 0;
  36.  printf("字符指针数组中的内容是:");
  37.  for(i = 0; i < pos; i++)
  38.  {
  39.   printf("%s ",buf[i]);
  40.  }
  41.  printf("\n");
  42.  return 0;
  43. }
  44. 两个嵌套的互斥锁会产生死锁

2、产生条件

虽然进程在运行过程中,可能发生死锁,但死锁的发生必须具备一定的条件,死锁的发生必须具有以下四个必要条件。

(1)互斥条件

指进程对所分配的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。

(2)请求和保持条件

只进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

(3)不剥夺条件

只进程已获得的资源,在未使用之前,不能被剥夺,只能在使用完时由自己释放。

(4)环路等待条件

只在发生死锁时,必然在一个进程 -- 资源的环形链,即进程集合{P0, P1, P2 ..., Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,....,Pn正在等待已被 P0 占用的资源。

3、处理方法

在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。

(1)预防死锁

这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。

(2)避免死锁

该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

(3)检测和解除死锁

先检测:这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源。检测方法包括定时检测、效率低时检测、进程等待时检测等。然后解除死锁:采取适当措施,从系统中将已发生的死锁清除掉。

这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

四、函数 pthread_mutex_timedlock

  1. #include <pthread.h>
  2. #include <time.h>
  3. int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
  4. const struct timespec *restrict abs_timeout);
  5. 返回值:成功返回 0;失败返回错误编号

1、函数功能

当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量原语允许绑定线程阻塞时间。pthread_mutex_timedlock 函数与 pthread_mutex_lock 是基本等价的,但是在达到超时时间时,pthread_mutex_timedlock 不会对互斥量进行加锁,而是返回错误码 ETIMEDOUT

超时指定愿意等待的绝对时间(与相对时间对比而言,指定在时间 X 之前可以阻塞等待,而不是说愿意阻塞 Y 秒)。
这个超时时间是用 timespec 结构来表示的,它用秒和纳秒来描述时间。

2、示例说明

  1. #include <stdio.h>
  2. #include <pthread.h>
  3. #include <time.h>
  4. #include "apue.h"
  5. int main (void)
  6. {
  7.  int err;
  8.  struct timespec tout;
  9.  struct tm *tmp;
  10.  char buf[64];
  11.  pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  12.  
  13.  pthread_mutex_lock (&lock);
  14.  printf ("mutex is locked\n");
  15.  clock_gettime (CLOCK_REALTIME, &tout);
  16.  tmp = localtime (&tout.tv_sec);
  17.  strftime (buf, sizeof (buf), "%r", tmp);
  18.  printf ("current time is %s\n", buf);
  19.  tout.tv_sec += 10;
  20.  err = pthread_mutex_timedlock (&lock, &tout);
  21.  clock_gettime (CLOCK_REALTIME, &tout);
  22.  tmp = localtime (&tout.tv_sec);
  23.  strftime (buf, sizeof (buf), "%r", tmp);
  24.  printf ("the time is now %s\n", buf);
  25.  if (err == 0)
  26.   printf ("mutex locked again\n");
  27.  else
  28.   printf ("can`t lock mutex again:%s\n", strerror (err));
  29.  return 0;
  30. }
  31. 编译:# gcc test.c  -lpthread -lrt
  32. 输出结果:
  33. mutex is locked
  34. current time is 03:11:30 PM
  35. the time is now 03:11:40 PM
  36. can`t lock mutex again:Connection timed out

3、示例解析

时间函数可查看:

首先,编译时,需要加上选项 -lrt。否则,会出现错误:undefined reference to `clock_gettime'
再有,pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 是静态初始化

这个程序故意对它已有的互斥量进行加锁,目的是演示 pthread_mutex_timedlock 是如何工作的。不推荐在实际中使用这种策略,因为它会导致死锁。
注意,阻塞的时间可能会有所不同,造成不同的原因有多种:开始时间可能再某秒的中间位置,系统时钟的精度可能不足以精确到支持我们指定的超时时间值,或者在程序继续运行前,调度延迟可能会增加时间值。

五、读写锁

读写锁与互斥量类似,不过读写锁允许更改的并行性。互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。
读写锁可以有 3 中状态:读模式下加锁状态、写模式加锁状态、不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有度模式的读写锁。
当读写锁在写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权限。
但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

读写锁也叫做共享互斥锁。当读写锁时读模式锁住时,就可以说成为以共享模式锁住的。当它是写模式锁住的时候,就可以说成是互斥模式锁住的。
与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。

1、读写锁初始化和销毁

POSIX 定义的读写锁的数据类型是: pthread_rwlock_t
  1. #include <pthread.h>
  2. int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
  3.     const pthread_rwlockattr_t *restrict attr);
  4. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  5. 两个函数返回值:若成功,返回 0;否则,返回错误编号
读写锁通过调用 pthread_rwlock_init 进行动态初始化如果希望读写锁有默认的属性,可以传一个 NULL 指针给 attr。可以调用常量 PTHREAD_RWLOCK_INITIALIZER 进行静态初始化
在释放读写锁占用的内存之前,需要调用 phtread_rwlock_destroy 做清理工作。
如果 pthread_rwlock_init 为读写锁分配了资源,pthread_rwlock_destroy 将释放这些资源。
如果在调用 pthread_rwlock_destroy 之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失。

2、读写锁解锁

  1. #include <pthread.h>
  2. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  3. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  4. int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  5. 成功则返回0, 出错则返回错误编号.
要在读模式下锁定读写锁,需要调用 pthread_rwlock_rdlock
要在写模式下锁定读写锁,需要调用 pthread_rwlock_wrlock
不管以何种方式锁住读写锁,都可以调用 pthread_rwlcok_unlock 进行解锁

3、示例说明
参看:线程同步与互斥:读写锁

  1. #include<stdio.h> 
  2. #include<unistd.h> 
  3. #include<pthread.h> 
  4.  
  5. pthread_rwlock_t rwlock; //读写锁 
  6. int num = 1
  7.  
  8. //读操作,其他线程允许读操作,却不允许写操作 
  9. void *fun1(void *arg) 
  10.     while(1
  11.     { 
  12.         pthread_rwlock_rdlock(&rwlock); 
  13.         printf("read num first===%d\n",num); 
  14.         pthread_rwlock_unlock(&rwlock); 
  15.         sleep(1); 
  16.     } 
  17.  
  18. //读操作,其他线程允许读操作,却不允许写操作 
  19. void *fun2(void *arg) 
  20.     while(1
  21.     { 
  22.         pthread_rwlock_rdlock(&rwlock); 
  23.         printf("read num second===%d\n",num); 
  24.         pthread_rwlock_unlock(&rwlock); 
  25.         sleep(2); 
  26.     } 
  27.  
  28. //写操作,其它线程都不允许读或写操作 
  29. void *fun3(void *arg) 
  30.     while(1
  31.     { 
  32.          
  33.         pthread_rwlock_wrlock(&rwlock); 
  34.         num++; 
  35.         printf("write thread first\n"); 
  36.         pthread_rwlock_unlock(&rwlock); 
  37.         sleep(2); 
  38.     } 
  39.  
  40. //写操作,其它线程都不允许读或写操作 
  41. void *fun4(void *arg) 
  42.     while(1
  43.     { 
  44.          
  45.         pthread_rwlock_wrlock(&rwlock); 
  46.         num++; 
  47.         printf("write thread second\n"); 
  48.         pthread_rwlock_unlock(&rwlock); 
  49.         sleep(1); 
  50.     } 
  51.  
  52. int main() 
  53.     pthread_t ptd1, ptd2, ptd3, ptd4; 
  54.      
  55.     pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁 
  56.      
  57.     //创建线程 
  58.     pthread_create(&ptd1, NULL, fun1, NULL); 
  59.     pthread_create(&ptd2, NULL, fun2, NULL); 
  60.     pthread_create(&ptd3, NULL, fun3, NULL); 
  61.     pthread_create(&ptd4, NULL, fun4, NULL); 
  62.      
  63.     //等待线程结束,回收其资源 
  64.     pthread_join(ptd1,NULL); 
  65.     pthread_join(ptd2,NULL); 
  66.     pthread_join(ptd3,NULL); 
  67.     pthread_join(ptd4,NULL); 
  68.      
  69.     pthread_rwlock_destroy(&rwlock);//销毁读写锁 
  70.      
  71.     return 0
  72. 编译:# gcc test.c -lpthread
  73. 输出结果:
  74. write thread second
  75. write thread first
  76. read num second===3
  77. read num first===3
  78. write thread second
  79. read num first===4
  80. write thread first
  81. read num second===5
  82. write thread second
  83. read num first===6
  84. .....

4、示例解析

在此示例程序中,共创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。
当某个线程读操作时,其他线程允许读操作,却不允许写操作;
当某个线程写操作时,其他线程都不允许读或写操作。

六、条件变量

1、条件变量概念

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。
在使用条件变量之前,必须先对它进行初始化。由 pthread_cond_t 数据类型表示的条件变量可以用两种方式进行初始化,可以把常量 PTHREAD_COND_INITIALIZER 赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用 pthread_cond_init 函数对它进行初始化。
在释放条件变量底层的内存空间之前,可以使用 pthread_cond_destroy 函数对条件变量进行反初始化。

2、条件变量初始化和销毁

  1. #include <pthread.h>
  2. int pthread_cond_destroy(pthread_cond_t *cond);
  3. int pthread_cond_init(pthread_cond_t *restrict cond,
  4. const pthread_condattr_t *restrict attr);
  5. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  6. 两个函数的返回值:若成功,返回 0;否则,返回错误编号

(1)参数解析

当参数 attr 为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由 attr 中的属性值来决定。调用 pthread_cond_init 函数时,参数 attr 为空指针等价于 attr 中的属性为缺省属性,只是前者不需要 attr 所占用的内存开销。这个函数返回时,条件变量被存放在参数 cond 指向的内存中。
可以用宏 PTHREAD_COND_INITIALIZER 来初始化静态定义的条件变量,使其具有缺省属性。这和用pthread_cond_init 函数动态分配的效果是一样的。初始化时不进行错误检查。
如:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

3、条件变量等待

  1. #include <pthread.h>
  2. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  3. int pthread_cond_timedwait(pthread_cond_t *cond,
  4.     pthread_mutex_t *mutex, const struct timespec *abstime);
  5. 两个函数的返回值:若陈宫,返回 0;否则,返回错误编号

(1)参数解析

函数将解锁 mutex 参数指向的互斥锁,并使当前线程阻塞在 cond 参数指向的条件变量上。被阻塞的线程可以被pthread_cond_signal 函数,pthread_cond_broadcast 函数唤醒,也可能在被信号中断后被唤醒。pthread_cond_wait 函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。pthread_cond_wait 函数返回时,相应的互斥锁将被当前线程锁定,即使是函数出错返回。
一般一个条件表达式都是在一个互斥锁的保护下被检查。当条件表达式未被满足时,线程将仍然阻塞在这个条件变量上。当另一个线程改变了条件的值并向条件变量发出信号时,等待在这个条件变量上的一个线程或所有线程被唤醒,接着都试图再次占有相应的互斥锁。阻塞在条件变量上的线程被唤醒以后,直到 pthread_cond_wait() 函数返回之前条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥锁之前,必须重新测试条件值。最好的测试方法是循环调用 pthread_cond_wait 函数,并把满足条件的表达式置为循环的终止条件。如:
  1. pthread_mutex_lock();
  2. while (condition_is_false)
  3.     pthread_cond_wait(); 
  4. pthread_mutex_unlock();
阻塞在同一个条件变量上的不同线程被释放的次序是不一定的。
注意:pthread_cond_wait() 函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条件变量相关的互斥锁仍将处在锁定状态。


pthread_cond_timedwait 函数的功能与 pthread_cond_wait 函数相似,只是多了一个超时(tsptr)。超时值指定了我们愿意等待多长时间,它是通过 timespec 结构指定的。

4、条件变量唤醒

  1. #include <pthread.h>
  2. int pthread_cond_broadcast(pthread_cond_t *cond);
  3. int pthread_cond_signal(pthread_cond_t *cond);
  4. 两个函数的返回值:若成功,返回 0;否则,返回错误编号

(1)函数解析

pthread_cond_signal 函数被用来释放被阻塞在指定条件变量上的一个线程。至少能唤醒一个等待该条件的线程。必须在互斥锁的保护下使用相应的条件变量。否则对条件变量的解锁有可能发生在锁定条件变量之前,从而造成死锁。
唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是 SCHED_OTHER 类型的,系统将根据线程的优先级唤醒线程。如果没有线程被阻塞在条件变量上,那么调用 pthread_cond_signal() 将没有作用。

pthread_cond_broadcast 函数唤醒所有被 pthread_cond_wait 函数阻塞在某个条件变量上的线程,参数 cond 被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,pthread_cond_broadcast 函数无效。由于 pthread_cond_broadcast 函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用 pthread_cond_broadcast 函数。

5、示例说明

  1. //示例一
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4. #include <unistd.h> 
  5. #include <pthread.h> 
  6.  
  7. pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER; 
  8. pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; 
  9.  
  10. void *traveler_arrive(void *name) 
  11.     char *p = (char *)name; 
  12.  
  13.     printf ("Travelr: %s need a taxi now!\n", p); 
  14.     pthread_mutex_lock(&taximutex); 
  15.     pthread_cond_wait(&taxicond, &taximutex); 
  16.     pthread_mutex_unlock(&taximutex); 
  17.     printf ("traveler: %s now got a taxi!\n", p); 
  18.     pthread_exit(NULL); 
  19.  
  20. void *taxi_arrive(void *name) 
  21.     char *p = (char *)name; 
  22.     printf ("Taxi: %s arrives.\n", p); 
  23.     pthread_cond_signal(&taxicond); 
  24.     pthread_exit(NULL); 
  25.  
  26. int main (int argc, char **argv) 
  27.     char *name; 
  28.     pthread_t thread; 
  29.     pthread_attr_t threadattr; 
  30.     pthread_attr_init(&threadattr); 
  31.  
  32.     name = "Jack"; 
  33.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  34.     sleep(1); 
  35.     name = "Susan"; 
  36.     pthread_create(&thread, &threadattr, traveler_arrive, name); 
  37.     sleep(1); 
  38.     name = "Mike"; 
  39.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  40.     sleep(1); 
  41.  
  42.     return 0; 
  43. }
  44. 编译:# gcc test.c  -lpthread
  45. 输出结果:
  46. Taxi: Jack arrives.
  47. Travelr: Susan need a taxi now!
  48. Taxi: Mike arrives.
  49. traveler: Susan now got a taxi!
  1. //示例二
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4. #include <unistd.h> 
  5. #include <pthread.h> 
  6.  
  7. int travelercount = 0; 
  8. pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER; 
  9. pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER; 
  10.  
  11. void *traveler_arrive(void *name) 
  12.     char *p = (char *)name; 
  13.  
  14.     pthread_mutex_lock(&taximutex); 
  15.  
  16.     printf ("traveler: %s need a taxi now!\n", p); 
  17.     travelercount++; 
  18.     pthread_cond_wait(&taxicond, &taximutex); 
  19.              
  20.     pthread_mutex_unlock(&taximutex); 
  21.     printf ("traveler: %s now got a taxi!\n", p); 
  22.     pthread_exit(NULL); 
  23.  
  24. void *taxi_arrive(void *name) 
  25.     char *p = (char *)name; 
  26.     printf ("Taxi: %s arrives.\n", p); 
  27.     for(;;){ 
  28.         if(travelercount){ 
  29.             pthread_cond_signal(&taxicond); 
  30.             travelercount--; 
  31.             break; 
  32.         } 
  33.     } 
  34.  
  35.     pthread_exit(NULL); 
  36.  
  37. int main (int argc, char **argv) 
  38.     char *name; 
  39.     pthread_t thread; 
  40.     pthread_attr_t threadattr; 
  41.     pthread_attr_init(&threadattr); 
  42.  
  43.     name = "Jack"; 
  44.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  45.     sleep(1); 
  46.     name = "Susan"; 
  47.     pthread_create(&thread, &threadattr, traveler_arrive, name); 
  48.     sleep(3); 
  49.     name = "Mike"; 
  50.     pthread_create(&thread, &threadattr, taxi_arrive, name); 
  51.     sleep(4); 
  52.  
  53.     return 0; 
  54. }
  55. 编译:# gcc test.c  -lpthread
  56. 输出结果:
  57. Taxi: Jack arrives.
  58. traveler: Susan need a taxi now!
  59. traveler: Susan now got a taxi!
  60. Taxi: Mike arrives.

七、自旋锁

自旋锁与互斥量功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁!!!
自旋锁在用户态使用的比较少,在内核使用的比较多!自旋锁的使用场景:锁的持有时间比较短,或者说小于2次上下文切换的时间。
自旋锁在用户态的函数接口和互斥量一样,把pthread_mutex_xxx()中mutex换成spin,如:pthread_spin_init()。

八、总结

APUE 第 11 章,线程部分,之前没有怎么深入研究过,而且就用到了互斥量。其他的像读写锁、条件变量、屏障、自旋锁都不熟悉。
如果想继续深入研究,参看:随笔分类 - linux编程-线程
人家写的不错,而且讲的不我清楚多了。
                                   
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!