当前位置: 首页 > news >正文

线程锁-互斥、自旋、读写、原子操作、线程池

1.自旋锁

2.互斥锁

原子操作

线程池

多线程同步编程详解

多线程同步是并发编程中的核心概念,下面我将详细介绍互斥锁、自旋锁、读写锁、原子操作和线程池,并提供代码示例、使用条件和常见易错点。

1. 互斥锁 (Mutex)

基本概念

互斥锁是最常用的线程同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问临界区。

代码示例 (C++11)

#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx;
int shared_data = 0;void increment() {for (int i = 0; i < 100000; ++i) {mtx.lock();++shared_data;mtx.unlock();}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value: " << shared_data << std::endl;return 0;
}

使用条件

  • 当多个线程需要访问同一共享资源时
  • 临界区代码执行时间较长(如文件I/O、复杂计算)

注意事项

  1. 避免死锁​:确保锁总是被释放,可以使用std::lock_guardstd::unique_lock自动管理
  2. 锁粒度​:锁的范围不宜过大,否则会降低并发性能
  3. 异常安全​:临界区内可能抛出异常,导致锁无法释放

易错点

  • 忘记解锁(推荐使用RAII风格的锁管理)
  • 锁的嵌套使用不当导致死锁
  • 在不同函数中加锁/解锁,导致逻辑混乱

2. 自旋锁 (Spinlock)

基本概念

自旋锁是一种忙等待锁,线程在获取锁失败时会循环检查锁状态,而不是被挂起。

代码示例 (C++11)

#include <atomic>
#include <thread>class Spinlock {std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:void lock() {while (flag.test_and_set(std::memory_order_acquire));}void unlock() {flag.clear(std::memory_order_release);}
};Spinlock spin;
int shared_data = 0;void increment() {for (int i = 0; i < 100000; ++i) {spin.lock();++shared_data;spin.unlock();}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value: " << shared_data << std::endl;return 0;
}

使用条件

  • 临界区代码执行时间非常短
  • 不希望线程被挂起(避免上下文切换开销)
  • 多核处理器环境

注意事项

  1. CPU占用​:自旋锁会持续占用CPU
  2. 单核慎用​:单核CPU上可能导致性能问题
  3. 优先级反转​:高优先级线程可能被低优先级线程阻塞

易错点

  • 在长时间临界区使用自旋锁(应改用互斥锁)
  • 忘记释放自旋锁
  • 在单核系统上使用导致性能下降

3. 读写锁 (Read-Write Lock)

基本概念

读写锁允许多个读操作同时进行,但写操作需要独占访问,适用于读多写少的场景。

代码示例 (C++17)

#include <shared_mutex>
#include <thread>
#include <vector>std::shared_mutex rw_mutex;
int shared_data = 0;void reader(int id) {for (int i = 0; i < 5; ++i) {{std::shared_lock lock(rw_mutex);std::cout << "Reader " << id << " sees: " << shared_data << std::endl;}std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}void writer(int id) {for (int i = 0; i < 2; ++i) {{std::unique_lock lock(rw_mutex);++shared_data;std::cout << "Writer " << id << " updated to: " << shared_data << std::endl;}std::this_thread::sleep_for(std::chrono::milliseconds(200));}
}int main() {std::vector<std::thread> readers;for (int i = 0; i < 5; ++i) {readers.emplace_back(reader, i);}std::vector<std::thread> writers;for (int i = 0; i < 2; ++i) {writers.emplace_back(writer, i);}for (auto& t : readers) t.join();for (auto& t : writers) t.join();return 0;
}

使用条件

  • 读操作远多于写操作
  • 读操作不需要修改共享数据
  • 读操作持续时间较长

注意事项

  1. 写者饥饿​:持续有读者可能导致写者无法获取锁
  2. 升级问题​:从读锁升级到写锁通常不支持
  3. 公平性​:某些实现可能不公平,导致某些线程长期得不到机会

易错点

  • 在持有读锁时修改数据(未定义行为)
  • 错误估计读写比例(写多读少时性能可能不如互斥锁)
  • 忽略锁的升级限制

4. 原子操作 (Atomic Operations)

基本概念

原子操作是不可分割的操作,由CPU直接保证其原子性,无需额外锁机制。

代码示例 (C++11)

#include <atomic>
#include <thread>
#include <vector>std::atomic<int> counter(0);void increment(int n) {for (int i = 0; i < n; ++i) {counter.fetch_add(1, std::memory_order_relaxed);}
}int main() {const int num_threads = 10;const int increments_per_thread = 100000;std::vector<std::thread> threads;for (int i = 0; i < num_threads; ++i) {threads.emplace_back(increment, increments_per_thread);}for (auto& t : threads) t.join();std::cout << "Final counter value: " << counter << std::endl;return 0;
}

使用条件

  • 简单的共享变量操作(如计数器、标志位)
  • 需要最高性能的同步场景
  • 对单个变量的操作

注意事项

  1. 内存顺序​:理解并正确使用memory_order
  2. 复合操作​:多个原子操作组合不是原子的
  3. ABA问题​:某些场景下需要考虑

易错点

  • 错误使用memory_order导致可见性问题
  • 认为多个原子操作组合是原子的
  • 忽略缓存一致性问题

5. 线程池 (Thread Pool)

