【线程同步机制】Day13线程同步:互斥锁、条件变量、自旋锁、读写锁

进程间通信详解,移步:https://blog.csdn.net/Thmos_vader/article/details/140743256

线程同步

对于一个单线程进程来说,不需要处理线程同步的问题,所以线程同步是在多线程环境下是需要注意的一个问题。

线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题

本文来学习如何使用线程同步机制来避免这样的问题!

⚫ 为什么需要线程同步;

⚫ 线程同步之互斥锁;

⚫ 线程同步之信号量;

⚫ 线程同步之条件变量;

⚫ 线程同步之读写锁。

拓展

⚫互斥锁死锁的解决:

​ https://blog.csdn.net/qq_44045338/article/details/104769194

​ http://t.csdnimg.cn/CdZtF

⚫进程同步和通信解决的问题(见下)


同步与通信解决的问题

进程同步:指多个进程或线程之间按照一定的顺序执行,以避免竞争条件和不一致的结果;

进程通信:指进程之间交换信息和共享资源的机制,使它们能够相互协作和协调工作;

进程间同步与通信,易混淆,详见链接解疑:

https://cloud.tencent.com/developer/article/2304029


线程同步目的

  1. 线程同步是为了对共享资源的访问进行保护。

    这里说的共享资源指的是多个线程都会进行访问的资源;
    譬如定义了一个全局变量 a,线程 1 访问了变量 a、同样在线程 2 中也访问了变量 a,那么此时变量 a 就是
    多个线程间的共享资源,大家都要访问它。
    
  2. 保护的目的是为了解决数据一致性的问题。

    当然什么情况下才会出现数据一致性的问题,根据不同的情况进行区分;
    	如果每个线程访问的变量都是其它线程不会读取和修改的(譬如线程函数内定义的局部变量或者只有一个线程访
    问的全局变量),那么就不存在数据一致性的问题;
    	同样,如果变量是只读的,多个线程同时读取该变量也不会有数据一致性的问题;
    	但是,当一个线程可以修改的变量,其它的线程也可以读取或者修改的时候,这个时候就存在数据一致性的问
    题,需要对这些线程进行同步操作,确保它们在访问变量的存储内容时不会访问到无效的值。
    
  3. 出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。

    	前面已介绍,进程中的多个线程间是并发执行的,每个线程都是系统调用的基本单元,参与到系统调度队列中;
    	对于多个线程间的共享资源,并发执行会导致对共享资源的并发访问,并发访问所带来的问题就是竞争(如果多个线程同时对共享资源进行访问就表示存在竞争,跟现实生活当中的竞争有一定的相似之处,譬如一个队伍当中需要选出一名队长,现在有两个人在候选名单中,那么意味着这两个人就存在竞争关系),并发访问就可能会出现数据一致性问题,所以就需要解决这个问题;
    	要防止并发访问共享资源,那么就需要对共享资源的访问进行保护,防止出现并发访问共享资源。
    
问题引入
	当一个线程修改变量时,其它的线程在读取这个变量时可能会看到不一致的值,图 12.1.1 描述了两个线程读写相同变量(共享变量、共享资源)的假设例子。
	在这个例子当中,线程 A 读取变量的值,然后再给这个变量赋予一个新的值,但写操作需要 2 个时钟周期(这里只是假设);当线程 B 在这两个写周期中间读取了这个变量,它就会得到不一致的值,这就出现了数据不一致的问题。

在这里插入图片描述

