01Linux下C语言锁的学习之Linux下的互斥锁
概述:
为什么需要锁,因为当我们多个线程访问全局变量时,同时操作的话可能存在问题,例如小明和小红同时去拿家里的银行账号里的钱,小明看到时为100块,小红也是,但是小明先拿到,若不加锁,那么小红再拿100的时候就会报错。所以必须在小明操作的时候限制小红操作。
不考虑相应初始化下,实际上所有的锁的使用步骤都是上锁,操作,解锁。
锁的粒度:即你上锁和解锁中间的操作,步骤越少越好。
注意:下面的互斥锁和题目一样,只是针对于Linux下使用,Windows下有自己相应的函数和锁,并且有临界点。当然,如果熟悉锁的人一般不使用这种了,例如我C++开发时一般使用guard这种,方便安全。
1 与Linux下的互斥锁相关的函数介绍
1)pthread_mutex_init函数
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
/*
参1:传出参数,调用时应传&mutex。
参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。
1).静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。例如pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
2).动态初始化:局部变量应采用动态初始化。例如pthread_mutex_init(&mutex, NULL)。这是平时常用的方法。调用该初始化函数就是动态初始化。
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
例如:int a=3;
int *restrict p = &a;
int *q=p;
那么
*q=5//error
*/
2)pthread_mutex_destroy函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*
销毁一个互斥锁。
*/
3)pthread_mutex_lock函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
/*
加锁。
若得不到锁,该函数会阻塞,即该函数具有阻塞功能。
*/
4)pthread_mutex_unlock函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/*
解锁。
unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
*/
5)pthread_mutex_trylock函数
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/*
尝试解锁。不会阻塞,直接返回错误号,例如EBUSY,具体查看man手册。
*/
感觉没什么好讲的。下面直接看例子即可,注意看主线程和子线程的上锁和解锁中间的共享资源即可。这里的共享资源是stdout屏幕。
2 代码例子
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
//互斥锁
pthread_mutex_t mutex;
void *tfn(void *arg){
srand(time(NULL));
while (1) {
//上锁
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主执行其它*/
printf("world\n");
sleep(rand() % 3);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void){
pthread_t tid;
srand(time(NULL));//随机种子
//1 初始化
int ret = pthread_mutex_init(&mutex, NULL);
if(ret < 0){
printf("pthread_mutex_init failed.\n");
return -1;
}
pthread_create(&tid, NULL, tfn, NULL);
while (1) {
//2 上锁
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
sleep(rand() % 3);
//3 解锁
pthread_mutex_unlock(&mutex);
}
//等待子进程结束回收资源
pthread_join(tid, NULL);
//4 销毁互斥锁
ret = pthread_mutex_destroy(&mutex);
if(ret < 0){
printf("pthread_mutex_destroy failed.\n");
return -1;
}
return 0;
}
3 互斥锁的两种死锁可能
1)一把锁,一个共享全局变量的情况
看下面代码:
//上锁
pthread_mutex_lock(&mutex);
//pthread_mutex_lock(&mutex);也会造成死锁。
printf("HELLO ");//或者其它函数
//解锁
pthread_mutex_unlock(&mutex);
第一种的死锁又分两种情况:
- 如果当printf()函数出现错误直接返回的话,那么就无法往下执行解锁,其它线程一直阻塞等待,而锁无法得到释放就造成了死锁,谁也获取不了,程序干巴巴的运行着却什么也做不了。
- 如果你在操作里面再一次上锁也会造成死锁,因为自己锁住后又上锁,导致卡死在操作里面。
2)两把锁,两个共享全局变量的情况
例如,线程T1拥有A锁,请求获取B锁;线程T2拥有B锁,请求获取A锁。
看图分析:
上图看到,实际上和第一种的第二个情况类似。
当T1线程拥有A锁后,再去申请B锁,那么T1此时阻塞等待B锁的释放;而T2此时拥有B锁,同样去申请A锁,那么T2此时阻塞等待A锁的释放。这样就导致,T1和T2线程均阻塞并且无法互相释放A和B两把互斥锁,导致死锁。
注意:以T1线程为例,T1线程申请B锁必定是在A上锁的情况下操作,否则就不叫T1线程拥有A锁。T2线程同理。
不过开发时很少用到两种全局共享变量,一个已经足够用了。
4 总结互斥锁
一般这种互斥锁在嵌入式Linux的C下用得比较多,C++开发有自己的锁例如std::lock_guard以及用于高级用例的std::unique_lock。
所以建议C++开发的话不使用这种锁,Linux下C使用这种锁的话必须细心观察,在中间操作的地方防止有可能出现死锁的情况和再加锁。