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

文章介绍了volatile关键字和互斥锁在C语言多线程环境中的作用,volatile用于防止编译器优化,确保共享变量的可见性,而互斥锁提供了一种同步机制,确保对共享资源的互斥访问。通过代码实例展示了在没有同步机制、使用互斥锁以及结合volatile使用时的不同效果,强调了在多线程编程中同步的重要性。最后提到了原子操作作为另一种解决数据竞争的手段。
摘要由CSDN通过智能技术生成


一、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);  // 创建线程1
    pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2

    pthread_join(tid1, NULL);  // 等待线程1结束
    pthread_join(tid2, NULL);  // 等待线程2结束

    printf("counter = %d\n", counter);  // 输出最终计数器的值

    return 0;
}

结果:
在这里插入图片描述

在上述代码中,我们没有使用互斥锁或其他同步机制来保护共享变量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);  // 创建线程1
    pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2

    pthread_join(tid1, NULL);  // 等待线程1结束
    pthread_join(tid2, NULL);  // 等待线程2结束

    printf("counter = %d\n", counter);  // 输出最终计数器的值

    return 0;
}

结果:
在这里插入图片描述

在上述代码中,我们使用了互斥锁来保护共享变量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);  // 创建线程1
    pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2

    pthread_join(tid1, NULL);  // 等待线程1结束
    pthread_join(tid2, NULL);  // 等待线程2结束

    printf("counter = %d\n", counter);  // 输出最终计数器的值

    return 0;
}

结果:
在这里插入图片描述

运行的结果依旧是正确的,我们为什么要添加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);  // 创建线程1
    pthread_create(&tid2, NULL, thread_func, NULL);  // 创建线程2

    pthread_join(tid1, NULL);  // 等待线程1结束
    pthread_join(tid2, NULL);  // 等待线程2结束

    printf("counter = %d\n", counter);  // 输出最终计数器的值

    return 0;
}

结果:
在这里插入图片描述

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

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

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


  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值