Linux线程(十)线程互斥锁-互斥锁概念详解

互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,

它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。

举一个非常简单容易理解的例子,就拿卫生间(共享资源)来说,当来了一个人(线程)看到卫生间没人,然后它进去了、并且从里边把门锁住(互斥锁上锁)了;此时又来了两个人(线程),它们也想进卫生间方便,发生此时门打不开(互斥锁上锁失败),因为里边有人,所以此时它们只能等待(陷入阻塞);当里边的人方便完了之后(访问共享资源完成),把锁(互斥锁解锁)打开从里边出来,此时外边有两个人在等,当然它们都迫不及待想要进去(尝试对互斥锁进行上锁),自然两个人只能进去一个,进去的人再次把门锁住,另外一个人只能继续等待它出来。

在我们的程序设计当中,只有将所有线程访问共享资源都设计成相同的数据访问规则,互斥锁才能正常工作。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。

互斥锁使用 pthread_mutex_t 数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作。

互斥锁初始化

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);

使用该函数需要包含头文件<pthread.h>。

函数参数和返回值含义如下:

mutex:

参数 mutex 是一个 pthread_mutex_t 类型指针,指向需要进行初始化操作的互斥锁对象;

attr:

参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性(在 12.2.6 小计中介绍),若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为默认值,在这种情况下其实就等价于 PTHREAD_MUTEX_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查。

返回值:

成功返回 0;失败将返回一个非 0 的错误码。

<aside> 💡 Tips:注意,当在 Ubuntu 系统下执行"man 3 pthread_mutex_init"命令时提示找不到该函数,并不是 Linux下没有这个函数,而是该函数相关的 man 手册帮助信息没有被安装,这时我们只需执行"sudo apt-get install manpages-posix-dev"安装即可。

</aside>

使用 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);

3.互斥锁加锁和解锁

互斥锁初始化之后,处于一个未锁定状态,调用函数 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);

使用这些函数需要包含头文件<pthread.h>,参数 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 的访问。

//示例代码 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 万次,如下所示:

图 12.2.1 测试结果

可以看到确实得到了我们想看到的正确结果,每次对 g_count 的累加总是能够保持正确,但是在运行程序的过程中,明显会感觉到锁消耗的时间会比较长,这就涉及到性能的问题了,后续会介绍!

pthread_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。

使用示例

对示例代码 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_lock()效果是一样的,大家可以自己测试。

4. 销毁互斥锁

当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy()函数来销毁互斥锁,其函数原型如下所示:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

使用该函数需要包含头文件<pthread.h>,参数 mutex 指向目标互斥锁;同样在调用成功情况下返回 0,失败返回一个非 0 值的错误码。

  • 不能销毁还没有解锁的互斥锁,否则将会出现错误;
  • 没有初始化的互斥锁也不能销毁。

被 pthread_mutex_destroy()销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用pthread_mutex_init()对互斥锁进行初始化之后才能使用。

使用示例

对示例代码 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);
}

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),那么该线程释放所有互斥锁,可以经过一段时间之后从头再试。与第一种按照层级关系来避免死锁的方法变比,这种方法效率要低一些,因为可能需要经历多次循环。

解决互斥锁死锁的问题还有很多方法,笔者也没详细地去学习过,当大家在实际编程应用中需要用到这些知识再去查阅相关资料、书籍进行学习。

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 中类型:

  • **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);

使用这些函数需要包含头文件<pthread.h>,参数 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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dola_Pan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值