多线程环境中的共享变量怎么保护起来的(volatile关键字与互斥锁)

news/2024/10/18 9:22:58/

文章目录

    • 一、volatile关键字与互斥锁介绍
      • (1)volatile关键字
      • (2)互斥锁
    • 二、volatile关键字与互斥锁的作用
      • (1)第一个代码实例
      • (2)第二个代码实例
      • (3)第三个代码实例
    • 三、扩展(原子操作)


一、volatile关键字与互斥锁介绍

(1)volatile关键字

在C语言中,使用volatile关键字可以告诉编译器某个变量是易变的,需要每次从内存中读取或写入,而不是对变量进行优化缓存。因为编译器会对变量进行各种优化,比如寄存器优化、指令重排等等,这些优化可能会导致变量的读写顺序出现问题,从而导致程序错误。

在多线程编程中,如果一个变量被多个线程同时访问和修改,那么就会出现竞态条件问题,为了避免这种问题的发生,需要使用同步机制来保护共享变量。但是,同步机制仅能确保互斥访问,不能确保变量访问操作的顺序性和完整性。而使用volatile可以告诉编译器,这个变量可能会被其他线程修改,不要优化掉它的读写指令,必须从内存中读取它的值,并把它写回内存。

(2)互斥锁

互斥锁是一种用于同步多线程、避免竞态条件(Race Condition)问题的机制。在多线程环境下,当多个线程同时访问共享资源时,可能会出现读写冲突的情况,从而导致数据不一致或程序崩溃。

为了避免这种情况发生,我们需要使用互斥锁来对共享资源进行保护。当一个线程获取到互斥锁后,其他线程就不能再获取该锁,只能等待当前线程释放锁后才能重新竞争获取锁。

互斥锁的基本操作包括加锁和解锁。当一个线程想要访问共享资源时,它必须先尝试获取互斥锁,如果锁已经被其他线程持有,则当前线程会一直阻塞,直到获取到锁为止。当线程访问完共享资源后,需要释放锁,以便其他线程可以继续访问资源。

在实际编程中,互斥锁一般是由操作系统提供的,我们可以通过系统调用来使用互斥锁。通常还会有一些高级的同步机制,如读写锁、信号量等,它们都是基于互斥锁实现的。

需要注意的是,使用互斥锁不是万能的解决方案,它可能会带来一些额外的开销和问题,比如死锁、优先级反转等。因此,在使用互斥锁时,需要根据实际情况进行权衡,并考虑其他同步机制的可能性。


二、volatile关键字与互斥锁的作用

三个例子告诉你他们的作用。

(1)第一个代码实例

#include <stdio.h>
#include <pthread.h>int counter = 0;  // 声明为volatile类型的共享变量void *thread_func(void *arg) 
{int i;for (i = 0; i < 100000; i++) {counter++;  // 对共享变量进行加1操作}pthread_exit(NULL);  // 终止线程
}int main(int argc, char * argv[]) 
{pthread_t tid1, tid2;pthread_create(&tid1, NULL, thread_func, NULL);  // 创建线程1pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2pthread_join(tid1, NULL);  // 等待线程1结束pthread_join(tid2, NULL);  // 等待线程2结束printf("counter = %d\n", counter);  // 输出最终计数器的值return 0;
}

结果:

wangdengtao@wangdengtao-virtual-machine:~/c_test$ gcc a.c
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 111741
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 106999
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 117765
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 108233

在上述代码中,我们没有使用互斥锁或其他同步机制来保护共享变量counter,因此会出现竞态条件的问题。两个线程将同时对计数器进行加1操作,由于两个线程的执行顺序和时间都不确定,它们有可能会在相同的时刻读取并修改同一个变量,从而导致不可预测的结果发生。

例如,假设线程1和线程2同时读取了计数器的值为100,然后各自加1并将结果写回,那么最终的计数器值应该是102,但是由于两个线程的运行顺序不确定,可能先执行线程1,也可能先执行线程2,因此最终计数器的值可能是101或者更小的值,而不是102。

(2)第二个代码实例

我们在第一个代码的例子上添加上互斥锁:

#include <stdio.h>
#include <pthread.h>int counter = 0;  // 声明为volatile类型的共享变量pthread_mutex_t mutex;  // 声明互斥锁void *thread_func(void *arg) {int i;for (i = 0; i < 100000; i++) {pthread_mutex_lock(&mutex);  // 获取互斥锁counter++;  // 对共享变量进行加1操作pthread_mutex_unlock(&mutex);  // 释放互斥锁}pthread_exit(NULL);  // 终止线程
}int main(int argc, char * argv[]) {pthread_t tid1, tid2;pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁pthread_create(&tid1, NULL, thread_func, NULL);  // 创建线程1pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2pthread_join(tid1, NULL);  // 等待线程1结束pthread_join(tid2, NULL);  // 等待线程2结束printf("counter = %d\n", counter);  // 输出最终计数器的值return 0;
}