典例
	在 2 个线程在常规方式下访问共享资源,这里的共享资源指的就是静态全局变量 g_count。
	该程序创建了两个线程,且均执行同一个函数,该函数执行一个循环,重复以下步骤:
	将全局变量 g_count 复制到本地变量 l_count 变量中,然后递增l_count,再把 l_count 复制回 g_count,以此不断增加全局变量 g_count 的值。
	因为 l_count 是分配于线程栈中的自动变量(函数内定义的局部变量),所以每个线程都有一份。
	循环重复的次数要么由命令行参数指定,要么去默认值 1000 万次,循环结束之后线程终止,主线程回收两个线程之后,再将全局变量 g_count 的值打印出来。
					/*示例代码 12.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);
    原子哥在线教学:www.yuanzige.com 论坛:http://www.openedv.com/forum.php
    434
    I.MX6U 嵌入式 Linux C 应用编程指南
     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次:

每个线程递增 1000 次,最后的数值就是 2000;

在这里插入图片描述

接着我们把递增次数加大,采用默认值 1000 万次:

可以发现,结果与预计不一致,执行到最后,应该是 2000 万才对,其实出现图12.1.1 中所示问题,数据不一致

问题解决
  1. 如何解决对共享资源的并发访问出现数据不一致的问题?

线程同步技术:实现同一时间只允许一个线程访问该变量,防止出现并发访问的情况、消除数据不一致的问题。

图解:

图 12.1.4 描述了这种同步操作,从图中可知,线程 A 和线程 B 都不会同时访问这个变量,当线程 A 需要修改变量的值时,必须等到写操作完成之后(不能打断它的操作),才运行线程 B 去读取。

在这里插入图片描述

线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享。

不过这种便捷的共享是有代价:必须确保多个线程不会同时修改同一变量、或者某一线程不会读取正由其它线程修改的变量,也就是必须确保不会出现对共享资源的并发访问

Linux 系统提供了多种用于实现线程同步的机制,常见的方法有:互斥锁、条件变量、自旋锁以及读写锁


互斥锁(mutex)

  • 又叫互斥量,本质上说是一把锁;

  • 访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);

    • 1.对互斥锁进行上锁后,任何其它试图再对互斥锁进行加锁的线程都被阻塞,直到当前线程释放互斥锁。

    • 2.释放互斥锁时有一个以上线程阻塞,那这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁

    • 3.当有一个线程成功对互斥锁上锁后,其它线程就不能再次上锁,只能再次陷入阻塞,等待下一次解锁。

    • 典例:
      卫生间(共享资源):
      1.当来了一个人(线程)看到卫生间没人,然后它进去了、并且从里边把门锁住(互斥锁上锁)了;
      2.此时又来了两个人(线程),它们也想进卫生间方便,发生此时门打不开(互斥锁上锁失败),因为里边有     人,所以此时它们只能等待(陷入阻塞);
      3.当里边的人方便完了之后(访问共享资源完成),把锁(互斥锁解锁)打开从里边出来,此时外边有两个人在	等,当然它们都迫不及待想要进去(尝试对互斥锁进行上锁),自然两个人只能进去一个,进去的人再次把门   锁住,另外一个人只能继续等待它出来。
      
  • 程序设计当中,只有将所有线程访问共享资源都设计成相同的数据访问规则,互斥锁才能正常工作。

    如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。

互斥锁初始化pthread_mutex_t
  • 互斥锁使用 pthread_mutex_t 数据类型表示,pthread_mutex_t 其实是一个结构体类型

    #define PTHREAD_MUTEX_INITIALIZER \
     { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
    
  • 互斥锁初始化之后,处于一个未锁定状态;

  • 在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作;

    • 使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁
    • 使用 pthread_mutex_init() 函数初始化互斥锁
PTHREAD_MUTEX_INITIALIZER宏初始化互斥锁
  • 宏PTHREAD_MUTEX_INITIALIZER 其实是一个对结构体赋值操作的封装

  • 宏只适用于在定义的时候就直接进行初始化

    PTHREAD_MUTEX_INITIALIZER 宏已经携带了互斥锁的默认属性。

原型

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthreadmutex_init()函数初始化互斥锁
  • 这些情况下,可以使用 pthread_mutex_init()函数对互斥锁进行初始化:

    譬如先定义互斥锁,后再进行初始化,或者在堆中动态分配的互斥锁;
    譬如使用 malloc()函数申请分配的互斥锁对象;
    

原型

#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 类型对象;
返回值:
	成功返回 0;
	失败将返回一个非 0 的错误码
pthread_mutexattr_t 类型对象:
	该对象用于定义互斥锁的属性(在后文介绍);
	若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为默认值,
	在这种情况下其实就等价于 PTHREAD_MUTEX_INITIALIZER 这种方式初始化,
	而不同之处在于,使用宏不进行错误检查。

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);
加锁和解锁
  • 互斥锁初始化之后,处于一个未锁定状态;
pthread_mutex_lock()
  • 调用函数 pthread_mutex_lock() 可以对互斥锁加锁、获取互斥锁
    • 若互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;
    • 若互斥锁已被锁定,那pthread_mutex_lock()会一直阻塞到该互斥锁被解锁时,锁定互斥锁并返回;
pthread_mutex_unlock()
  • 调用函数 pthread_mutex_unlock() 可以对互斥锁解锁、释放互斥锁
    • 已经处于锁定状态的互斥锁进行解锁,以下行为均错误!!!
      • 对处于未锁定状态的互斥锁进行解锁操作;
      • 解锁由其它线程锁定的互斥锁;
  • 如果有多个线程处于阻塞状态等待互斥锁被解锁,当互斥锁被解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会如愿以偿!

原型

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/*
参数:
	mutex:指向互斥锁对象;
返回值:
	调用成功时返回 0;
	失败将返回一个非 0 值的错误码。

典例

修改示例代码 12.2.1,使用了一个互斥锁来保护对全局变量 g_count 的访问.

				/*示例代码 12.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 万次:

在这里插入图片描述

得到了我们想看到的正确结果,每次对 g_count 的累加总是能够保持正确;

但是在运行程序的过程中,明显会感觉到锁消耗的时间会比较长,这就涉及到性能的问题;

pthread_mutex_trylock()不想加锁被阻塞的加锁
  • 当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁

    如果线程不希望被阻塞,可以使用 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。

典例

对示例代码 12.2.1 进行修改,使用 pthread_mutex_trylock()替换 pthread_mutex_lock()

					/*示例代码 12.2.2 以非阻塞方式对互斥锁进行加锁*/
