原文链接:https://blog.csdn.net/weixin_55796564/article/details/119699146
一、概述
本章介绍了线程同步的几种不同的方法,包括互斥锁、条件变量、自旋锁以及读写锁,当然,除此之外,线程同步的方法其实还有很多,譬如信号量、屏障等等,如果大家有兴趣可以自己查阅相关书籍进行学习。在实际应用开发当中,用的最多的还是互斥锁和条件变量,当然具体使用哪一种线程同步方法还是得根据场景来进行选择,方能达到事半功倍的效果!
本章来聊一聊线程同步这个话题,对于一个单线程进程来说,它不需要处理线程同步的问题,所以线程同步是在多线程环境下可能需要注意的一个问题。线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题。
**
1、为什么需要线程同步;
2、线程同步之互斥锁;
3、线程同步之信号量;
4、线程同步之条件变量;
5、线程同步之读写锁。**
13.1为什么需要线程同步?
线程同步是为了对共享资源的访问进行保护。这里说的共享资源指的是多个线程都会进行访问的资源,譬如定义了一个全局变量a,线程1访问了变量a、同样在线程2中也访问了变量a,那么此时变量a就是多个线程间的共享资源,大家都要访问它。
保护的目的是为了解决数据一致性的问题。当然什么情况下才会出现数据一致性的问题,根据不同的情况进行区分;如果每个线程访问的变量都是其它线程不会读取和修改的(譬如线程函数内定义的局部变量或者只有一个线程访问的全局变量),那么就不存在数据一致性的问题;同样,如果变量是只读的,多个线程同时读取该变量也不会有数据一致性的问题;但是,当一个线程可以修改的变量,其它的线程也可以读取或者修改的时候,这个时候就存在数据一致性的问题,需要对这些线程进行同步操作,确保它们在访问变量的存储内容时不会访问到无效的值。
出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。前面给大家介绍了,进程中的多个线程间是并发执行的,每个线程都是系统调用的基本单元,参与到系统调度队列中;对于多个线程间的共享资源,并发执行会导致对共享资源的并发访问,并发访问所带来的问题就是竞争(如果多个线程同时对共享资源进行访问就表示存在竞争,跟现实生活当中的竞争有一定的相似之处,譬如一个队伍当中需要选出一名队长,现在有两个人在候选名单中,那么意味着这两个人就存在竞争关系),并发访问就可能会出现数据一致性问题,所以就需要解决这个问题;要防止并发访问共享资源,那么就需要对共享资源的访问进行保护,防止出现并发访问共享资源。
当一个线程修改变量时,其它的线程在读取这个变量时可能会看到不一致的值,图 12.1.1描述了两个线程读写相同变量(共享变量、共享资源)的假设例子。在这个例子当中,线程A读取变量的值,然后再给这个变量赋予一个新的值,但写操作需要2个时钟周期(这里只是假设);当线程B在这两个写周期中间读取了这个变量,它就会得到不一致的值,这就出现了数据不一致的问题。
图 多线程并发访问数据不一致
我们可以编写一个简单地代码对此文件进行测试,示例代码 12.1.1展示了在2个线程在常规方式下访问共享资源,这里的共享资源指的就是静态全局变量g_count。该程序创建了两个线程,且均执行同一个函数,该函数执行一个循环,重复以下步骤:将全局变量g_count复制到本地变量l_count变量中,然后递增l_count,再把l_count复制回g_count,以此不断增加全局变量g_count的值。因为l_count是分配于线程栈中的自动变量(函数内定义的局部变量),所以每个线程都有一份。循环重复的次数要么由命令行参数指定,要么去默认值1000万次,循环结束之后线程终止,主线程回收两个线程之后,再将全局变量g_count的值打印出来。
示例代码 13.1.1 两个线程并发访问同一全局变量
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
l_count = g_count;
l_count++;
g_count = l_count;
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
/* 获取用户传递的参数 */
if (2 > argc)
loops = 10000000; //没有传递参数默认为1000万次
else
loops = atoi(argv[1]);
/* 创建2个新线程 */
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("g_count = %d\n", g_count);
exit(0);
}
编译代码,进行测试,首先执行代码,传入参数1000,也就是让每个线程对全局变量g_count递增1000次,如下所示:
图 13.1.2 1000次测试结果
都打印结果看,得到了我们想象中的结果,每个线程递增1000次,最后的数值就是2000;接着我们把递增次数加大,采用默认值1000万次,如下所示:
图 13.1.3 1000万次测试结果
可以发现,结果竟然不是我们想看到的样子,执行到最后,应该是2000万才对,这里其实就出现图 12.1.1中所示的问题,数据不一致。
如何解决对共享资源的并发访问出现数据不一致的问题?
为了解决图 12.1.1中数据不一致的问题,就得需要Linux提供的一些方法,也就是接下来将要向大家介绍的线程同步技术,来实现同一时间只允许一个线程访问该变量,防止出现并发访问的情况、消除数据不一致的问题,图 12.1.2描述了这种同步操作,从图中可知,线程A和线程B都不会同时访问这个变量,当线程A需要修改变量的值时,必须等到写操作完成之后(不能打断它的操作),才运行线程B去读取。
图 13.1.4 线程同步访问变量
线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享。不过这种便捷的共享是有代价的,必须确保多个线程不会同时修改同一变量、或者某一线程不会读取正由其它线程修改的变量,也就是必须确保不会出现对共享资源的并发访问。Linux系统提供了多种用于实现线程同步的机制,常见的方法有:互斥锁、条件变量、自旋锁以及读写锁等,下面将向大家一一进行介绍。
13.2互斥锁
互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。
举一个非常简单容易理解的例子,就拿卫生间(共享资源)来说,当来了一个人(线程)看到卫生间没人,然后它进去了、并且从里边把门锁住(互斥锁上锁)了;此时又来了两个人(线程),它们也想进卫生间方便,发生此时门打不开(互斥锁上锁失败),因为里边有人,所以此时它们只能等待(陷入阻塞);当里边的人方便完了之后(访问共享资源完成),把锁(互斥锁解锁)打开从里边出来,此时外边有两个人在等,当然它们都迫不及待想要进去(尝试对互斥锁进行上锁),自然两个人只能进去一个,进去的人再次把门锁住,另外一个人只能继续等待它出来。
在我们的程序设计当中,只有将所有线程访问共享资源都设计成相同的数据访问规则,互斥锁才能正常工作。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。
互斥锁使用pthread_mutex_t数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作。
13.2.1互斥锁初始化
1、使用PTHREAD_MUTEX_INITIALIZER宏初始化互斥锁
互斥锁使用pthread_mutex_t数据类型表示,pthread_mutex_t其实是一个结构体类型,而宏PTHREAD_MUTEX_INITIALIZER其实是一个对结构体赋值操作的封装,如下所示:
define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
所以由此可知,使用PTHREAD_MUTEX_INITIALIZER宏初始化互斥锁的操作如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
PTHREAD_MUTEX_INITIALIZER宏已经携带了互斥锁的默认属性。
2、使用pthread_mutex_init()函数初始化互斥锁
使用PTHREAD_MUTEX_INITIALIZER宏只适用于在定义的时候就直接进行初始化,对于其它情况则不能使用这种方式,譬如先定义互斥锁,后再进行初始化,或者在堆中动态分配的互斥锁,譬如使用malloc()函数申请分配的互斥锁对象,那么在这些情况下,可以使用pthread_mutex_init()函数对互斥锁进行初始化,其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
函数参数和返回值含义如下:
mutex:参数mutex是一个pthread_mutex_t类型指针,指向需要进行初始化操作的互斥锁对象;
attr:参数attr是一个pthread_mutexattr_t类型指针,指向一个pthread_mutexattr_t类型对象,该对象用于定义互斥锁的属性(在12.2.5小计中介绍),若将参数attr设置为NULL,则表示将互斥锁的属性设置为默认值,在这种情况下其实就等价于PTHREAD_MUTEX_INITIALIZER这种方式初始化,而不同之处在于,使用宏不进行错误检查。
返回值:成功返回0;失败将返回一个非0的错误码。
Tips:注意,当在Ubuntu系统下执行"man 3 pthread_mutex_init"命令时提示找不到该函数,并不是Linux下没有这个函数,而是该函数相关的man手册帮助信息没有被安装,这时我们只需执行"sudo apt-get install manpages-posix-dev"安装即可。
使用pthread_mutex_init()函数对互斥锁进行初始化示例:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
或者:
pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);
13.2.2互斥锁加锁和解锁
互斥锁初始化之后,处于一个未锁定状态,调用函数pthread_mutex_lock()可以对互斥锁加锁、获取互斥锁,而调用函数pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数mutex指向互斥锁对象;pthread_mutex_lock()和pthread_mutex_unlock()在调用成功时返回0;失败将返回一个非0值的错误码。
调用pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用pthread_mutex_lock()会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。
调用pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:
对处于未锁定状态的互斥锁进行解锁操作;
解锁由其它线程锁定的互斥锁。
如果有多个线程处于阻塞状态等待互斥锁被解锁,当互斥锁被当前锁定它的线程调用pthread_mutex_unlock()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会如愿以偿!
使用示例
使用互斥锁的方式将示例代码 12.1.1进行修改,修改之后如示例代码 12.2.1所示,使用了一个互斥锁来保护对全局变量g_count的访问。
示例代码 13.2.1 使用互斥锁保护全局变量的访问
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
pthread_mutex_lock(&mutex); //互斥锁上锁
l_count = g_count;
l_count++;
g_count = l_count;
pthread_mutex_unlock(&mutex);//互斥锁解锁
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
/* 获取用户传递的参数 */
if (2 > argc)
loops = 10000000; //没有传递参数默认为1000万次
else
loops = atoi(argv[1]);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, NULL);
/* 创建2个新线程 */
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("g_count = %d\n", g_count);
exit(0);
}
在测试运行,使用默认值1000万次,如下所示:
图 13.2.1 测试结果
可以看到确实得到了我们想看到的正确结果,每次对g_count的累加总是能够保持正确,但是在运行程序的过程中,明显会感觉到锁消耗的时间会比较长,这就涉及到性能的问题了,后续会介绍!
13.2.3pthread_mutex_trylock()函数
当互斥锁已经被其它线程锁住时,调用pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用pthread_mutex_trylock()函数;调用pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码EBUSY。
其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数mutex指向目标互斥锁,成功返回0,失败返回一个非0值的错误码,如果目标互斥锁已经被其它线程锁住,则调用失败返回EBUSY。
使用示例
使用pthread_mutex_trylock()替换pthread_mutex_lock()。
以非阻塞方式对互斥锁进行加锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
while(pthread_mutex_trylock(&mutex)); //以非阻塞方式上锁
l_count = g_count;
l_count++;
g_count = l_count;
pthread_mutex_unlock(&mutex);//互斥锁解锁
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
/* 获取用户传递的参数 */
if (2 > argc)
loops = 10000000; //没有传递参数默认为1000万次
else
loops = atoi(argv[1]);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, NULL);
/* 创建2个新线程 */
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("g_count = %d\n", g_count);
exit(0);
}
整个执行结果跟使用pthread_mutex_lock()效果是一样的,大家可以自己测试。
13.2.4销毁互斥锁
当不再需要互斥锁时,应该将其销毁,通过调用pthread_mutex_destroy()函数来销毁互斥锁,其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数mutex指向目标互斥锁;同样在调用成功情况下返回0,失败返回一个非0值的错误码。
1、不能销毁还没有解锁的互斥锁,否则将会出现错误;
2、没有初始化的互斥锁也不能销毁。
被pthread_mutex_destroy()销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用pthread_mutex_init()对互斥锁进行初始化之后才能使用。
使用示例
对示例代码 12.2.1进行修改,在进程退出之前,使用pthread_mutex_destroy()函数销毁互斥锁。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
pthread_mutex_lock(&mutex); //互斥锁上锁
l_count = g_count;
l_count++;
g_count = l_count;
pthread_mutex_unlock(&mutex);//互斥锁解锁
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
/* 获取用户传递的参数 */
if (2 > argc)
loops = 10000000; //没有传递参数默认为1000万次
else
loops = atoi(argv[1]);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, NULL);
/* 创建2个新线程 */
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("g_count = %d\n", g_count);
/* 销毁互斥锁 */
pthread_mutex_destroy(&mutex);
exit(0);
}
13.2.5互斥锁死锁
试想一下,如果一个线程试图对同一个互斥锁加锁两次,会出现什么情况?情况就是该线程会陷入死锁状态,一直被阻塞永远出不来;这就是出现死锁的一种情况,除此之外,使用互斥锁还有其它很多种方式也能产生死锁。
**有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;**譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,
并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。如下示例代码中所示:
// 线程A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
// 线程B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);
这就好比是C语言中两个头文件相互包含的关系,那肯定编译报错!
在我们的程序当中,如果用到了多个互斥锁,要避免此类死锁的问题,最简单的方式就是定义互斥锁的层级关系,当多个线程对一组互斥锁操作时,总是应该按照相同的顺序对该组互斥锁进行锁定。譬如在上述场景中,如果两个线程总是先锁定mutex1在锁定mutex2,死锁就不会出现。有时,互斥锁之间的层级关系逻辑不够清晰,即使是这样,依然可以设计出所有线程都必须遵循的强制层级顺序。
但有时候,应用程序的结构使得对互斥锁进行排序是很困难的,程序复杂、其中所涉及到的互斥锁以及共享资源比较多,程序设计实在无法按照相同的顺序对一组互斥锁进行锁定,那么就必须采用另外的方法**。譬如使用pthread_mutex_trylock()以不阻塞的方式尝试对互斥锁进行**加锁,在这种方案中,线程先使用函数pthread_mutex_lock()锁定第一个互斥锁,然后使用pthread_mutex_trylock()来锁定其余的互斥锁。如果任一pthread_mutex_trylock()调用失败(返回EBUSY),那么该线程释放所有互斥锁,可以经过一段时间之后从头再试。与第一种按照层级关系来避免死锁的方法变比,这种方法效率要低一些,因为可能需要经历多次循环。
解决互斥锁死锁的问题还有很多方法,笔者也没详细地去学习过,当大家在实际编程应用中需要用到这些知识再去查阅相关资料、书籍进行学习。
13.2.6互斥锁的属性
如前所述,调用pthread_mutex_init()函数初始化互斥锁时可以设置互斥锁的属性,通过参数attr指定。参数attr指向一个pthread_mutexattr_t类型对象,该对象对互斥锁的属性进行定义,当然,如果将参数attr设置为NULL,则表示将互斥锁属性设置为默认值。关于互斥锁的属性本书不打算深入讨论互斥锁属性的细节,也不会将pthread_mutexattr_t类型中定义的属性一一列出。
如果不使用默认属性,在调用pthread_mutex_init()函数时,参数attr必须要指向一个pthread_mutexattr_t对象,而不能使用NULL。当定义pthread_mutexattr_t对象之后,需要使用pthread_mutexattr_init()函数对该对象进行初始化操作,当对象不再使用时,需要使用pthread_mutexattr_destroy()将其销毁,函数原型如下所示:
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
参数attr指向需要进行初始化的pthread_mutexattr_t对象,调用成功返回0,失败将返回非0值的错误码。
pthread_mutexattr_init()函数将使用默认的互斥锁属性初始化参数attr指向的pthread_mutexattr_t对象。关于互斥锁的属性比较多,譬如进程共享属性、健壮属性、类型属性等等,本书并不会一一给大家进行介绍,本小节讨论下类型属性,其它的暂时不去解释了。
互斥锁的类型属性控制着互斥锁的锁定特性,一共有4中类型:
1、PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。
2、PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次),返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
3、PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
4、PTHREAD_MUTEX_DEFAULT:此类互斥锁提供默认的行为和特性。使用宏PTHREAD_MUTEX_INITIALIZER初始化的互斥锁,或者调用参数arg为NULL的pthread_mutexattr_init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大灵活性,Linux上,PTHREAD_MUTEX_DEFAULT类型互斥锁的行为与PTHREAD_MUTEX_NORMAL类型相仿。
可以使用pthread_mutexattr_gettype()函数得到互斥锁的类型属性,使用pthread_mutexattr_settype()修改/设置互斥锁类型属性,其函数原型如下所示:
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
参数attr指向pthread_mutexattr_t类型对象;对于pthread_mutexattr_gettype()函数,函数调用成功会将互斥锁类型属性保存在参数type所指向的内存中,通过它返回出来;而对于pthread_mutexattr_settype()函数,会将参数attr指向的pthread_mutexattr_t对象的类型属性设置为参数type指定的类型。使用方式如下:
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
/* 初始化互斥锁属性对象 */
pthread_mutexattr_init(&attr);
/* 将类型属性设置为PTHREAD_MUTEX_NORMAL */
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, &attr);
…
/* 使用完之后 */
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
13.3条件变量
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。使用条件变量主要包括两个动作:
1、一个线程等待某个条件满足而被阻塞;
2、另一个线程中,条件满足时发出“信号”。
为了说明这个问题,来看一个没有使用条件变量的例子,生产者—消费者模式,生产者这边负责生产产品、而消费者负责消费产品,对于消费者来说,没有产品的时候只能等待产品出来,有产品就使用它。
这里我们使用一个变量来表示这个这个产品,生产者生产一件产品变量加1,消费者消费一次变量减1,示例代码如下所示:
示例代码 13.3.1 生产者—消费者示例代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_avail = 0;
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
for ( ; ; ) {
pthread_mutex_lock(&mutex);//上锁
while (g_avail > 0)
g_avail--; //消费
pthread_mutex_unlock(&mutex);//解锁
}
return (void *)0;
}
/* 主线程(生产者) */
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, NULL);
/* 创建新线程 */
ret = pthread_create(&tid, NULL, consumer_thread, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
for ( ; ; ) {
pthread_mutex_lock(&mutex);//上锁
g_avail++; //生产
pthread_mutex_unlock(&mutex);//解锁
}
exit(0);
}
此代码中,主线程作为“生产者”,新创建的线程作为“消费者”,运行之后它们都回处于死循环中,所以代码中没有加入销毁互斥锁、等待回收新线程相关的代码,进程终止时会自动被处理。
上述代码虽然可行,但由于新线程中会不停的循环检查全局变量g_avail是否大于0,故而造成CPU资源的浪费。采用条件变量这一问题就可以迎刃而解!条件变量允许一个线程休眠(阻塞等待)直至获取到另一个线程的通知(收到信号)再去执行自己的操作,譬如上述代码中,当条件g_avail > 0不成立时,消费者线程会进入休眠状态,而生产者生成产品后(g_avail++,此时g_avail将会大于0),向处于等待状态的线程发出“信号”,而其它线程收到“信号”之后,便会被唤醒!
Tips:这里提到的信号并不是第八章内容所指的信号,需要区分开来!
前面说到,条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题。
13.3.1条件变量初始化
条件变量使用pthread_cond_t数据类型来表示,类似于互斥锁,在使用条件变量之前必须对其进行初始化。初始化方式同样也有两种:使用宏PTHREAD_COND_INITIALIZER或者使用函数pthread_cond_init(),使用宏的初始化方法与互斥锁的初始化宏一样,这里就不再重述!譬如:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_init()函数原型如下所示:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
使用pthread_cond_init()函数初始化条件变量,当不再使用时,使用pthread_cond_destroy()销毁条件变量。
参数cond指向pthread_cond_t条件变量对象,对于pthread_cond_init()函数,类似于互斥锁,在初始化条件变量时设置条件变量的属性,参数attr指向一个pthread_condattr_t类型对象,pthread_condattr_t数据类型用于描述条件变量的属性。可将参数attr设置为NULL,表示使用属性的默认值来初始化条件变量,与使用PTHREAD_COND_INITIALIZER宏相同。
函数调用成功返回0,失败将返回一个非0值的错误码。
对于初始化与销毁操作,有以下问题需要注意:
1、在使用条件变量之前必须对条件变量进行初始化操作,使用PTHREAD_COND_INITIALIZER宏或者函数pthread_cond_init()都行;
2、对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
3、对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
4、对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
5、经pthread_cond_destroy()销毁的条件变量,可以再次调用pthread_cond_init()对其进行重新初始化。
13.3.2通知和等待条件变量
条件变量的主要操作便是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
函数pthread_cond_signal()和pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。
pthread_cond_signal()和pthread_cond_broadcast()函数原型如下所示:
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
参数cond指向目标条件变量,向该条件变量发送信号。调用成功返回0;失败将返回一个非0值的错误码。
pthread_cond_signal()和pthread_cond_broadcast()的区别在于:二者对阻塞于pthread_cond_wait()的多个线程对应的处理方式不同,pthread_cond_signal()函数至少能唤醒一个线程,而pthread_cond_broadcast()函数则能唤醒所有线程。使用pthread_cond_broadcast()函数总能产生正确的结果,唤醒所有等待状态的线程,但函数pthread_cond_signal()会更为高效,因为它只需确保至少唤醒一个线程即可,所以如果我们的程序当中,只有一个处于等待状态的线程,使用pthread_cond_signal()更好,具体使用哪个函数根据实际情况进行选择!
2、pthread_cond_wait()函数原型如下所示:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
当程序当中使用条件变量,当判断某个条件不满足时,调用pthread_cond_wait()函数将线程设置为等待状态(阻塞)。pthread_cond_wait()函数包含两个参数:
cond:指向需要等待的条件变量,目标条件变量;
mutex:参数mutex是一个pthread_mutex_t类型指针,指向一个互斥锁对象;前面开头便给大家介绍了,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的。
返回值:调用成功返回0;失败将返回一个非0值的错误码。
在pthread_cond_wait()函数内部会对参数mutex所指定的互斥锁进行操作,通常情况下,条件判断以及pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。调用pthread_cond_wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表上,然后将互斥锁解锁;当pthread_cond_wait()被唤醒返回时,会再次锁住互斥锁。
注意注意的是,条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。如果调用pthread_cond_signal()和pthread_cond_broadcast()向指定条件变量发送信号时,若无任何线程等待该条件变量,这个信号也就会不了了之。
当调用pthread_cond_broadcast()同时唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。
当消费者线程没有产品可消费时,让它处于等待状态,知道生产者把产品生产出来;当生产者把产品生产出来之后,再去通知消费者。
示例代码 13.3.2 使用条件变量和互斥锁实现线程同步
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex; //定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_avail = 0; //全局共享资源
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
for ( ; ; ) {
pthread_mutex_lock(&mutex);//上锁
while (0 >= g_avail)
pthread_cond_wait(&cond, &mutex);//等待条件满足
while (0 < g_avail)
g_avail--; //消费
pthread_mutex_unlock(&mutex);//解锁
}
return (void *)0;
}
/* 主线程(生产者) */
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
/* 初始化互斥锁和条件变量 */
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
/* 创建新线程 */
ret = pthread_create(&tid, NULL, consumer_thread, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
for ( ; ; ) {
pthread_mutex_lock(&mutex);//上锁
g_avail++; //生产
pthread_mutex_unlock(&mutex);//解锁
pthread_cond_signal(&cond);//向条件变量发送信号
}
exit(0);
}
全局变量g_avail作为主线程和新线程之间的共享资源,两个线程在访问它们之间首先会对互斥锁进行上锁,消费者线程中,当判断没有产品可被消费时(g_avail <= 0),调用pthread_cond_wait()使得线程陷入等待状态,等待条件变量,等待生产者制造产品;调用pthread_cond_wait()后线程阻塞并解锁互斥锁;而在生产者线程中,它的任务是生产产品(使用g_avail++来模拟),产品生产完成之后,调用pthread_mutex_unlock()将互斥锁解锁,并调用pthread_cond_signal()向条件变量发送信号;这将会唤醒处于等待该条件变量的消费者线程,唤醒之后再次自动获取互斥锁,然后再对产品进行消费(g_avai–模拟)。
13.3.3条件变量的判断条件
使用条件变量,都会有与之相关的判断条件,通常情况下,会涉及到一个或多个共享变量。譬如在示例代码 12.3.2中,与条件变量相关的判断是(0 >= g_avail)。细心的读者会发现,在这份示例代码中,我们使用了while循环、而不是if语句,来控制对pthread_cond_wait()的调用,这是为何呢?
必须使用while循环,而不是if语句,这是一种通用的设计原则:当线程从pthread_cond_wait()返回时,并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。
从pthread_cond_wait()返回后,并不能确定判断条件是真还是假,其理由如下:
1、当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。譬如示例代码 12.3.2中,如果有两个或更多个消费者线程,当其中一个消费者线程从pthread_cond_wait()返回后,它会将全局共享变量g_avail的值变成0,导致判断条件的状态由真变成假。
2、可能会发出虚假的通知。
13.3.4条件变量的属性
如前所述,调用pthread_cond_init()函数初始化条件变量时,可以设置条件变量的属性,通过参数attr指定。参数attr指向一个pthread_condattr_t类型对象,该对象对条件变量的属性进行定义,当然,如果将参数attr设置为NULL,表示使用默认值来初始化条件变量属性。
关于条件变量的属性本书不打算深入讨论,条件变量包括两个属性:进程共享属性和时钟属性。每个属性都提供了相应的get方法和set方法,各位读者如果有兴趣,可自行查阅资料学习,本书不再介绍!
三、自旋锁
内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择
1、一种是原地等待。
2、一种是挂起当前进程,进入休眠,系统调度其他进程执行
linux 内核中最常见的锁就是Spinlock自旋锁,自旋锁是“原地等待”的方式解决资源冲突的,即一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”。
自旋锁优点:
1、在线程时,自旋锁不会使线程状态发生切换,一直处于用户态,即线程—直都是运行的,不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux )调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)。
在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用 semaphore或者mutex的锁机制,但是现在中断上下文也掺和进来,那些可以导致睡眠的 lock就不能使用了,这时候,可以考虑使用spinlock。
在中断上下文,是不允许睡眠的,因为发送睡眠,进程被切换,保护现场的寄存器,被重写入其他进程的数据,要恢复上一个进程的调度信息时,数据已经被覆盖了,是回不来的,这样导致锁被占用,所以,这里需要的是一个不会导致睡眠的锁——spinlock。换言之,中断上下文要用锁,首选 spinlock。
我们要访问临界资源需要首先申请自旋锁,获取不到锁就自旋,如果能获得锁就进入临界区,当自旋锁释放后,自旋在这个锁的任务即可获得锁并进入临界区,退出临界区的任务必须释放自旋锁。
自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。
如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态进入休眠;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者是否已经释放了锁,“自旋”一词因此得名。
自旋锁的不足之处在于:自旋锁一直占用的CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着CPU,如果不能在很短的时间内获取锁,这无疑会使CPU效率降低。
试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为PTHREAD_MUTEX_ERRORCHECK类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。
因此我们要谨慎使用自旋锁,自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!
综上所述,再来总结下自旋锁与互斥锁之间的区别:
1、实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
2、开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得CPU使用效率降低,故自旋锁不适用于等待时间比较长的情况。
3、使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!
自旋锁死锁的2种情况
1、拥有自旋锁的进程A在内核态,休眠阻塞了,或者一直while循环等待条件的满足,死循环了,内核调度B进程,碰巧B进程也要获得自旋锁,此时B只能自旋转。而此时抢占已经关闭,不会调度A讲程了,B永远自旋产生死锁。
2、进程A拥有自旋锁,中断到来,CPU执行中断函数,中断处理函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,也只能自旋,产生死锁。
13.4.1自旋锁初始化
自旋锁使用pthread_spinlock_t数据类型表示,当定义自旋锁后,需要使用pthread_spin_init()函数对其进行初始化,当不再使用自旋锁时,调用pthread_spin_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
参数lock指向了需要进行初始化或销毁的自旋锁对象,参数pshared表示自旋锁的进程共享属性,可以取值如下:
1、PTHREAD_PROCESS_SHARED:共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
2、PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁。
这两个函数在调用成功的情况下返回0;失败将返回一个非0值的错误码。
13.4.2自旋锁加锁和解锁
可以使用pthread_spin_lock()函数或pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为EBUSY。不管以何种方式加锁,自旋锁都可以使用pthread_spin_unlock()函数对自旋锁进行解锁。
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
参数lock指向自旋锁对象,调用成功返回0,失败将返回一个非0值的错误码。
如果自旋锁处于未锁定状态,调用pthread_spin_lock()会将其锁定(上锁),如果其它线程已经将自旋锁锁住了,那本次调用将会“自旋”等待;如果试图对同一自旋锁加锁两次必然会导致死锁。
使用自旋锁替换互斥锁来实现线程同步,对共享资源的访问进行保护。使用自旋锁实现线程同步
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_spinlock_t spin;//定义自旋锁
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
pthread_spin_lock(&spin); //自旋锁上锁
l_count = g_count;
l_count++;
g_count = l_count;
pthread_spin_unlock(&spin);//自旋锁解锁
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
/* 获取用户传递的参数 */
if (2 > argc)
loops = 10000000; //没有传递参数默认为1000万次
else
loops = atoi(argv[1]);
/* 初始化自旋锁(私有) */
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
/* 创建2个新线程 */
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("g_count = %d\n", g_count);
/* 销毁自旋锁 */
pthread_spin_destroy(&spin);
exit(0);
}
运行结果:
图 13.4.1 测试结果
将互斥锁替换为自旋锁之后,测试结果打印也是没有问题的,并且通过对比可以发现,替换为自旋锁之后,程序运行所耗费的时间明显变短了,说明自旋锁确实比互斥锁效率要高,但是一定要注意自旋锁所适用的场景。
四、读写锁
互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有3种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁有如下两个规则:
1、当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
2、当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。
所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;当读写锁处于读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。所以在应用程序当中,使用读写锁实现线程同步,当线程需要对共享数据进行读操作时,需要先获取读模式锁(对读模式锁进行加锁),当读取操作完成之后再释放读模式锁(对读模式锁进行解锁);当线程需要对共享数据进行写操作时,需要先获取到写模式锁,当写操作完成之后再释放写模式锁。
读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。
13.5.1读写锁初始化
与互斥锁、自旋锁类似,在使用读写锁之前也必须对读写锁进行初始化操作,读写锁使用pthread_rwlock_t数据类型表示,读写锁的初始化可以使用宏PTHREAD_RWLOCK_INITIALIZER或者函数pthread_rwlock_init(),其初始化方式与互斥锁相同,譬如使用宏PTHREAD_RWLOCK_INITIALIZER进行初始化必须在定义读写锁时就对其进行初始化:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
对于其它方式可以使用pthread_rwlock_init()函数对其进行初始化,当读写锁不再使用时,需要调用pthread_rwlock_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
调用成功返回0,失败将返回一个非0值的错误码。
参数rwlock指向需要进行初始化或销毁的读写锁对象。对于pthread_rwlock_init()函数,参数attr是一个pthread_rwlockattr_t *类型指针,指向pthread_rwlockattr_t对象。pthread_rwlockattr_t数据类型定义了读写锁的属性(在12.5.3小节中介绍),若将参数attr设置为NULL,则表示将读写锁的属性设置为默认值,在这种情况下其实就等价于PTHREAD_RWLOCK_INITIALIZER这种方式初始化,而不同之处在于,使用宏不进行错误检查。
当读写锁不再使用时,需要调用pthread_rwlock_destroy()函数将其销毁。
读写锁初始化使用示例:
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
…
pthread_rwlock_destroy(&rwlock);
13.5.2读写锁上锁和解锁
以读模式对读写锁进行上锁,需要调用pthread_rwlock_rdlock()函数;以写模式对读写锁进行上锁,需要调用pthread_rwlock_wrlock()函数。不管是以何种方式锁住读写锁,均可以调用pthread_rwlock_unlock()函数解锁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数rwlock指向读写锁对象。调用成功返回0,失败返回一个非0值的错误码。
当读写锁处于写模式加锁状态时,其它线程调用pthread_rwlock_rdlock()或pthread_rwlock_wrlock()函数均会获取锁失败,从而陷入阻塞等待状态;
当读写锁处于读模式加锁状态时,其它线程调用pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用pthread_rwlock_wrlock()函数则不能获取到锁,从而陷入阻塞等待状态。
如果线程不希望被阻塞,可以调用pthread_rwlock_tryrdlock()和pthread_rwlock_trywrlock()来尝试加锁,如果不可以获取锁时。这两个函数都会立马返回错误,错误码为EBUSY。其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
参数rwlock指向需要加锁的读写锁,加锁成功返回0,加锁失败则返回EBUSY。
示例代码 演示了使用读写锁来实现线程同步,全局变量g_count作为线程间的共享变量,主线程中创建了5个读取g_count变量的线程,它们使用同一个函数read_thread,这5个线程仅仅对g_count变量进行读取,并将其打印出来,连带打印线程的编号(15);主线程中还创建了5个写g_count变量的线程,它们使用同一个函数write_thread,write_thread函数中会将g_count变量的值进行累加,循环10次,每次将g_count变量的值在原来的基础上增加20,并将其打印出来,连带打印线程的编号(15)。
示例代码 13.5.1 使用读写锁实现线程同步
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock;//定义读写锁
static int g_count = 0;
static void *read_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
printf("读线程<%d>, g_count=%d\n", number+1, g_count);
pthread_rwlock_unlock(&rwlock);//解锁
sleep(1);
}
return (void *)0;
}
static void *write_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);
pthread_rwlock_unlock(&rwlock);//解锁
sleep(1);
}
return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
pthread_t tid[10];
int j;
/* 对读写锁进行初始化 */
pthread_rwlock_init(&rwlock, NULL);
/* 创建5个读g_count变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j], NULL, read_thread, &nums[j]);
/* 创建5个写g_count变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
/* 等待线程结束 */
for (j = 0; j < 10; j++)
pthread_join(tid[j], NULL);//回收线程
/* 销毁自旋锁 */
pthread_rwlock_destroy(&rwlock);
exit(0);
}
编译测试,其打印结果如下:
图 13.5.2 测试结果
在这个例子中,我们演示了读写锁的使用,但仅作为演示使用,在实际的应用编程中,需要根据应用场景来选择是否使用读写锁。
13.5.3读写锁的属性
读写锁与互斥锁类似,也是有属性的,读写锁的属性使用pthread_rwlockattr_t数据类型来表示,当定义pthread_rwlockattr_t对象时,需要使用pthread_rwlockattr_init()函数对其进行初始化操作,初始化会将pthread_rwlockattr_t对象定义的各个读写锁属性初始化为默认值;当不再使用pthread_rwlockattr_t对象时,需要调用pthread_rwlockattr_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
参数attr指向需要进行初始化或销毁的pthread_rwlockattr_t对象;函数调用成功返回0,失败将返回一个非0值的错误码。
读写锁只有一个属性,那便是进程共享属性,它与互斥锁以及自旋锁的进程共享属性相同。Linux下提供了相应的函数用于设置或获取读写锁的共享属性。函数pthread_rwlockattr_getpshared()用于从pthread_rwlockattr_t对象中获取共享属性,函数pthread_rwlockattr_setpshared()用于设置pthread_rwlockattr_t对象中的共享属性,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
函数pthread_rwlockattr_getpshared()参数和返回值:
attr:指向pthread_rwlockattr_t对象;
pshared:调用pthread_rwlockattr_getpshared()获取共享属性,将其保存在参数pshared所指向的内存中;
返回值:成功返回0,失败将返回一个非0值的错误码。
函数pthread_rwlockattr_setpshared()参数和返回值:
attr:指向pthread_rwlockattr_t对象;
pshared:调用pthread_rwlockattr_setpshared()设置读写锁的共享属性,将其设置为参数pshared指定的值。参数pshared可取值如下:
1、PTHREAD_PROCESS_SHARED:共享读写锁。该读写锁可以在多个进程中的线程之间共享;
2、PTHREAD_PROCESS_PRIVATE:私有读写锁。只有本进程内的线程才能够使用该读写锁,这是读写锁共享属性的默认值。
返回值:调用成功的情况下返回0;失败将返回一个非0值的错误码。
使用方式如下:
pthread_rwlock_t rwlock; //定义读写锁
pthread_rwlockattr_t attr; //定义读写锁属性
/* 初始化读写锁属性对象 */
pthread_rwlockattr_init(&attr);
/* 将进程共享属性设置为PTHREAD_PROCESS_PRIVATE */
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
/* 初始化读写锁 */
pthread_rwlock_init(&rwlock, &attr);
…
/* 使用完之后 */
pthread_rwlock_destroy(&rwlock); //销毁读写锁
pthread_rwlockattr_destroy(&attr); //销毁读写锁属性对象
13.6总结
本章介绍了线程同步的几种不同的方法,包括互斥锁、条件变量、自旋锁以及读写锁,当然,除此之外,线程同步的方法其实还有很多,譬如信号量、屏障等等,如果大家有兴趣可以自己查阅相关书籍进行学习。在实际应用开发当中,用的最多的还是互斥锁和条件变量,当然具体使用哪一种线程同步方法还是得根据场景来进行选择,方能达到事半功倍的效果!
1、死锁:
什么是死锁?
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
2、死锁产生的原因:
(1)竞争不可抢占性资源;
竞争可消耗资源;
进程推进顺序不当;
(2)其根本原因是:系统资源不够
(3)产生死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不可抢占条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
3、处理死锁的办法
预防死锁;
避免死锁;
检测死锁;
解除死锁;
预防死锁:预防死锁的办法是通过产生破坏产生死锁的四个必要条件的一个或几个。其中互斥条件是非共享设备所必须的,不能够改变,还用加以保证,因此 主要破坏产生死锁的后三个条件。
(1)请求与保持条件:两个协议保证;
(2)不可抢占条件:该进程已占有的资源再次请求时不能够满足必须释放当前已有的资源,即被抢占了,破坏“不可抢占”条件;
(3)循环等待条件:
避免死锁:同是属于事先预防策略,破坏产生死锁的必要条件,而是在资源动态分配过程中,防止系统进入不安全状态,以避免发生死锁,主要方法是银行家算法来避免发生死锁;
一,死锁原因
死锁问题大概可以分为3种情况:
-
接口卡主锁不能及时释放,导致别的线程卡主,这类不算真正意义的死锁。
-
同一个线程锁多次导致死锁。
-
两个锁交叉使用导致死锁。
这三种情况的定位方法都是一样,都是根据锁的owner顺藤摸瓜,比如线程1在等锁,必然有别的线程在占有锁,可以查看线程1中的锁的owner找到线程2,依次类推,知道把锁的关系理清楚就知道死锁原因了。
自旋锁死锁的2种情况
1)拥有自旋锁的进程A在内核态阻塞了,内核调度B进程,碰巧B进程也要获得自旋锁,此时B只能自旋转。而此时抢占已经关闭,不会调度A讲程了,B永远自旋产生死锁。
2)进程A拥有自旋锁,中断到来,CPU执行中断函数,中断处理函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,也只能自旋,产生死锁。