C++中锁和原子操作的区别及取舍
文章目录
- 一、锁和原子操作的基本概念
- (一)锁
- (二)原子操作
- 二、锁和原子操作的区别
- (一)定义与实现
- (二)性能特点
- (三)适用场景
- 三、锁和原子操作的应用场景图片示例
- (一)C++中锁的应用场景
- (二)C++中原子操作的应用场景
- 四、如何取舍
- (一)简单操作场景
- (二)复杂操作场景
在现代多线程编程中,确保数据的一致性和线程的协调运行是至关重要的。C++为此提供了两种主要的同步机制:锁和原子操作。这两种机制在内存管理、执行效率和编程模式上有着本质的不同,但它们的共同目标是保证线程之间的正确数据共享和操作顺序。
一、锁和原子操作的基本概念
(一)锁
锁通常用于保护代码的临界区域,确保在同一时间内只有一个线程可以执行该区域内的代码。锁的类型多样,包括互斥锁(std::mutex
)、递归锁(std::recursive_mutex
)、读写锁(std::shared_mutex
)等,各有其适用场景和特点。锁的使用简单直观,可以轻松保护复杂的数据结构或多步操作,但可能因为引入死锁、锁竞争和上下文切换等问题而影响程序的性能。
例如,使用互斥锁保护共享数据的示例代码如下:
#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx;
int shared_data = 0;void increment() {std::lock_guard<std::mutex> lock(mtx);++shared_data;std::cout << "Incremented: " << shared_data << std::endl;
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final shared data: " << shared_data << std::endl;return 0;
}
(二)原子操作
原子操作则提供了一种更为细粒度的同步方式,它可以保证对单个变量的操作是不可分割的,无需加锁即可完成。原子类型(如std::atomic<int>
)确保了在多核处理器上的线程安全访问,通常用于计数器、标志位等简单数据的同步。原子操作能够显著减少同步的开销,特别是在无锁编程中,它们能提高程序的响应性和并行性能。
例如,使用原子操作实现计数器的示例代码如下:
#include <iostream>
#include <thread>
#include <atomic>std::atomic<int> counter(0);void increment() {for (int i = 0; i < 1000; ++i) {counter++;}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Counter value: " << counter << std::endl;return 0;
}
二、锁和原子操作的区别
(一)定义与实现
原子操作是指一个不可分割的操作,要么全部执行成功,要么全部不执行,不会出现中间状态。原子操作通常由硬件指令(如CAS,Compare-And-Swap)或编译器提供的原子类型(如C++的std::atomic
)实现。
锁是一种同步机制,用于保护临界区,确保同一时间只有一个线程可以访问共享资源。锁通常基于操作系统提供的同步原语(如互斥锁、读写锁)或用户空间的实现(如自旋锁)。
(二)性能特点
原子操作通常由硬件指令直接支持,性能开销较低,且不会导致线程阻塞,适合高并发场景。但原子操作的适用范围有限,只能用于简单的操作(如加减、比较交换),复杂的操作无法直接实现。
锁的实现通常涉及操作系统调用(如futex),可能导致线程阻塞和上下文切换,性能开销较大。但锁的适用范围广,可以保护任意复杂的临界区,适用性更强。
(三)适用场景
原子操作适用于对单个变量进行简单的读写、加减、比较交换等操作,以及对性能要求较高的场景,如计数器、标志位等,同时适用于不能接受线程阻塞的场景。
锁适用于需要保护复杂临界区的场景,如对多个变量的操作、复杂的数据结构等,以及各种同步需求,如生产者 - 消费者模型、读写锁等,并且适用于可以接受线程阻塞的场景。
三、锁和原子操作的应用场景图片示例
(一)C++中锁的应用场景
这是一张流程图,展示了锁在分布式系统中的应用流程。从“开始”起,先“创建顺序子节点”,接着判断是否为最小节点,若是则“获取锁”,通过Watcher监听事件唤醒阻塞锁后“删除lock节点,释放锁”最后“结束”;若不是最小节点则“阻塞等待锁,并在上一节点注册watcher”,被通知后回到获取锁步骤。
(二)C++中原子操作的应用场景
这是一张表格图片,包含操作、triv、int type、ptr type、效果五列。操作列有诸如atomic a = val
等多种操作;triv、int type、ptr type列在各操作对应行均为Yes;效果列详细说明了每个操作的效果,如atomic a = val
是以val为a的初值等。表格内容为关于atomic相关操作及效果的说明。
四、如何取舍
(一)简单操作场景
如果需要对单个变量进行简单的操作(如加减、比较交换),并且对性能要求较高,优先选择原子操作。例如,实现一个简单的计数器:
#include <atomic>
#include <thread>
#include <iostream>std::atomic<int> counter(0);void increment() {for (int i = 0; i < 100000; ++i) {counter.fetch_add(1, std::memory_order_relaxed);}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value of counter: " << counter << std::endl;return 0;
}
(二)复杂操作场景
如果需要保护复杂的临界区或操作多个共享资源,优先选择锁。例如,保护一个复杂的数据结构:
#include <mutex>
#include <thread>
#include <list>
#include <iostream>std::mutex mtx;
std::list<int> myList;void insert(int value) {std::lock_guard<std::mutex> lock(mtx);myList.push_back(value);
}void remove(int value) {std::lock_guard<std::mutex> lock(mtx);myList.remove(value);
}int main() {std::thread t1(insert, 1);std::thread t2(insert, 2);std::thread t3(remove, 1);t1.join();t2.join();t3.join();for (int value : myList) {std::cout << value << " ";}std::cout << std::endl;return 0;
}
综上所述,在实际应用中,应根据具体需求选择合适的同步机制,以平衡性能和复杂性。