#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_destroy()函数来销毁互斥锁

pthread_mutex_destroy()销毁互斥锁
  • 未解锁、初始化的互斥锁不能销毁,否则将会出现错误;

  • 被 pthread_mutex_destroy()销毁之后的互斥锁

    • 不能再对它进行上锁和解锁

    • 需要再次调用pthread_mutex_init()对互斥锁进行初始化之后才能使用

原型

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*
参数:
	mutex 指向目标互斥锁;
返回值:
	成功:返回 0,
	失败:返回一个非 0 值的错误码。

典例

对示例代码 12.2.1 进行修改,在进程退出之前,使用 pthread_mutex_destroy()函数销毁互斥锁

					/*示例代码 12.2.3 销毁互斥锁*/
#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);
}
互斥锁死锁
产生原因
  • 使用互斥锁产生死锁方式

    • 同一个互斥锁加锁两次,该线程会陷入死锁状态一直被阻塞永远出不来;
    • 一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理,
      当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;
    	譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。
    	因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁;
    

典例

好比是 C 语言中两个头文件相互包含的关系,那肯定编译报错!

// 线程 A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
// 线程 B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);
解决方法
  • 如果用到了多个互斥锁,要避免此类死锁的问题,最简单的方式就是定义互斥锁的层级关系;

  • 当多个线程对一组互斥锁操作时,总是应该按照相同的顺序对该组互斥锁进行锁定。

    譬如在上述场景中,如果两个线程总是先锁定 mutex1 在锁定 mutex2,死锁就不会出现。
    有时,互斥锁之间的层级关系逻辑不够清晰,即使是这样,依然可以设计出所有线程都必须遵循的强制层级顺序。
    
  • pthread_mutex_trylock()以不阻塞的方式尝试对互斥锁进行加锁

    	有时候,应用程序的结构使得对互斥锁进行排序是很困难的,程序复杂、其中所涉及到的互斥锁以及共享资源比较多,程序设计实在无法按照相同的顺序对一组互斥锁进行锁定,那么就必须采用另外的方法。
    	譬如使用 pthread_mutex_trylock()以不阻塞的方式尝试对互斥锁进行加锁,在这种方案中,线程先使用函数pthread_mutex_lock()锁定第一个互斥锁,然后使用 pthread_mutex_trylock()来锁定其余的互斥锁。
    	如果任一pthread_mutex_trylock()调用失败(返回 EBUSY),那么该线程释放所有互斥锁,可以经过一段时间之后从头再试。
    	与第一种按照层级关系来避免死锁的方法变比,这种方法效率要低一些,因为可能需要经历多次循环。
    

