简 述: 在 Linux 中,使用互斥量(互斥锁🔐) Mutex 来给保证多线程 ,在访问公共变量的时候能够 “串行” 代码。从而使得多线程正确的同步执行。关于多线程创建和使用可以参考前面几篇的文章,争取早日把 Linux 系统篇之 系统编程给发布完系列的教程。
PS:好几天没有接着学习 Linux 的系统函数和理论知识了。可能是前面几天有点忙了吧;时间流逝啊,总是这么得快,,,,
关于互斥锁的使用如下:
- pthread_mutex_t *mutex; //创建一个锁
- pthread_mutex_init(); //初始化一个互斥锁
- pthread_mutex_lock(); //上锁🔓,或者使用 pthread_mutex_trylock()
- pthread_mutex_unlock(); //解锁🔐
- pthread_mutex_destroy; //销毁互斥锁
[TOC]
本文初发于 “偕臧的小站“,同步转载于此。
编程环境:
💻: uos20
📎 gcc/g++ 8.3
📎 gdb8.0
💻: MacOS 10.14
📎 gcc/g++ 9.2
📎 gdb8.3
背景铺垫:
程序:
先写一个多线程程序,A 和 B 两个子线程一起数出输出到 100000;然后在没有锁🔐的情况下,每一线程轮流执行 10 毫秒,模拟时间片轮转切片。
代码程序:
这个代码还没有加锁,保护共享变量。完整代码如下;
#include <stdio.h> #include <unistd.h> #include <pthread.h> int g_num = 0; //在全局区域,共享 #define MAXNUMBER 100000 void* funA(void* arg); void* funB(void* arg); int main(int argc, char *argv[]) { pthread_t pthreadA = 0; pthread_t pthreadB = 0; pthread_create(&pthreadA, nullptr, funA, nullptr); //创建两个子线程 pthread_create(&pthreadB, nullptr, funB, nullptr); pthread_join(pthreadA, nullptr); //阻塞,回收资源 pthread_join(pthreadB, nullptr); return 0; } void* funA(void* arg) { for (int i = 0; g_num < MAXNUMBER; i++) { int a = g_num; //27-29行,增加寄存器和内存之间的数据交换操作,使得出现内存没来得及保存数据的现象的概率大一点 a++; g_num = a; printf("A thread id: %ld, num = %d\n", pthread_self(), g_num); usleep(10); //沉睡 10 毫秒, 模拟时间片轮转,效果更明显 } return nullptr; } void* funB(void* arg) { for (int i = 0; g_num < MAXNUMBER; i++) { int b = g_num; b++; g_num = b; printf("B thread id: %ld, num = %d\n", pthread_self(), g_num); usleep(10); //沉睡 10 毫秒, 模拟时间片轮转,效果更明显 } return nullptr; }
结果分析:
下面截图里面,在 MacOS 10.14.6 和 Ubuntu 20.04 里面跑,都是正确的结果;
[疑惑]可能是因为系统系统内核或者底层作了修改???又或者是数数的间隔过小,数的数字不多? 预判的结果是 实际数数的大小会比实际正确结果要小一点(原因是时间片轮转的原因),待日后挖坟考古?难道是系统比较新?所以连续出现几次都是最正确的结果。。。
[解答]没有出现内存来不及保存增加的数据,cpu 的时间片就被其他资源占去了,导致最后出现的小数有可能出现在大数的后面打印,这是一个概率问题;而 27-29 行和 42-44 行,是刻意增加寄存器和内存之间的数据交换操作次数,使得出现内存没来得及保存数据的现象的概率大一点。
如果在 Ubuntu 16.04 里面跑改程序,如果出现了最后一个数字比 100000 小一点,那么解释的可能如下:
其中会发现结果有点混乱。这是两个子线程互相操作同一个数据,时间片切换的时候,还新的数据还没有来得及保存进内存里面,就切换到了下一个线程中去了。
- [实际结果]多数了一位,最后一个结果是100001,且有时候还会下面还会再打印一个小的数字
- [期望结果]只数数到 100000;
- 分析图如下:
使用互斥量(锁) Mutex:
如果我们想使用互斥锁同步线程,那么所有线程,只要是有访问同一个共享公共变量的线程都要加锁。
- 使用流程步骤:
创建一把互斥锁
pthread_mutex_t *mutex
对互斥锁进程初始化 init
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
使用互斥锁:每个子线程要访问公共变量的时候,都加上锁,使用完后就解锁
//若果没有上锁,就上锁;若果已经上锁,直接返回,不阻塞 int pthread_mutex_trylock(pthread_mutex_t *mutex); //若果没有上锁,就上锁;若果已经上锁,就阻塞该线程 int pthread_mutex_lock(pthread_mutex_t *mutex); //解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用完毕,销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
改写例子,使用互斥量(锁)实例:
将上面的例子改了改,然后在 A、B 两个子线程里面,使用互斥量(锁),会发现也会得到取其正确的结果。结果符合预期。
- 修改后的加锁例子:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int g_num = 0; //在全局区域,共享
#define MAXNUMBER 100000
pthread_mutex_t g_mutex; //创建全局的互斥锁
void* funA(void* arg);
void* funB(void* arg);
int main(int argc, char *argv[])
{
pthread_mutex_init(&g_mutex, nullptr); //对锁进行初始化
pthread_t pthreadA = 0;
pthread_t pthreadB = 0;
pthread_create(&pthreadA, nullptr, funA, nullptr); //创建两个子线程
pthread_create(&pthreadB, nullptr, funB, nullptr);
pthread_join(pthreadA, nullptr); //阻塞,回收资源
pthread_join(pthreadB, nullptr);
pthread_mutex_destroy(&g_mutex); //释放互斥锁资源
return 0;
}
void* funA(void* arg)
{
for (int i = 0; g_num < MAXNUMBER; i++) {
pthread_mutex_lock(&g_mutex);
int a = g_num;
a++;
g_num = a;
printf("A thread id: %ld, num = %d\n", pthread_self(), g_num);
pthread_mutex_unlock(&g_mutex);
usleep(10); //沉睡 10 毫秒, 模拟时间片轮转,效果更明显
}
return nullptr;
}
void* funB(void* arg)
{
for (int i = 0; g_num < MAXNUMBER; i++) {
pthread_mutex_lock(&g_mutex);
int b = g_num;
b++;
g_num = b;
printf("B thread id: %ld, num = %d\n", pthread_self(), g_num);
pthread_mutex_unlock(&g_mutex);
usleep(10); //沉睡 10 毫秒, 模拟时间片轮转,效果更明显
}
return nullptr;
}
运行结果:
实际结果和预期结果一致。
下载地址:
欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。