c++++多线程同步通过多种机制确保线程安全;1.互斥锁(mutex)用于保护共享资源,如代码中使用mtx.lock()和mtx.unlock()控制counter访问;2.条件变量(condition variable)用于线程等待特定条件,如cv.wait()和cv.notify_one()配合unique_lock实现线程通信;3.原子操作(atomic operations)提供轻量级同步,如std::atomic保证counter++的原子性;4.读写锁(read-write lock)允许多个线程同时读取,如std::shared_mutex配合shared_lock和unique_lock实现读写控制;5.信号量(semaphore)控制资源访问数量,如std::counting_semaphore管理最多三个并发线程。选择时应根据场景:互斥锁适合保护共享数据,条件变量适合等待条件触发,原子操作适合简单计数器,读写锁适合读多写少,信号量适合资源池管理。避免死锁的方法包括避免嵌套锁、使用超时锁、减小锁粒度、资源排序等。c++11后新增了recursive_mutex、timed_mutex、future/promise等工具提升并发编程能力。原子操作适用于简单操作,互斥锁则更适合复杂资源保护。
C++多线程同步,简单来说,就是让多个线程能够安全地共享资源,避免出现数据竞争和死锁等问题。要做到这一点,你需要用到一些同步机制,比如互斥锁、条件变量、原子操作等等。
互斥锁(Mutex) 最常用的同步机制之一。想象一下,你和你的朋友想同时用一支笔,但笔只有一个,这时候就需要互斥锁。
#include <iostream> #include <thread> #include <mutex> std::mutex mtx; // 定义一个互斥锁 int counter = 0; void increment() { for (int i = 0; i < 10000; ++i) { mtx.lock(); // 加锁 counter++; mtx.unlock(); // 解锁 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter value: " << counter << std::endl; // 预期结果:20000 return 0; }
这段代码里,mtx.lock() 就像是你拿起了笔,mtx.unlock() 就像是你用完放下了笔。只有拿到锁的线程才能访问 counter 变量。
条件变量(Condition Variable)
立即学习“C++免费学习笔记(深入)”;
互斥锁可以保证资源的安全访问,但如果线程需要等待某个特定条件满足才能继续执行,互斥锁就不够用了。这时候就需要条件变量。
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false; void worker_thread() { std::unique_lock<std::mutex> lock(mtx); // 自动解锁的互斥锁 cv.wait(lock, []{ return ready; }); // 等待条件满足 std::cout << "Worker thread is processing..." << std::endl; } void signal_ready() { std::this_thread::sleep_for(std::chrono::seconds(1)); { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_one(); // 通知一个等待的线程 } int main() { std::thread worker(worker_thread); std::thread signaler(signal_ready); worker.join(); signaler.join(); return 0; }
cv.wait(lock, []{ return ready; }) 让线程进入等待状态,直到 ready 变为 true。cv.notify_one() 用于唤醒一个等待的线程。 std::unique_lock 是一个RAII风格的锁,离开作用域会自动解锁,避免忘记解锁导致死锁。
原子操作(Atomic Operations)
原子操作是一种更轻量级的同步机制,适用于简单的计数器或者标志位。原子操作保证操作的原子性,即不可分割。
#include <iostream> #include <thread> #include <atomic> std::atomic<int> counter(0); void increment() { for (int i = 0; i < 10000; ++i) { counter++; // 原子操作 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter value: " << counter << std::endl; // 预期结果:20000 return 0; }
std::atomic
读写锁(Read-Write Lock)
读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种锁适用于读多写少的场景。C++标准库并没有直接提供读写锁,但你可以使用第三方库,例如 Boost。
#include <iostream> #include <thread> #include <shared_mutex> // C++17引入 std::shared_mutex rw_mutex; int data = 0; void reader() { for (int i = 0; i < 5; ++i) { std::shared_lock<std::shared_mutex> lock(rw_mutex); // 共享锁 std::cout << "Reader: " << data << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } void writer() { for (int i = 0; i < 3; ++i) { std::unique_lock<std::shared_mutex> lock(rw_mutex); // 独占锁 data++; std::cout << "Writer: " << data << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } int main() { std::thread t1(reader); std::thread t2(writer); std::thread t3(reader); t1.join(); t2.join(); t3.join(); return 0; }
std::shared_lock 用于获取共享锁,允许多个线程同时读取数据。std::unique_lock 用于获取独占锁,只允许一个线程写入数据。C++17 引入了 std::shared_mutex,简化了读写锁的使用。
信号量(Semaphore)
信号量是一种更通用的同步机制,可以控制对有限资源的访问。
#include <iostream> #include <thread> #include <semaphore> std::counting_semaphore<3> semaphore(3); // 允许最多3个线程同时访问 void worker(int id) { semaphore.acquire(); // 获取信号量 std::cout << "Thread " << id << " is working..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Thread " << id << " is done." << std::endl; semaphore.release(); // 释放信号量 } int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); std::thread t3(worker, 3); std::thread t4(worker, 4); std::thread t5(worker, 5); t1.join(); t2.join(); t3.join(); t4.join(); t5.join(); return 0; }
std::counting_semaphore semaphore(3) 定义了一个初始值为 3 的信号量。semaphore.acquire() 用于获取信号量,如果信号量的值为 0,线程将进入等待状态。semaphore.release() 用于释放信号量,使信号量的值加 1。
如何选择合适的同步机制?
选择合适的同步机制取决于你的具体需求。
- 互斥锁:适用于保护共享资源,确保同一时间只有一个线程可以访问。
- 条件变量:适用于线程需要等待特定条件满足才能继续执行的场景。
- 原子操作:适用于简单的计数器或者标志位,性能较高。
- 读写锁:适用于读多写少的场景,可以提高并发性能。
- 信号量:适用于控制对有限资源的访问。
多线程同步可能遇到的问题有哪些?
多线程同步虽然能解决并发访问的问题,但也可能引入一些新的问题。
- 死锁:当多个线程互相等待对方释放资源时,就会发生死锁。
- 活锁:线程不断重试一个操作,但由于其他线程的干扰,始终无法成功。
- 饥饿:某个线程长时间无法获得所需的资源,导致无法执行。
- 优先级反转:高优先级线程等待低优先级线程释放资源,导致高优先级线程的执行被延迟。
如何避免死锁?
死锁是多线程编程中最常见的问题之一,以下是一些避免死锁的常用方法:
- 避免嵌套锁:尽量避免在一个锁的保护范围内再请求另一个锁。如果必须使用嵌套锁,确保所有线程以相同的顺序获取锁。
- 使用超时锁:try_lock 可以尝试获取锁,如果一段时间内无法获取,则返回失败,避免永久等待。
- 锁的粒度:减小锁的粒度,尽量只保护需要同步的最小代码块,减少锁的竞争。
- 资源排序:对所有需要锁定的资源进行排序,所有线程按照相同的顺序获取锁。
C++11 之后有哪些新的同步工具?
C++11 引入了许多新的同步工具,例如:
- std::mutex:互斥锁。
- std::recursive_mutex:递归锁,允许同一个线程多次获取同一个锁。
- std::timed_mutex:定时锁,可以设置获取锁的超时时间。
- std::condition_variable:条件变量。
- std::atomic:原子操作。
- std::future 和 std::promise:用于异步编程,可以方便地获取异步操作的结果。
- std::shared_mutex (C++17):读写锁。
- std::counting_semaphore (C++20):信号量。
什么时候应该使用原子操作而不是互斥锁?
原子操作通常比互斥锁更轻量级,性能更高,但原子操作只能用于简单的操作,例如计数器或者标志位的更新。如果需要保护复杂的共享资源,或者需要执行多个操作的原子性,应该使用互斥锁。
总的来说,选择合适的同步机制,并正确地使用它们,是编写高效、安全的并发程序的关键。