解决互斥锁死锁的问题还有很多方法,详见:

​ https://blog.csdn.net/qq_44045338/article/details/104769194

​ http://t.csdnimg.cn/CdZtF

互斥锁的属性pthread_mutexattr_t
  • 如前所述,调用 pthread_mutex_init()函数初始化互斥锁时可以设置互斥锁的属性,通过参数 attr 指定

  • 参数 attr 指向一个 pthread_mutexattr_t 类型对象来对属性进行定义,如将 attr设为 NULL,则为默认值

  • 属性:用则初始化,不用销毁

pthread_mutexattr_init()
  • 定义 pthread_mutexattr_t 对象后,需要使用 pthread_mutexattr_init()函数对该对象进行初始化操作
pthread_mutexattr_destroy()
  • 当对象不再使用时,需要使用 pthread_mutexattr_destroy()将其销毁

原型

  • pthread_mutexattr_init()函数将使用默认的互斥锁属性初始化参数 attr 指向的 pthread_mutexattr_t 对象。
#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 值的错误码。

关于互斥锁的属性比较多,譬如进程共享属性、健壮属性、类型属性等等,不详细讨论

类型属性
  • 互斥锁的类型属性控制着互斥锁的锁定特性,一共有 4 中类型:

    ⚫ PTHREAD_MUTEX_NORMAL:
    	一种标准的互斥锁类型,不做任何的错误检查或死锁检测。
    	如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;
    	互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。
    ⚫ PTHREAD_MUTEX_ERRORCHECK:
    	这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
    	此类互斥锁会提供错误检查。
    	譬如这三种情况都会导致返回错误:
    		线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次),返回错误;
    		线程对由其它线程锁定的互斥锁进行解锁,返回错误;
    		线程对处于未锁定状态的互斥锁进行解锁,返回错误。
    ⚫ PTHREAD_MUTEX_RECURSIVE:
    	此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;
    	所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
    ⚫ 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 类型对象;
返回值:
	gettype()函数:
	成功:将互斥锁类型属性保存在参数 type 所指向的内存中,通过它返回出来;
	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);

条件变量

  • 条件变量 (condition variable)用于自动阻塞线程,直至某个特定事件发生或某个条件满足为止;

  • 通常情况下,条件变量是和互斥锁一起搭配使用,主要的两个使用动作:

    • 一个线程等待某个条件满足而被阻塞;

    • 另一个线程中,条件满足时发出“信号”。

    • 条件变量通常搭配互斥锁来使用:
      	因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题!!!
      
问题提出
一个没有使用条件变量的例子**,生产者---消费者模式;
生产者这边负责生产产品、而消费者负责消费产品;
对于消费者来说,没有产品的时候只能等待产品出来,有产品就使用它。

典例