结果:

wangdengtao@wangdengtao-virtual-machine:~/c_test$ gcc a.c
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000

在上述代码中,我们使用了互斥锁来保护共享变量counter,从而避免了竞态条件的问题。每个线程在修改计数器之前都会先获取互斥锁,因此只有一个线程能够进入临界区,保证了计数器的互斥访问。

具体来说,当一个线程调用pthread_mutex_lock()函数获取互斥锁时,如果其他线程正在使用这个锁,那么该线程将被阻塞,直到该锁被释放为止。这样就可以确保每次只有一个线程能够访问临界区,避免了对共享变量的同时访问。

另外,由于多个线程共享同一个内存空间,因此修改共享变量时需要考虑内存缓存一致性的问题。在上述代码中,使用了互斥锁来同步线程,以确保每个线程都能及时地读取到其他线程对共享变量所做的修改,从而避免了共享变量的数据不一致问题。

所以我们的结果是正确的。

(3)第三个代码实例

我们在上述代码中添加上volatile关键字:

#include <stdio.h>
#include <pthread.h>volatile int counter = 0;  // 声明为volatile类型的共享变量pthread_mutex_t mutex;  // 声明互斥锁void *thread_func(void *arg) {int i;for (i = 0; i < 100000; i++) {pthread_mutex_lock(&mutex);  // 获取互斥锁counter++;  // 对共享变量进行加1操作pthread_mutex_unlock(&mutex);  // 释放互斥锁}pthread_exit(NULL);  // 终止线程
}int main() {pthread_t tid1, tid2;pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁pthread_create(&tid1, NULL, thread_func, NULL);  // 创建线程1pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2pthread_join(tid1, NULL);  // 等待线程1结束pthread_join(tid2, NULL);  // 等待线程2结束printf("counter = %d\n", counter);  // 输出最终计数器的值return 0;
}

结果:

wangdengtao@wangdengtao-virtual-machine:~/c_test$ gcc a.c
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000

运行的结果依旧是正确的,我们为什么要添加volatile关键字呢?有的人可能会想:加互斥锁就可以了为什么还需要声明volatile类型的共享变量才行?

在多线程环境下,如果一个变量被多个线程访问和修改,那么在没有同步机制的情况下会出现竞态条件问题。为了避免竞态条件问题,我们需要使用同步机制来保护共享变量。

互斥锁是一种常用的同步机制,可以确保同时只有一个线程可以访问和修改共享变量,从而避免了竞态条件问题。因此,在上述示例代码中添加了互斥锁之后,可以保证多个线程对计数器变量的访问和修改是安全和正确的。

但是,即使使用了互斥锁,依然需要将共享变量声明为volatile类型的变量。这是因为,在多线程程序中,除了访问和修改共享变量之外,还存在其他的操作,例如对变量地址的读取和写入操作。如果没有将共享变量声明为volatile类型的变量,则==编译器可能会对程序进行优化,将变量缓存到寄存器或高速缓存中,而不是每次从内存中读取变量。这样就可能会出现一个线程读取到另一个线程修改后的过期数据,从而导致程序错误。==而将变量声明为volatile类型的变量,可以告诉编译器不要对该变量进行优化,必须在每次读取和写入变量时都从内存中读取和写入。

因此,为了确保多线程程序的正确性,我们需要同时使用互斥锁和将共享变量声明为volatile类型的变量。这样才能确保所有操作都是同步的,并且不会出现数据过期的问题。


三、扩展(原子操作)

使用原子操作来解决多线程的数据竞争问题:

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>atomic_int counter = ATOMIC_VAR_INIT(0);  // 原子变量void *thread_func(void *arg) {int i;for (i = 0; i < 100000; i++) {atomic_fetch_add(&counter, 1);  // 对原子变量进行加1操作}pthread_exit(NULL);  // 终止线程
}int main(int argc, char * argv[]) {pthread_t tid1, tid2;pthread_create(&tid1, NULL, thread_func, NULL);  // 创建线程1pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2pthread_join(tid1, NULL);  // 等待线程1结束pthread_join(tid2, NULL);  // 等待线程2结束printf("counter = %d\n", counter);  // 输出最终计数器的值return 0;
}

