序言
实验室项目采用多线程实现,然而暂时只涉及到几个基本的线程操作函数,线程和进程的区别、线程的同步和异步机制以及线程通信等暂时都没有涉及,打算在这里做些总结,以备后用。本文打算学习死锁和线程同步。
1. 死锁
死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。
举例说明: 资源S1,S2; 进程P1,P2
资源S1,S2都是不可剥夺资源(内存是可剥夺资源):一个进程申请了之后,不能强制收回,只能进程结束之后自动释放;
进程P1申请了资源S1,进程P2申请了资源S2;
接下来P1的操作用到资源S2,P2的资源用到资源S1。但是P1,P2都得不到接下来的资源,那么就引发了死锁。举例说明:线程t1,t2,t3
如果三个线程t1,t2,t3要实现同步,在某种情况下,t1在等t2,t2要等t3,而此时t3却在等t1… 那么问题来了,很显然,t1,t2,t3都不会运行,这种现象叫死锁。若无外力作用,这些线程都将无法向前推进。这种情况在我们的程序中是不允许出现的,这种无限的等待没有意义。
1.1 死锁产生的主要原因
系统资源竞争:资源分配不当,以及系统资源的竞争导致系统资源不足,导致死锁;
进程运行推进顺序不合适:进程在运行过程中,请求和释放资源的顺序不当,导致死锁。
1.2 产生死锁的四个必要条件
互斥条件:一个资源一次只能被一个进程使用。
请求与保持条件: 进程已经保持了至少一个资源,又提出了新的资源请求,而该资源已经被其他进程占有。此时请求进程被阻塞,而对自己已获得的资源保持不放。
不可剥夺条件:进程所获得的资源在未使用完之前,不能被其他进程强行夺走,即只能由进程自己主动释放。
循环等待条件:若干进程间形成首尾相接循环等待资源的关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而”只要上述条件之一不满足,就不会发生死锁“。
1.3 死锁的避免与预防
死锁避免
系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。
在系统设计、进程调度等方面注意如何让这四个必要条件不成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。死锁预防
死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现。
死锁避免不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁,死锁避免是在系统运行过程中注意避免死锁的最终发生。
2. 同步
文中打算介绍三种线程同步机制:互斥锁 + 条件变量 + 读写锁
- 互斥锁:适用于线程可用的资源只有一个的情况
- 信号量:适用同时可用的资源为多个的情况
- 读写锁:提高互斥锁在数据库系统数据访问(大量读,较少写)等应用领域的效率
2.1 互斥锁
2.1.1 互斥锁原理
互斥锁以排他方式防止共享数据被并发访问。互斥锁是一个二元变量,只有锁定(禁止1)和解锁(允许0)两种状态,互斥锁可以看作是特殊意义的全局变量,因为在同一时刻只有一个线程能够对互斥锁进行操作。
将某个共享资源与某个特定互斥锁在逻辑上绑定,即要申请该资源必须先获取锁。对该共享资源的访问操作如下:
(1) 首先申请互斥锁,如果该互斥锁处于锁定状态,默认阻塞当前线程;如果处于解锁状态,则申请到该锁并立即占有该锁,使锁处于锁定状态防止其他线程访问该资源。
(2) 只有锁定该互斥锁的线程才能释放该互斥锁,其他先吃呢个试图释放操作无效。
2.1.2 互斥锁基本操作函数
功能 | 函数 |
---|---|
初始化互斥锁 | pthread_mutex_init |
阻塞申请互斥锁 | pthread_mutex_lock |
非阻塞申请互斥锁 | pthread_mutex_trylock |
释放互斥锁 | pthread_mutex_unlock |
销毁互斥锁 | pthread_mutex_destroy |
使用互斥锁前先定义该互斥锁(全局变量)
pthread_mutex_t lock;
在使用互斥锁以前,必须首先对它进行初始化
静态分配的互斥锁:置为常量PTHREAD_MUTEX_INITIALIZER,属性为NULL,也可以调用pthread_mutex_init函数
动态分配的互斥锁:例如通过调用malloc函数分配的互斥锁,只能调用pthread_mutex_init,且在释放内存前需要调用pthread_mutex_destroy
(1) 初始化互斥锁
int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
形参:
mutex 要初始化的互斥锁的指针
mutexattr 要初始化的互斥锁的属性;NULL表示使用默认属性
其他:也可使用宏初始化静态分配的互斥锁
#define PTHREAD_MUTEX_INITIALIZER {{0,}}
pthread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER;
返回值:成功返回0,否则返回一个错误编号
(2) 销毁互斥锁
调用pthread_mutex_init初始化的互斥锁,在释放内存前需要调用pthread_mutex_destroy
int pthread_mutex_destroy (pthread_mutex_t *mutex);
形参:
mutex 指向要初始化的互斥锁的指针
返回值:成功返回0,否则返回一个错误编号
(3) 阻塞方式申请互斥锁
int pthread_mutex_lock (pthread_mutex_t *mutex);
说明:如果一个线程要占用一个共享资源,必须先申请一个对应的互斥锁
返回值:成功返回0,否则返回一个错误编号
(4) 非阻塞方式申请互斥锁
int pthread_mutex_trylock (pthread_mutex_t *mutex);
返回值:成功返回0,否则返回一个错误编号,以指明错误
(5) 释放互斥锁
int pthread_mutex_unlock (pthread_mutex_t *mutex);
说明:释放操作只能有占有该互斥锁的线程完成
返回值:成功返回0,否则返回一个错误编号
2.2 条件变量
2.2.1 条件变量原理
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。条件变量不能单独使用,必须配合互斥锁一起实现对资源的互斥访问。
条件变量分为两部分:条件和变量。条件本身是由互斥量保护的,线程在改变条件状态前先要锁住互斥量。
条件变量使线程睡眠等待某种条件出现。条件变量主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。
条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
2.2.2 条件变量基本操作函数
功能 | 函数 |
---|---|
初始化条件变量 | pthread_cond_init |
阻塞等待条件变量 | pthread_cond_wait |
在指定的时间内阻塞等待条件变量 | pthread_cond_timedwait |
通知等待该条件变量的第一个线程 | pthread_cond_signal |
通知等待该条件变量的所有线程 | pthread_cond_broadcast |
销毁条件变量状态 | pthread_cond_destroy |
使用条件变量前,先定义该条件变量(全局变量)
pthread_cond_t condition;
pthread_cond_t数据类型的条件变量可以用两种方式进行初始化
静态分配的条件变量:把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,属性为NULL
动态分配的条件变量:使用pthread_cond_init函数进行初始化
(1) 初始化条件变量
int pthread_cond_init (pthread_cond_t *restrict cond, pthread_condattr_t *restrict cond_attr);
说明:使用属性attr来初始化条件变量cond
形参:
cond 指向要初始化的条件变量指针
cond_attr 指向属性对象的指针,该属性对象定义要初始化的条件变量的特性;NULL表示使用默认属性
返回值:成功返回0,否则返回错误编号以指明错误
pthread_cond_t cond;
pthread_condattr_t cattr;
int ret; //返回值
ret = pthread_cond_init(&cond, NULL); //默认属性初始化条件变量
ret = pthread_cond_init(&cond, &cattr); //特定属性初始化条件变量
(2) 通知等待条件变量的线程
int pthread_cond_signal (pthread_cond_t *cond);
说明:通知等待条件变量的第一个线程
其他:如果cond没有阻塞任何线程,则此函数不起作用
如果cond阻塞了多个线程,则调度策略将确定要取消阻塞的线程
显然在此函数被调用时隐含了释放当前线程占用的信号量的操作
返回值:成功返回0,否则返回错误编号以指明错误
int pthread_cond_broadcast (pthread_cond_t *cond);
说明:唤醒等待与条件变量cond关联的条件的所有线程
其他:如果cond上没有阻塞任何线程,则此函数不起作用
返回值:成功返回0,否则返回错误编号以指明错误
(3) 等待条件变量
int pthread_cond_wait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
说明:阻塞等待某个条件变量
形参:
cond 指向要等待的条件变量的指针
mutex 指向与条件变量cond关联的互斥锁的指针
返回值:成功返回0,否则返回一个错误编号
int pthread_cond_timedwait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
说明:在指定的时间范围内等待条件变量
形参:
cond 指向要等待的条件变量的指针
mutex 指向与条件变量cond关联的互斥锁的指针
abstime 等待过期时的绝对时间,如果在此时间范围内取到该条件变量函数将返回
使用UTC时钟,即为一个绝对时间,该数据结构声明如下
struct timespec
{
long ts_sec; //秒部分
long ts_nsec; //纳秒部分
}
使用这个结构时,需要指定愿意等待多长时间,时间值是一个绝对数而不是相对数。
例如,如果能等待3分钟,就需要把当前时间加上3分钟再转换到timespec结构,而不是把3分钟转换成timespec结构。
其他:wait和timedwait函数都包含了一个互斥锁。
如果线程因等待条件变量而进入等待状态,将隐含释放其申请的互斥锁;
同样,在返回时,银行申请到该互斥锁对象操作
(4) 销毁条件变量
在释放底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行去除初始化(deinitialize)。
int pthread_cond_destroy (pthread_cond_t *cond);
返回值:成功返回0,否则返回错误编号以指明错误
互斥锁是为了上锁而设计的,条件变量是为了等待而设计的。
2.3 读写锁
2.3.1 读写锁原理
读写锁与互斥锁类似,不过读写锁允许更高的并行性。互斥锁要么是锁定状态要么是解锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。
读写锁非常适合于对数据结构读的次数远大于写的情况,例如对数据库系统数据的访问。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读模式下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。
读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。
读写锁分为读锁和写锁,具体如下:
(1) 如果某线程申请了读锁,其他线程可以再申请读锁,但不能申请写锁
(2) 如果某线程申请了写锁,则其他线程不能在申请读锁也不能申请写锁
2.3.2 读写锁的基本操作函数
功能 | 函数 |
---|---|
初始化读写锁 | pthread_rwlock_init |
阻塞申请读锁 | pthread_rwlock_rdlock |
非阻塞申请读锁 | pthread_rwlock_tryrdlock |
阻塞申请写锁 | pthread_rwlock_wrlock |
非阻塞申请写锁 | pthread_rwlock_trywrlock |
释放锁(读锁和写锁) | pthread_rwlock_unlock |
销毁读写锁) | pthread_rwlock_destroy |
与互斥锁一样,读写锁在使用之前必须初始化,在释放它们底层的内存前必须销毁
pthread_rwlock_t rwlock; //全局变量
pthread_rwlock_t数据类型的读写锁可以用两种方式进行初始化
静态分配的读写锁:把常量PTHREAD_COND_INITIALIZER赋给静态分配的读写锁,属性为NULL
动态分配的读写锁:使用pthread_cond_init函数进行初始化
区别在于:静态初始化不执行错误检查,使用默认属性初始化读写锁
(1) 初始化读写锁
int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
说明:使用属性attr来初始化读写锁,如果attr为NULL则使用默认的读写锁属性
形参:
rwlock 指向要初始化的读写锁的指针
attr 指向属性对象的指针,该属性对象定义要初始化的读写锁的特性
返回值:成功返回0,否则返回错误编号以指明错误
(2) 申请读锁
int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);
说明:以阻塞方式申请读锁
返回值:成功返回0,否则返回错误编号以指明错误
其他:如果不能申请到该读锁,pthread_rwlock_rdlock将阻塞当前进程
int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);
说明:以非阻塞方式申请读锁
返回值:成功返回0,否则返回错误编号以指明错误
其他:如果不能申请到该读锁,pthread_rwlock_tryrdlock将返回错误
(3) 申请写锁
int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);
说明:以阻塞方式申请写锁
返回值:成功返回0,否则返回错误编号以指明错误
其他:如果不能申请到该写锁,pthread_rwlock_wrlock将阻塞当前进程
int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);
说明:以非阻塞方式申请写锁
返回值:成功返回0,否则返回错误编号以指明错误
其他:如果不能申请到该写锁,pthread_rwlock_trywrlock将返回错误
注:申请读锁和写锁的形参都是全局变量rwlock
(4) 解锁
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);
说明:如果无论是读锁还是写锁,都使用该函数来释放锁
返回值:成功返回0,否则返回错误编号以指明错误
其他:
(1) 如果调用该函数来释放读锁,但当前还有其他读锁定,则保持读锁定状态,只不过当前线程已不再是其所有者之一
如果释放最后一个读锁,则读写锁将处于解锁状态
(2) 如果调用此函数释放写锁,则置读写锁为解锁状态
(5) 销毁读写锁
在释放读写锁占用的内存之前,需要调用pthread_rwlock_destroy做清理工作。
int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);
返回值:成功返回0,否则返回一个错误编号以指明错误
如果pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就丢失了。
Acknowledgements:
http://blog.csdn.net/jhonz/article/details/52786280
http://blog.csdn.net/u014588619/article/details/44684575
http://www.cnblogs.com/feisky/archive/2010/03/08/1680950.html
http://www.cnblogs.com/nufangrensheng/p/3521654.html
《高级程序设计-第三版》,人民邮电出版社,杨宗德、吕光宏等著
2017.04.17