使用一个变量来表示这个这个产品,生产者生产一件产品变量加 1,消费者消费一次变量减 1;

					/*示例代码 12.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:这里提到的信号并不是第八章内容所指的信号,需要区分开来!

条件变量初始化pthread_cond_t
  • 条件变量使用 pthread_cond_t 数据类型表示,pthread_cond_t其实是一个结构体类型

  • 在使用条件变量之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作;

    • 使用 PTHREAD_COND_INITIALIZER 宏初始化条件变量
    • 使用 pthread_cond_init() 函数初始化条件变量
PTHREAD_COND_INITIALIZER 宏初始化
  • 宏PTHREAD_COND_INITIALIZER 其实是一个对结构体赋值操作的封装

  • 宏只适用于在定义的时候就直接进行初始化

    宏PTHREAD_COND_INITIALIZER 已经携带了条件变量的默认属性。

原型

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_init()函数初始化
  • 使用 pthread_cond_init()函数初始化条件变量,当不再使用时,使用 pthread_cond_destroy()销毁条件变量

  • 可将参数 attr 设置为 NULL,表示使用属性的默认值来初始化条件变量,与使用宏初始化效果相同

原型

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
/*
参数:
	cond:指向 pthread_cond_t 条件变量对象,对于 pthread_cond_init()函数,类似于互斥锁,在初始化条件变量时设置条件变量的属性,
	attr:指向一 pthread_condattr_t 类型对象,pthread_condattr_t 数据类型用于描述条件变量的属性。
返回值:
	函数调用成功返回 0;
	失败将返回一个非 0 值的错误码。

注意

初始化与销毁操作,有以下问题需要注意:

⚫ 在使用条件变量之前必须对条件变量进行初始化操作,使用宏或者函数初始化都行;
⚫ 对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
⚫ 对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
⚫ 对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
⚫ 经 pthread_cond_destroy()销毁的条件变量,可以再次调用 pthread_cond_init()对其进行重新初始化。
通知和等待条件变量
  • 条件变量的主要操作便是发送信号(signal)和等待
    • 发送信号操作通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足;
    • 等待操作:指在收到一个通知前一直处于阻塞状态;
pthread_cond_signal()
pthread_cond_broadcast()
  • 函数 pthread_cond_signal()和 pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多

    个处于等待状态的线程。

  • 区别:二者对阻塞于 pthread_cond_wait()的多个线程对应的处理方式不同

    • pthread_cond_signal()函数至少能唤醒一个线程;
    • pthread_cond_broadcast()函数则能唤醒所有线程.
  • 发送信号通知进程的函数选择

    使用 pthread_cond_broadcast()函数总能产生正确的结果,唤醒所有等待状态的线程,
    但函数 pthread_cond_signal()会更为高效,因为它只需确保至少唤醒一个线程即可,所以如果我们的程序当中,只有一个处于等待状态的线程,使用 pthread_cond_signal()更好;
    具体使用哪个函数根据实际情况进行选择!
    

原型

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
/*
参数:
	cond:指向目标条件变量,向该条件变量发送信号。
返回值:
	调用成功返回 0;失败将返回一个非 0 值的错误码。
pthread_cond_wait()
  • 调用 函数是线程阻塞,直到收到条件变量的通知;

  • 当程序当中使用条件变量,当判断某个条件不满足时,调用 pthread_cond_wait()函数将线程设置为等待

    状态(阻塞)。

原理

	在 pthread_cond_wait()函数内部会对参数 mutex 所指定的互斥锁进行操作,通常情况下,条件判断以及pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁。
	调用pthread_cond_wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表上,然后将互斥锁解锁;当 pthread_cond_wait()被唤醒返回时,会再次锁住互斥锁。**
	当调用 pthread_cond_broadcast()同时唤醒所有线程时,**互斥锁也只能被某一线程锁住,其它线程获取锁**
**失败又会陷入阻塞**。

原型

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/*
前面开头便给大家介绍:
	条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的。
参数:
	cond:指向需要等待的条件变量,目标条件变量;
	mutex:一个pthread_mutex_t 类型指针,指向一个互斥锁对象;
返回值:
	调用成功返回 0;
	失败将返回一个非 0 值的错误码。

典例

使用条件变量对示例代码 12.3.1 进行修改,当消费者线程没有产品可消费时,让它处于等待状态,知道生产者把产品生产出来;当生产者把产品生产出来之后,再去通知消费者。

				/*示例代码 12.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--模拟)。
*/
条件变量的判断条件

使用条件变量,都会有与之相关的判断条件,通常情况下,会涉及到一个或多个共享变量。

	譬如在示例代码 12.3.2 中,与条件变量**相关的判断是(0 >= g_avail)**。
	细心的读者会发现,在这份示例代码中,我们使用了 while 循环、而不是 if 语句,来控制对 pthread_cond_wait()的调用,这是为何呢?
	必须使用 while 循环,而不是 if 语句,这是一种**通用的设计原则**:
	当线程从 pthread_cond_wait()返回时,并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。