结果:

wangdengtao@wangdengtao-virtual-machine:~/c_test$ gcc b.c
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000
wangdengtao@wangdengtao-virtual-machine:~/c_test$ ./a.out 
counter = 200000

在使用原子变量时,不需要使用volatile关键字来修饰变量。因为原子变量本身已经在语言标准中定义了内存顺序,对原子变量的操作会自动同步到内存中,确保多线程程序的正确性。

事实上,volatile和原子变量是两种不同的机制,其作用也不同。volatile关键字只是告诉编译器不要对变量进行优化,而原子变量则可以保证多线程访问的同步性

使用volatile关键字修饰共享变量时,仍然需要使用其他机制来保证多线程程序的正确性,比如使用互斥锁。虽然volatile关键字可以防止编译器对变量进行优化,但是它并不能保证多线程访问的同步性,因此并不能彻底解决多线程程序中的数据竞争问题。



http://www.ppmy.cn/news/103965.html

相关文章

TDengine 数据库SQL操作 | 建库、建表、数据读写

一、前文 TDengine 入门教程——导读 二、库操作 2.1 创建库 CREATE DATABASE test; BUFFER: 一个 VNODE 写入内存池大小&#xff0c;单位为 MB&#xff0c;默认为 96&#xff0c;最小为 3&#xff0c;最大为 16384。CACHEMODEL&#xff1a;表示是否在内存中缓存子表的最近数据…

【CTF】2023Ciscn WEB方向题解

前言 太菜了太菜了&#xff0c;太久没打比赛啥也不会做&#xff0c;部分题目可去NSSCTF进行复现:NSSCTF 比赛体验一般&#xff0c;一黑灯基本上题都烂掉 unzip 这道题估计大家都会&#xff0c;算是一道原题了 参考:https://xz.aliyun.com/t/10533 由于环境没了&#xff0c;靠…

javascript获取对象的键名列表、键值列表

Object.keys&#xff1a;获取对象的键名列表 Object.values&#xff1a;获取对象的键值列表 <script>var obj {name: 1,age: 2,order: 3}const klist Object.keys(obj)const vals Object.values(obj)console.log(obj, obj)console.log(键名列表, klist)console.log(键…

三十五、数学知识——快速幂(反复平方法 + 快速幂求逆元)

快速幂算法主要内容 一、基本原理1、概念 暴力求解2、核心原理——反复平方法3、快速幂求逆元 二、Java、C语言模板实现三、例题题解 一、基本原理 1、概念 暴力求解 问题目标&#xff1a; 快速求出 a^k mod p 的结果&#xff0c;时间复杂度为 O(logk)&#xff0c;其中 a,p…

【P43】JMeter 吞吐量控制器(Throughput Controller)

文章目录 一、吞吐量控制器&#xff08;Throughput Controller&#xff09;参数说明二、测试计划设计2.1、Total Executions2.2、Percent Executions2.3、Per User 一、吞吐量控制器&#xff08;Throughput Controller&#xff09;参数说明 允许用户控制后代元素的执行的次数。…

干货 | 出国留学申请必备的6种材料,速来!!!

Hello,大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐~ 我们又见面啦~你还好吗&#xff1f; 这是喵君姐姐的第n篇诚意推送~ 01 为什么要留学&#xff1f; 想去看外面的世界&#xff1f;想要打破科研的壁垒&#xff1f;想去更好的平台提升自己&#xff1f…

WMS:系统窗口添加过程

WMS:系统窗口添加过程 1、经常使用的两大类窗口2、系统窗口StatusBar2.1 StatusBarWindowView添加流程2.2 简要时序图 android12-release 1、经常使用的两大类窗口 Android中的“窗口”类型有很多&#xff0c;经常使用的“窗口”大致分为两大类&#xff1a;一是&#xff0c;由系…

cubemx stm32 pca9685pw模块 16路PWM 可用于舵机驱动 驱动代码

资料 淘宝链接请点这里 淘宝资料资料&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1Kda-c7QdZdQ03FBMa0zeRA 提取码&#xff1a;1234 pca9685pw介绍 这个模块是 I2C 通信控制 16 路 PWM 的模块。 所有路的 频率 是统一设置的&#xff0c;所以每一路的频率都一样&a…