1. 互斥锁的加锁和解锁
1.1 加锁解锁说明
在处理线程同步
时,第一种方式就是使用互斥锁
。互斥锁只能同时被一个线程使用
,锁的所有权只能被一个线程拥有。互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块
,被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
在 Linux 中互斥锁的类型为 pthread_mutex_t
,创建一个这种类型的变量就得到了一把互斥锁:
pthread_mutex_t mutex;
在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开
,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞
,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁
,锁的个数和线程的个数无关。
Linux 提供的互斥锁操作函数如下,如果函数调用成功会返回 0,调用失败会返回相应的错误号:
// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex:
互斥锁变量的地址attr:
互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULLrestrict:
是一个关键字, 用来修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的。比如说将p=mux
,虽然将互斥锁变量赋值给了p
,但p不能访问该互斥锁指向的内存地址。
// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);
这个函数被调用,首先会判断参数 mutex
互斥锁中的状态是不是锁定状态:
- 没有被锁定,是打开的,这个线程可以加锁成功,这个这个锁中会记录是哪个线程加锁成功了
- 如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上
- 当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞
// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。
互斥锁加锁和解锁操作,需要成对出现
。从互斥锁加锁到解锁包括的区域称为临界区
,如何确定临界区的范围,首先需要找到共享资源(多线程同时读写的变量),然后将共享资源上下文中涉及到处理共享资源的代码,作为临界区
。 临界区内的代码是串行执行的,虽然牺牲了执行效率,但可以确保访问资源的安全。
// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_trylock
和pthread_mutex_lock
,都可以对互斥锁加锁,主要区别是
:如果互斥锁没有被锁住的情况下,这两种操作都可以对互斥锁加锁,但是如果这把互斥锁已经锁上了,此时线程调用pthread_mutex_lock
就堵塞到这把互斥锁了。而线程调用pthread_mutex_trylock
,表示尝试给这把互斥锁加锁,如果加锁失败了的话,还可以继续干一会其他事情,然后再重新尝试给互斥锁加锁
。总之:pthread_mutex_lock
在互斥锁已经加锁的时候,线程会堵塞在那,然后一直死等,等待重新尝试上锁,而pthread_mutex_trylock
则会比较灵活,它发现无法加锁,拿不到cpu时间片,就回去干些其他事情,事情干完之后,再回来尝试对互斥锁进行重新解锁。
这些互斥锁的加锁和解锁操作,都有一个参数pthread_mutex_t *mutex
, 这个对象时通过mutex进行pthread_mutex_init
初始化得到的,因此在做线程同步的时候,一定要先通过pthead_mutex_t
创建 一个互斥锁mutex,然后调用pthread_mutex_init
对创建的互斥锁进行初始化,并且在做线程锁同步期间,这个锁的资源是不能被释放的
。
1.2 互斥锁使用
我们可以将:多线程(2):线程同步中多线程交替数数的例子修改一下,使用互斥锁进行线程同步。两个线程一共操作了同一个全局变量,因此需要添加一互斥锁,来控制这两个线程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#define MAX 100
// 全局变量
int number;
// 创建一把互斥锁
// 全局变量, 多个线程共享
pthread_mutex_t mutex;
// 线程处理函数
void* funcA_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// 如果线程A加锁成功, 不阻塞
// 如果B加锁成功, 线程A阻塞
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
usleep(10);
number = cur;
pthread_mutex_unlock(&mutex);
printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
}
return NULL;
}
void* funcB_num(void* arg)
{
for(int i=0; i<MAX; ++i)
{
// a加锁成功, b线程访问这把锁的时候是锁定的
// 线程B先阻塞, a线程解锁之后阻塞解除
// 线程B加锁成功了
pthread_mutex_lock(&mutex);
int cur = number;
cur++;
number = cur;
pthread_mutex_unlock(&mutex);
printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
usleep(5);
}
return NULL;
}
int main(int argc, const char* argv[])
{
pthread_t p1, p2;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建两个子线程
pthread_create(&p1, NULL, funcA_num, NULL);
pthread_create(&p2, NULL, funcB_num, NULL);
// 阻塞,资源回收
pthread_join(p1, NULL);
pthread_join(p2, NULL);
// 销毁互斥锁
// 线程销毁之后, 再去释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
-
使用互斥锁
pthread_mutex_t
,需要引入头文件<pthread.h>
,为了保证在线程同步期间,互斥锁资源一直存在,因此这里创建了全局变量的互斥锁。 -
在
数数
的过程中,很明显共享资源/临界资源
就是变量number
, 在做线程同步的时候,我们需要确定的是我们要找到的临界区范围肯定是越小越好
,因为临界区内只允许一个线程同时执行,所以多线程是串行执行的,执行效率比较低,所以我们要缩小代码块的范围。对于funcA_num
这个任务函数,可以看到for循环内的代码都很临界资源number有关系,其中printf这句不加也可以。因此在临界区开始位置加锁操作pthread_mutex_lock(&mutex)
,在临界区下面加锁解锁操作pthread_mutex_unlock(&mutex)
。
-
同理对于
funcB_num
这个任务函数,它的临界区也是操作共享资源number
这部分代码块,其中usleep由于不涉及操作共享资源,因此临界区可以不包括usleep:
-
接下来分析下这个多线程的执行过程,
funcA_um
和funcB_num
这两个任务函数分别是由两个线程p1
和p2
来执行的。假设线程p1先抢到cpu的时间片,然后向下执行,然后执行到pthread_mutex_lock
加锁成功,继续向下执行到usleep
,usleep使得线程强制放弃了cpu时间片。休眠的过程中,线程p2
抢到了cpu时间片
, 执行任务函数funcB_num
,执行到pthread_mutex_lock(&mutex)
,但是这个互斥锁mutex
已经被线程p1
给锁定了,线程p2
就被阻塞了,只能在那死等,在阻塞期间它就放弃了cpu资源。线程p1睡醒了之后,又抢到了cpu时间片,继续从原来的位置向下执行,执行完成之后,通过pthread_mutex_unlock(&mutex)把互斥锁mutex解锁,解锁后线程p2
就被解除堵塞了,解除堵塞后,线程p2
马上就抢到了cpu时间片,然后通过pthread_mutex_lock(&mutex)
把锁重新锁上。然后执行funcB_num
中数数的代码,在数数
期间,线程p1
可能又抢到时间片,但它不能执行funcA_um
中数数的代码,因为锁已经让线程p2
锁上了,线程p1
只能堵塞在这把互斥锁上。 -
从前面的分析可以知道,线程p1和线程p2在访问共享资源
number
时是线性执行(顺序或串行),而不是并行执行。既然是顺序执行,就不存在同时访问共享资源的情况,那么数据就不会发生错落了。
另外互斥锁mutex变量需要在线程调用前进行初始化,不然的话会导致线程在使用互斥锁出现问题。
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
- 当线程做完同步,并且线程资源已经被回收了,此时互斥锁就没有存在的意义了,就需要将互斥锁的资源释放掉。
// 销毁互斥锁
// 线程销毁之后, 再去释放互斥锁
pthread_mutex_destroy(&mutex);
- 最后执行编译,两个线程同时数数,每个线程各数50次,可以正常数到100,从打印的结果可以看出,两个线程并不是交替执行,同一个线程可能连续数3次,可能间隔一段时间才数数,这是因为线程抢到cpu资源是随机的。