从 pthread_cond_wait()返回后,并不能确定判断条件是真还是假,其理由如下:

⚫ 当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。
	譬如示例代码 12.3.2 中,如果有两个或更多个消费者线程,当其中一个消费者线程从 pthread_cond_wait()返回后,它会将全局共享变量 g_avail 的值变成 0,导致判断条件的状态由真变成假。
⚫ 可能会发出虚假的通知。
条件变量的属性
  • 调用 pthread_cond_init()函数初始化条件变量可以设置条件变量的属性为参数 attr 指定;
  • 参数 attr 指向一个 pthread_condattr_t 类型对象,该对象对条件变量的属性进行定义;
  • 如果将参数attr 设置为 NULL,表示使用默认值来初始化条件变量属性。
  • 条件变量包括两个属性:进程共享属性和时钟属性。每个属性都提供了相应的 get 方法和 set 方法

自旋锁

  • 自旋锁与互斥锁相似;本质上说也是一把锁,在访问共享资源前进行上锁,访问完后释放自旋锁(解锁)

  • 如果在获取自旋锁时:

    • 自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);

    • 自旋锁已经处于锁定状态,则获取锁操作将原地“自旋”,直到该自旋锁的持有者释放锁。

与互斥锁区别
  • 无法获取到锁时,互斥锁阻塞等待状态,自旋锁**原地“自旋”**等待状态;

    “自旋”:
    	调用者一直在循环查看该自旋锁的持有者是否已经释放了锁;
    不足之处:
    	自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋),
    	所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。
    
  • 实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层;

在这里插入图片描述

适用情况

要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁;

而“自旋”等待的线程也只需等待很短的时间,在这种情况下就比较适合使用自旋锁,效率高!

在这里插入图片描述

自旋锁初始化pthread_spinlock_t
  • 自旋锁使用 pthread_spinlock_t 数据类型表示;
    • 当定义自旋锁后,需要使用 pthread_spin_init()函数对其进行初始化
    • 当不再使用自旋锁时,调用 pthread_spin_destroy()函数将其销毁
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 表示自旋锁的进程共享属性;
	取值如下:
		⚫ PTHREAD_PROCESS_SHARED: 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
		⚫ PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁。
返回值:
	调用成功的情况下返回 0;
	失败将返回一个非 0 值的错误码。
自旋锁加锁和解锁
  • **pthread_spin_lock()**函数或 pthread_spin_trylock()函数对自旋锁进行加锁
pthread_spin_lock()
  • 如果试图对同一自旋锁加锁两次必然会导致死锁。
  • 其它线程对自旋锁已加锁未获取到锁时一直“自旋”,处于未锁定状态将其锁定(上锁);
pthread_spin_trylock()
  • 如果未能获取到锁,就立刻返回错误,错误码为 EBUSY。
pthread_spin_unlock()
  • 不管以何种方式加锁,自旋锁都可以使用 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 值的错误码。

典例

对示例代码 12.2.1 进行修改,使用自旋锁替换互斥锁来实现线程同步,对共享资源的访问进行保护

				/*示例代码 12.4.1 使用自旋锁实现线程同步*/
#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);
}

在这里插入图片描述


读写锁

  • 互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁

    读写锁有3 种状态

    • 读模式下的加锁状态(以下简称读加锁状态);

    • 写模式下的加锁状态(以下简称写加锁状态);

    • 不加锁状态(见图)。

  • 一次只有一个线程占有写模式的读写锁,但可有多个线程同时占有读模式的读写锁,并行性更高

在这里插入图片描述

  • 读写锁也叫做共享互斥锁
    • 当读写锁是读模式锁住时,就可以说成是共享模式锁住
    • 当读写锁是写模式锁住时,就可以说成是互斥模式锁住
读写锁的两个规则
⚫ 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
⚫ 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
	虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。
	所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。

	当读写锁处于写模式加锁状态时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;
	当读写锁处于读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。
	所以在应用程序当中,使用读写锁实现线程同步,当线程需要对共享数据进行读操作时,需要先获取读模式锁(对读模式锁进行加锁),当读取操作完成之后再释放读模式锁(对读模式锁进行解锁);
	当线程需要对共享数据进行写操作时,需要先获取到写模式锁,当写操作完成之后再释放写模式锁。
