1. 基础知识

多个线程同时访问共享数据时可能会冲突,比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:

    1. 从内存读变量值到寄存器

    2. 寄存器的值加1

    3. 将寄存器的值写回内存

可能你执行到这三条指令中的某一条时,时间片到,另一个线程也执行这三条指令,就会发生访问冲突,如下代码

//unmutex.c
  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<pthread.h>
  4 
  5 #define NLOOP 5000
  6 
  7 static int g_count=0;
  8 void* thread(void* val)
  9 {
 10     int x=0;
 11     int i=0;
 12     for(;i<NLOOP;i++)
 13     {
 14         x=g_count;
 15         printf("pthread id is:%u,g_count is:%d\n",(unsigned long)pthread_self(),g_count);
 16         g_count=x+1;
 17     }   
 18     return NULL;
 19 }   
 20 int main()
 21 {
 22     pthread_t tid1;
 23     pthread_t tid2;
 24     pthread_create(&tid1,NULL,thread,NULL);
 25     pthread_create(&tid2,NULL,thread,NULL);
 26     pthread_join(tid1,NULL);
 27     pthread_join(tid2,NULL);
 28     printf("g_count final val is:%d\n",g_count);
 29     return 0;
 30 }
 
 //makefile
  1 unmutex:unmutex.c
  2     gcc -o $@ $^ -lpthread
  3 .PHONY:clean
  4 clean:
  5     rm -f unmutex

输出结果:

wKiom1cZhTrzBJA1AABAp6s6w2w674.png

结果分析:

   我们创建两个线程各自把g_count加5000次,预想最后结果为10000,但实际为5000多-6000,且每次运行结果不一样。这是因为出现了访问冲突。

   解决办法:加入互斥锁。


2.互斥锁(Mutex,Mutua1 Exclusive Lock)

Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:

wKioL1cZqpHQpV5zAAAvVuOeruE556.png

2.1相关函数:

wKiom1cZqS3iXRUoAAAvVuOeruE458.png

wKioL1cZierhSQStAAAnwLYFrVA642.png

成功返回0,失败返回错误码

2.2代码实现

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<pthread.h>
  4 
  5 #define NLOOP 5000
  6 
  7 static int g_count=0;
  8 pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
  9 void* thread(void* val)
 10 {
 11 
 12     int x=0;
 13     int i=0;
 14     for(;i<NLOOP;i++)
 15     {
 16         pthread_mutex_lock(&mutex);
 17         x=g_count;
 18         printf("pthread id is:%u,g_count is:%d\n",(unsigned long)pthread_self(),g_count);
 19         g_count=x+1;
 20         pthread_mutex_unlock(&mutex);
 21     }
 22     return NULL;
 23 }
 24 int main()
 25 {
 26     pthread_t tid1;
 27     pthread_t tid2;
 28     pthread_create(&tid1,NULL,thread,NULL);
 29     pthread_create(&tid2,NULL,thread,NULL);
 30     pthread_join(tid1,NULL);
 31     pthread_join(tid2,NULL);
 32     printf("g_count final val is:%d\n",g_count);
 33     return 0;
 34 }

输出结果:

wKioL1cXRGWiDSFmAAA8fkAYIMo291.png

结果分析:

  我们引入了互斥锁。即把上面的“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。


3.互斥锁操作的实现:

  大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 

wKioL1cZs17TkQsBAABiMkYV0yM181.png

                   ▼

wKiom1cZsyahBk2UAAB_cJW0n9A957.png


4.死锁问题

4.1发生死锁的情形:

    1.如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此 就永远处于挂起等待状了。

     2.线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。


4.2解决方法

    1.在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁。

    2.如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock 调用


5.条件变量(Condition Variable)

5.1定义

  线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。

Condition Variable用pthread_cond_t类型的变量表示,可以这样初始化和销毁:

wKioL1cZw5XxukaFAAAtY6-EYK8735.png  

5.2相关函数

和Mutex的初始化和销毁类似,pthread_cond_init函数初始化一个条件变量,attr参数为NULL则表示缺省属性,pthread_cond_destroy函数销毁一个条件变量。如果条件变量是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER初始化,相当于用pthread_cond_init函数初始化并且attr参数为NULL。条件变量的操作可以用下列函数

wKioL1cZxo3hMKSBAAA0ANzktlQ635.png

返回值:成功返回0,失败返回错误号


  可见,一个条件变量总是和一个Mutex搭配使用。一个线程可以调用pthread_cond_wait在一个条件变量上阻塞等待,这个函数做以下三步操作:

    1. 释放Mutex

    2. 阻塞等待

    3. 当被唤醒时,重新获得Mutex并返回


6.信号量

(1)Mutex变量是非0即1的,可看作一种资源的可用数量。

初始化时Mutex是1,表示有一个可用资源,加锁时获得该资源,将Mutex减到0,表示不再有可用资源,解锁时释放该资源,将Mutex重新加到1,表示又有了一个可用资源。

(2)信号量(Semaphore)和Mutex类似,表示可用资源的数量。

(3)区别:和Mutex不同的是这个数量可以大于1。(如果信号量描述的资源数目是1时,此时的信号量和互斥锁相同!)


7.读写锁

(1)这种锁相对于自旋锁言,能提高并发性。 

(2)因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最可能的读者数为实际的逻辑CPU数。 

(3)写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关)

(4)但不能同时既有读者又有写者。