互斥锁(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);
函数参数和返回值含义如下:
mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向需要进行初始化操作的互斥锁对象;
attr:参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性,若将参数 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);
互斥锁加锁和解锁
互斥锁初始化之后,处于一个未锁定状态,调用函数 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()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会如愿以偿!
使用示例
使用互斥锁的方式将线程保护(1)示例代码进行修改,使用了一个互斥锁来保护对全局变量 g_count 的访问。
#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 的累加总是能够保持正确,但是在运行程序的过程中,明显会感觉到锁消耗的时间会比较长,这就涉及到性能的问题了,后续会介绍。