互斥锁
使用互斥锁实现线程同步时,系统会为共享资源添加一个称为互斥锁的标记,防止多个线程在同一时刻访问相同的共用资源。互斥锁通常也被称为互斥量(mutex),它相当于一把锁,使用互斥锁可以保证以下3点:
(1)原子性:如果在一个线程设置了一个互斥锁,那么在加锁与解锁之间的操作会被锁定为一个原子操作,这些操作要么全部完成,要么一个也不执行;
(2)唯一性:如果为一个线程锁定了一个互斥锁,在解除锁定之前,没有其它线程可以锁定这个互斥量;
(3)非繁忙等待:如果一个线程已经锁定了一个互斥锁,此后第二个线程试图锁定该互斥锁,则第二个线程会被挂起;直到第一个线程解除对互斥锁的锁定时,第二个线程才会被唤醒,同时锁定这个互斥锁。
使用互斥锁实现线程同步时主要包含四步:初始化互斥锁、加锁、解锁、销毁锁,Linux系统中提供了一组与互斥锁相关的系统调用,分别为:pthread_mutex_init()、pthread_mutex_lock()、pthread_mutex_unlock()和pthread_mutxe_destroy(),这四个系统调用存在于函数库pthread.h中,下面分别对这四个接口进行讲解。
① pthread_mutex_init()
pthread_mutex_init()函数的功能为初始化互斥锁,该函数的声明如下:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_init()函数中参数mutex为一个pthread_mutex_t*类型的传出参数,关于该参数有以下几个要点:
● pthread_mutext_t类型的本质是结构体,为简化理解,读者可将其视为整型;
● pthread_mutex_t类型的变量mutex只有两种取值:0和1,加锁操作可视为mutex-1;解锁操作可视为mutex+1;
● 参数mutex之前的restrict是一个关键字,该关键字用于限制指针,其功能为告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。
函数中第二个参数attr同样是一个传入参数,代表互斥量的属性,通常传NULL,表示使用默认属性。
若函数pthread_mutex_init()调用成功则返回0,否则返回errno,errno的常见取值为EAGAIN和EDEADLK,其中EAGAIN表示超出互斥锁递归锁定的最大次数,因此无法获取该互斥锁;EDEADLK表示当前线程已有互斥锁,二次加锁失败。
通过pthread_mutex_init()函数初始化互斥量又称为动态初始化,一般用于初始化局部变量,示例如下:
pthread_mutex_init(&mutex, NULL);
此外互斥锁也可以直接使用宏进行初始化,示例如下:
pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
此条语句与以上动态初始化示例语句功能相同。
② pthread_mutex_lock()
当在线程中调用pthread_mutex_lock()函数时,该线程将会锁定指定互斥量。pthread_mutext_lock()函数的声明如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);
该函数中只有一个参数mutex,表示待锁定的互斥量。程序中调用该函数后,直至程序中调用pthread_mutex_unlock()函数之前,此间的代码均被上锁,即在同一时刻只能被一个线程执行。若函数pthread_mutex_lock()调用成功则返回0,否则返回errno。
若需要使用的互斥锁正在被使用,调用pthread_mutxe_lock()函数的线程会进入阻塞,但有些情况下,我们希望线程可以先去执行其它功能,此时需要使用非阻塞的互斥锁。Linux系统中提供了pthread_mutex_trylock()函数,该函数的功能为尝试加锁,若锁正在被使用,不阻塞等待,而是直接返回并返回错误号。pthread_mutext_trylock()函数的声明如下:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
该函数中的参数mutex同样表示待锁定的互斥量;若函数调用成功则返回0,否则返回errno,其中常见的errno有两个,分别为EBUSY和EAGAIN,它们代表的含义如下:
● EBUSY:参数mutex指向的互斥锁已锁定;
● EAGAIN:超过互斥锁递归锁定的最大次数。
③ pthread_mutex_unlock()
当在线程中调用pthread_mutex_unlock()函数时,该线程将会为指定互斥量解锁。pthread_mutext_unlock()函数的声明如下:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
函数中的参数mutex表示待解锁的互斥量。若函数pthread_mutex_lock()调用成功则返回0,否则返回errno。
④ pthread_mutex_destroy()
互斥锁也是系统中的一种资源,因此使用完毕后应将其释放。当在线程中调用pthread_mutex_destroy()函数时,该线程将会为指定互斥量解锁。pthread_mutext_destroy()函数的声明如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
函数中的参数mutex表示待销毁的互斥量。若函数pthread_mutex_lock()调用成功则返回0,否则返回errno。
下面通过一个案例,来展示互斥锁在程序中的用法及功能。
案例9:在主线程和子线程中分别进行打印操作,使主线程分别打印“HELLO”、“ WORLD”,子线程分别打印“hello”、“world”。
为使读者能更为直观地感受互斥锁的功能,本案例中使用两段代码,分别展示未使用互斥锁的程序与使用了互斥锁的程序的执行结果。案例实现如下:
pthread_share.c //未添加mutex
1 #include <stdio.h>
2 #include <pthread.h>
3 #include <unistd.h>
4 void *tfn(void *arg)
5 {
6 srand(time(NULL));
7 while (1) {
8 printf("hello ");
9 //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
10 sleep(rand() % 3);
11 printf("world\n");
12 sleep(rand() % 3);
13 }
14 return NULL;
15 }
16 int main(void)
17 {
18 pthread_t tid;
19 srand(time(NULL));
20 pthread_create(&tid, NULL, tfn, NULL);
21 while (1) {
22 printf("HELLO ");
23 sleep(rand() % 3);
24 printf("WORLD\n");
25 sleep(rand() % 3);
26 }
27 pthread_join(tid, NULL);
28 return 0;
29 }
此段程序为未添加互斥量的程序,编译pthread_share.c文件,执行程序,执行结果如下:
HELLO hello WORLD
HELLO world
hello world
hello WORLD
world
观察以上执行结果,可知主线程与子线程中的字符串未能成对打印。
在以上程序中添加互斥量,进行线程同步,程序实现如下:
pthread_mutex.c //添加mutex
1 #include <stdio.h>
2 #include <string.h>
3 #include <pthread.h>
4 #include <stdlib.h>
5 #include <unistd.h>
6 pthread_mutex_t m; //定义互斥锁
7 void err_thread(int ret, char *str)
8 {
9 if (ret != 0) {
10 fprintf(stderr, "%s:%s\n", str, strerror(ret));
11 pthread_exit(NULL);
12 }
13 }
14 void *tfn(void *arg)
15 {
16 srand(time(NULL));
17 while (1) {
18 pthread_mutex_lock(&m); //加锁:m--
19 printf("hello ");
20 //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
21 sleep(rand() % 3);
22 printf("world\n");
23 pthread_mutex_unlock(&m); //解锁:m++
24 sleep(rand() % 3);
25 }
26 return NULL;
27 }
28 int main(void)
29 {
30 pthread_t tid;
31 srand(time(NULL));
32 int flag = 5;
33 pthread_mutex_init(&m, NULL); //初始化mutex:m=1
34 int ret = pthread_create(&tid, NULL, tfn, NULL);
35 err_thread(ret, "pthread_create error");
36 while (flag--) {
37 pthread_mutex_lock(&m); //加锁:m--
38 printf("HELLO ");
39 sleep(rand() % 3);
40 printf("WORLD\n");
41 pthread_mutex_unlock(&m); //解锁:m--
42 sleep(rand() % 3);
43 }
44 pthread_cancel(tid);
45 pthread_join(tid, NULL);
46 pthread_mutex_destroy(&m);
47 return 0;
48 }
在pthread_mutex.c中,终端即为共享资源,主线程和子线程在临界区代码中都需要向终端中打印数据,为了使两个线程输出的字符串能够匹配,互斥锁将程序中两次访问终端的一段代码绑定为原子操作,因此在获取互斥锁的线程完成两次打印操作前,其它线程无法获取终端。编译pthread_mutex.c,执行程序,执行结果如下:
HELLO WORLD
HELLO WORLD
hello world
HELLO WORLD
HELLO WORLD
观察执行结果,主线程与子线程中的字符串成对输出,可知线程加锁成功。