【C++并发编程】数据竞争及其解决方案

本文介绍了C++中数据竞争的概念,通过示例展示了如何使用互斥锁(包括std::lock_guard和std::unique_lock)来解决线程间的数据冲突。同时,文章提到了原子操作的重要性,以及在多线程环境下如何通过原子操作优化性能。
摘要由CSDN通过智能技术生成

数据竞争

  • 同一进程的多个线程共享地址空间和系统资源,由此可能出现数据竞争的问题。
  • 数据竞争举例
    #include <iostream>
    #include <thread>
    
    int main()
    {
    	while (true) {
    		std::thread t1([]() {
    			std::cout << "1234567890" << "1234567890" << std::endl;
    			});
    
    		std::thread t2([]() {
    			std::cout << "abcdefghijk" << "abcdefghijk" << std::endl;
    			});
    
    		t1.join();
    		t2.join();
    		if (getchar() == 'n')
    			break;
    	}
    	return 0;
    }
    
    • 上述程序可能出现多种结果,如
    abcdefghijkabcdefghijk
    12345678901234567890
    
    1234567890abcdefghijkabcdefghijk
    1234567890
    
    abcdefghijkabcdefghijk12345678901234567890
    
    • 究其原因,两个子线程处于并行执行的状态,却都使用了相同的输出流对象,因此发生了数据竞争。

锁机制

  • 如何解决数据竞争问题呢?
    从根本上来说,发生数据竞争是因为多个线程同时使用共享资源,因此为了解决这个问题,可以避免多个线程同时使用共享资源。

使用互斥锁

  • 当互斥量(mutex)被某个线程锁住时,其他尝试获取该互斥量的线程会进入阻塞状态,直到该互斥量被释放为止。但这些线程仍然可以执行其他与mutex无关的代码,即它们不会完全停止执行,只是无法进入被mutex保护的临界区。
    #include <iostream>
    #include <thread>
    #include <mutex>
    
    int main()
    {
    	std::mutex mtx;
    
    	while (true) {
    		std::thread t1([&]() {
    			mtx.lock();
    			std::cout << "1234567890" << "1234567890" << std::endl;
    			mtx.unlock();
    			});
    
    		std::thread t2([&]() {
    			mtx.lock();
    			std::cout << "abcdefghijk" << "abcdefghijk" << std::endl;
    			mtx.unlock();
    			});
    
    		t1.join();
    		t2.join();
    		if (getchar() == 'n')
    			break;
    	}
    	return 0;
    }
    
    • 使用互斥锁后,只可能出现以下两种结果,即同一线程内的输出顺序正常。
    12345678901234567890
    abcdefghijkabcdefghijk
    
    abcdefghijkabcdefghijk
    12345678901234567890
    

使用std::lock_guard

  • 实际使用互斥锁的过程中,常常忘记unlock,为了解决这个问题,C++提供了RAII风格的锁包装器std::lock_guard,它在构造时自动锁定给定的互斥量,并在析构时自动解锁。这有助于确保互斥量在异常情况下也能被正确解锁。
#include <iostream>
#include <thread>
#include <mutex>

int main()
{
	std::mutex mtx;

	while (true) {
		std::thread t1([&]() {
			std::lock_guard<std::mutex> lg(mtx);	// 之前未加锁
			std::cout << "1234567890" << "1234567890" << std::endl;
			});

		std::thread t2([&]() {
			mtx.lock();
			std::lock_guard<std::mutex> lg(mtx, std::adopt_lock);	// 之前已加锁
			std::cout << "abcdefghijk" << "abcdefghijk" << std::endl;
			});

		t1.join();
		t2.join();
		if (getchar() == 'n')
			break;
	}
	return 0;
}

使用std::unique_lock

  • std::unique_lock对象构造时会对互斥量加锁,析构时解锁。此外还能对互斥量进行更灵活的管理,比如延迟加锁。
  • 可以用std::unique_lock替换std::lock_guard,构造时不加锁:std::unique_lock<std::mutex> ul(mtx, std::defer_lock);。支持手动lock和unlock
  • 延迟加锁

原子操作

  • 原子操作是最小的且不可并行化的操作。原子操作省去了mutex上锁、解锁的时间消耗。
  • 对原子变量的以下操作是原子的:
    • 加载(Load):从一个原子变量中读取值。
    • 存储(Store):将一个值写入一个原子变量。
    • 交换(Exchange):将原子变量的当前值与一个新值进行交换,并返回旧值。
    • 比较并交换(Compare-and-Swap, CAS):如果原子变量的当前值与预期值匹配,则将其更新为新值。这个操作返回原子变量在操作之前的值。
  • 对原子操作的组合操作不是原子的,比如n = n + 1
  • 对原子变量执行原子操作的情况下,在任何时候只有一个线程能操作该变量。
    #include <iostream>
    #include <thread>
    #include <atomic>
    using namespace std;
    
    atomic_int n = 0;
    // int n = 0;
    
    void count10000() {
    	for (int i = 1; i <= 10000; i++) {
    		n++;
    	}
    }
    
    int main() {
    	thread th[100];
    	for (thread& x : th)
    		x = thread(count10000);
    	for (thread& x : th)
    		x.join();
    	cout << n << endl;
    	return 0;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值