学科分类
目录
Linux编程

互斥锁

使用互斥锁实现线程同步时,系统会为共享资源添加一个称为互斥锁的标记,防止多个线程在同一时刻访问相同的共用资源。互斥锁通常也被称为互斥量(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

观察执行结果,主线程与子线程中的字符串成对输出,可知线程加锁成功。

点击此处
隐藏目录