读写锁初始化pthread_rwlock_t
  • 读写锁使用pthread_rwlock_t 数据类型表示,读写锁的初始化可以使用宏或者函数
  • 宏 PTHREAD_RWLOCK_INITIALIZER 进行初始化必须在定义读写锁时就对其进行初始化:
  • 其它方式可以使用 pthread_rwlock_init()函数对其进行初始化;
  • 当读写锁不再使用时,需要调用pthread_rwlock_destroy()函数将其销毁
PTHREAD_RWLOCK_INITIALIZER
pthread_rwlock_init()
pthread_rwlock_destroy()
  • pthread_rwlockattr_t 数据类型定义了读写锁的属性(在 12.5.3 小节中介绍);

  • 若将参数 attr 设置为 NULL,则表示将读写锁的属性设置为默认值,在这种情况下其实就等价于 PTHREAD_RWLOCK_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查

原型

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
/*
参数:
	rwlock:	指向需要进行初始化或销毁的读写锁对象;
	attr:	是一个pthread_rwlockattr_t *类型指针,指向 pthread_rwlockattr_t 对象。

典例

读写锁初始化使用示例:

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
......
pthread_rwlock_destroy(&rwlock);
读写锁上锁和解锁
  • 读模式对读写锁进行上锁,需要调用 pthread_rwlock_rdlock()函数;

    写模式对读写锁进行上锁,需要调用 pthread_rwlock_wrlock()函数。

  • 不管是以何种方式锁住读写锁,均可以调用 pthread_rwlock_unlock()函数解锁

阻塞加锁
pthread_rwlock_rdlock()
pthread_rwlock_wrlock()
当读写锁处于读模式加锁状态时,
其它线程调用pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用 pthread_rwlock_wrlock()函数则不能获取到锁,从而陷入阻塞等待状态。

当读写锁处于写模式加锁状态时:
其它线程调用 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_tryrdlock()
pthread_rwlock_trywrlock()

如果线程不希望被阻塞,可以调用 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。

典例

示例代码 12.5.1 演示了使用读写锁来实现线程同步,全局变量 g_count 作为线程间的共享变量,主线程中创建了 5 个读取 g_count 变量的线程,它们使用同一个函数 read_thread,这 5 个线程仅仅对 g_count 变量进行读取,并将其打印出来,连带打印线程的编号(1~5);主线程中还创建了 5 个写 g_count 变量的线程,它们使用同一个函数 write_thread,write_thread 函数中会将 g_count 变量的值进行累加,循环 10 次,每次将 g_count 变量的值在原来的基础上增加 20,并将其打印出来,连带打印线程的编号(1~5)。

				/*示例代码 12.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);
}

在这里插入图片描述

读写锁的属性 pthread_rwlockattr_t
  • 读写锁的属性使用 pthread_rwlockattr_t 数据类型来表示,只有一个属性,那便是进程共享属性

  • 定义pthread_rwlockattr_t 对象时,需要使用 pthread_rwlockattr_init()函数对其进行初始化操作,初始化会将pthread_rwlockattr_t 对象定义的各个读写锁属性初始化为默认值

  • 不再使用 pthread_rwlockattr_t 对象时,需要调用 pthread_rwlockattr_destroy()函数将其销毁

初始化与销毁
pthread_rwlockattr_init()
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对象中的共享属性

pthread_rwlockattr_getpshared()
pthread_rwlockattr_setpshared()

原型

#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 可取值如下:
				⚫ PTHREAD_PROCESS_SHARED:共享读写锁。该读写锁可以在多个进程中的线程之间共享;
				⚫ 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); //销毁读写锁属性对象

总结

线程同步的几种不同的方法,包括互斥锁、条件变量、自旋锁以及读写锁

除此之外,线程同步的方法其实还有很多,譬如信号量、屏障等等;

在实际应用开发当中,用的最多的还是互斥锁和条件变量,具体使用哪一种还是得根据场景来进行选择

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值