基本概念

线程池预先创建一组线程,避免频繁创建销毁线程的开销,提高任务执行效率。

代码示例 (C++11简单实现)

#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>class ThreadPool {
public:ThreadPool(size_t threads) : stop(false) {for (size_t i = 0; i < threads; ++i) {workers.emplace_back([this] {while (true) {std::function<void()> task;{std::unique_lock<std::mutex> lock(this->queue_mutex);this->condition.wait(lock, [this] {return this->stop || !this->tasks.empty();});if (this->stop && this->tasks.empty()) return;task = std::move(this->tasks.front());this->tasks.pop();}task();}});}}template<class F, class... Args>auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {using return_type = typename std::result_of<F(Args...)>::type;auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<return_type> res = task->get_future();{std::unique_lock<std::mutex> lock(queue_mutex);if(stop) throw std::runtime_error("enqueue on stopped ThreadPool");tasks.emplace([task](){ (*task)(); });}condition.notify_one();return res;}~ThreadPool() {{std::unique_lock<std::mutex> lock(queue_mutex);stop = true;}condition.notify_all();for (std::thread &worker : workers)worker.join();}private:std::vector<std::thread> workers;std::queue<std::function<void()>> tasks;std::mutex queue_mutex;std::condition_variable condition;bool stop;
};// 使用示例
int main() {ThreadPool pool(4);std::vector<std::future<int>> results;for (int i = 0; i < 8; ++i) {results.emplace_back(pool.enqueue([i] {std::cout << "Task " << i << " started\n";std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Task " << i << " finished\n";return i*i;}));}for (auto&& result : results)std::cout << "Result: " << result.get() << std::endl;return 0;
}

使用条件

  • 需要执行大量短期任务
  • 避免频繁创建销毁线程的开销
  • 需要限制并发线程数量

注意事项

  1. 任务队列大小​:无界队列可能导致内存耗尽
  2. 异常处理​:任务中的异常需要妥善处理
  3. 线程数量​:根据CPU核心数和任务类型选择合适数量
  4. 任务依赖​:注意任务间的依赖关系

易错点

  • 忘记正确处理线程池关闭
  • 任务抛异常导致线程退出
  • 任务之间有共享状态但未同步
  • 线程数量设置不合理(过多或过少)

综合对比

同步机制适用场景优点缺点
互斥锁一般共享资源访问简单可靠,线程挂起不占CPU上下文切换开销大
自旋锁极短临界区,多核无上下文切换,响应快忙等待消耗CPU
读写锁读多写少场景允许多个读者并发实现复杂,可能写者饥饿
原子操作简单变量操作最高性能,无锁只能用于简单操作
线程池大量短期任务避免线程创建开销,控制并发度实现复杂,需管理任务队列

最佳实践建议

  1. 优先考虑更高层次的抽象​:如任务并行库(TBB)、OpenMP等
  2. 避免过早优化​:先用简单互斥锁,有性能问题再考虑其他
  3. 尽量减少共享数据​:通过设计减少同步需求
  4. 使用工具检测问题​:如ThreadSanitizer检测数据竞争
  5. 理解内存模型​:特别是使用原子操作时
  6. 测试多线程代码​:多线程bug往往难以重现,需要专门测试

多线程编程复杂但功能强大,正确使用这些同步机制可以构建高效可靠的并发程序。

http://www.xdnf.cn/news/1238581.html

相关文章:

  • 江协科技STM32 14-1 WDG看门狗
  • Python篇---环境变量软件安装
  • 【视频内容创作】PR的关键帧动画
  • C++23 Concepts:用类型约束重构泛型编程的终极方案
  • k8s+isulad 国产化技术栈云原生技术栈搭建2-crictl
  • io_cancel系统调用及示例
  • 数据结构:单向链表的函数创建
  • 二叉树的锯齿形层次遍历
  • 思途JSP学习 0802(项目完整流程)
  • day 44 文件的规范书写与拆分
  • 《 ThreadLocal 工作机制深度解析:高并发场景的利与弊》
  • Spring+K8s+AI实战:3全栈开发指南
  • Redis实战(7)-- 高级特性 Redis Stream数据结构与基础命令
  • HCIE-Datacom题库_07_设备【道题】
  • kafka与其他消息队列(如 RabbitMQ, ActiveMQ)相比,有什么优缺点?
  • Qt-vs加载exe图标
  • 日常--详细介绍qt Designer常用快捷键(详细图文)
  • 其它IO函数
  • Fay数字人如何使用GPT-SOVITS进行TTS转换以及遇到的一些问题
  • 《基于通道注意力与空洞卷积的胸片肺气肿检测算法》论文解析
  • [硬件电路-138]:模拟电路 - 什么是正电源?什么是负电源?集成运放为什么有VCC+和VCC-
  • Python切片命名技术详解:提升代码可读性与维护性的专业实践
  • 2106. 摘水果
  • 关于assert()函数,eval()函数,include
  • 第N个泰波那契数
  • Spring lookup-method实现原理深度解析
  • e2studio开发RA4M2(6)----GPIO外部中断(IRQ)配置
  • 信创及一次ORACLE到OB的信创迁移
  • 图像、视频、音频多模态大模型中长上下文token压缩方法综述
  • 使用 Vuepress + GitHub Pages 搭建项目文档