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

数据竞争

  • 同一进程的多个线程共享地址空间和系统资源,由此可能出现数据竞争的问题。
  • 数据竞争举例
    #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;
    }
    
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: "并发编程实战"是一本经典的编程书籍,该书的指导了我们如何处理并发编程的各种问题。在书,作者通过详细的示例代码向读者展示了如何解决并发编程的各种挑战。 书的示例代码是作者通过实践总结出来的,能够真实地反映出并发编程的现实情况。它们涵盖了并发编程的各个方面,包括线程间的协作、共享资源的同步访问、死锁的避免和解决等等。 这些示例代码不仅仅是为了演示问题,更重要的是教会读者如何正确地处理并发编程的问题。通过学习这些示例代码,我们可以了解到并发编程常见的错误和陷阱,并学会如何规避它们。 在书,作者并没有止步于示例代码的展示,他还详细地解释了每段代码的设计思路和原理。通过深入地解析这些示例代码,我们可以更好地理解并发编程的关键概念和技术,从而能够在实际项目灵活运用。 总的来说,"并发编程实战"这本书的示例代码是非常实用和有价值的。通过学习和理解这些代码,我们可以提升自己在并发编程领域的能力,更好地应对并发编程的各种挑战。 ### 回答2: "C 并发编程实战" 是一本经典的编程书籍,详细介绍了并发编程的各个方面包括并发模型、线程同步、线程安全、锁机制、线程池等。这本书的核心在于通过源代码实例,帮助读者深入理解并发编程概念和技术,并从实践角度掌握相关知识。 "C 并发编程实战"源代码示例给读者提供了一个实践的框架,使他们能够在实际的项目应用并发编程技术。这些源代码示例往往是基于实际应用场景设计的,通过实例代码的编写和运行,读者可以更加直观地感受到并发编程的特点和挑战。 源代码示例可能涉及到多线程的创建、线程同步的实现、线程安全的问题处理等。例如,可以有一个生产者-消费者模型的示例,展示如何通过多线程实现任务的生产和处理,以及如何使用锁机制来保证线程安全。另外,还可以有一个基于线程池的示例,用于展示如何利用线程池提高并发性能,以及如何管理线程池的各个参数。 通过源代码示例,读者可以学习到在并发编程的方方面面的知识,掌握并发编程的基本技能。同时,通过实践的错误和问题,读者还可以学会如何处理常见的并发编程问题,如死锁、饥饿等。 总之,"C 并发编程实战"的源代码示例是一种非常宝贵的学习资源,它能够帮助读者深入理解并发编程的概念和技术,并通过实践掌握相关的编程技能。 ### 回答3: "c并发编程实战"是一本关于并发编程的经典书籍,旨在帮助读者理解和应用并发编程的原理和技术。在该书,作者通过详细讲解并发编程的核心概念,并提供了丰富的源代码示例,以帮助读者更好地理解和运用这些概念。 书的源代码示例非常实用,涵盖了多线程、锁、线程池、阻塞队列、并发容器等多个方面的内容。这些示例代码通过具体的场景和问题,展示了不同并发编程技术的应用,使读者能够学以致用。 源代码示例,作者注重讲解每个示例的实现原理和核心思想,并配以详细的注释,方便读者理解。读者可以通过运行这些示例代码,深入理解并发编程的实际应用,并通过代码的演示,发现其的问题和解决方案。 此外,书的源代码示例还提供了一些常见的并发编程模式,如生产者-消费者模式、读写锁模式等。这些模式可以帮助读者在实际项目更好地设计和开发高效的并发程序。 总的来说,“c并发编程实战”这本书的源代码示例丰富实用,帮助读者深入理解并发编程的原理和技术,并能够应用这些知识解决实际问题。无论是新手还是有一定经验的开发者,都可以从获益匪浅。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值