cpp多线程\进程学习
1.thread
std::thread是 C++11 引入的跨平台线程管理类,封装了操作系统的线程 API(如 pthread、Windows 线程),提供统一的线程操作接口。线程的生命周期由join()和detach()控制。
thread在创建时就开始执行
join():join () 会让调用该方法的线程(通常是主线程)阻塞在调用 join () 的位置,直到子线程 完整执行完毕,调用线程才会解除阻塞,继续执行join()之后的代码。
detach():分离线程(后台运行),无需等待。线程结束后自动释放资源(无法再join)。
joinable():检查线程是否可连接
get_id():获取线程 ID,this_thread::get_id获取当前线程id
swap():交换线程所有权,线程不变,交换句柄
2.mutex
mutex用于保证线程安全,属于互斥锁,直接调用lock()/unlock()易导致死锁(如临界区抛出异常未解锁)。互斥锁(std::mutex)本身没有主动 “通知” 等待线程的机制,但等待线程可以通过被动感知锁的状态变化来获取锁已释放的信息,依赖操作系统的 “唤醒机制”。
C++ 提供 RAII(资源获取即初始化)包装类自动管理锁的生命周期,保证异常安全:
std::lock_guard:构造时加锁,析构时解锁(不可手动解锁)。
std::unique_lock:构造时可选延迟加锁(std::defer_lock,适用于多锁协调场景,防止死锁),支持手动解锁、转移锁所有权。
互斥锁(等不及就休眠):
自旋锁(死等不放):cpp自旋锁依赖于atomic(compare_exchange_weak)实现
3.condition_variable
std::condition_variable是线程间通信的工具,允许一个或多个线程等待某个条件满足(由其他线程通知唤醒),避免无效的轮询等待。
与unique_lock结合使用
wait(lock):阻塞当前线程并释放锁,直到被notify_one()/notify_all()唤醒并满足条件(自动释放锁),否则继续阻塞。
wait(lock, predicate):带谓词的等待:仅当predicate()返回true时唤醒(避免虚假唤醒)。谓词(predicate)的作用是在被notify唤醒后检查条件是否满足,其内部等价于下述代码
while (!predicate()) { wait(lock); // 释放锁并阻塞,直到被唤醒 }notify_one():从等待队列中,随机唤醒一个线程
notify_all():唤醒等待队列中所有的线程,多个线程竞争
4.Semaphore
包括counting_semaphore、binary_semaphore两个信号量
具有方法 acquire,信号量-1;try_acquire,允许失败,不阻塞;release,信号量+1;
#include <chrono> #include <iostream> #include <semaphore> #include <thread> std::binary_semaphore //std::counting_semaphore<1> smphSignalMainToThread{0}, smphSignalThreadToMain{0}; void ThreadProc() { smphSignalMainToThread.acquire(); std::cout << "[thread] Got the signal\n"; // response message using namespace std::literals; std::this_thread::sleep_for(3s); std::cout << "[thread] Send the signal\n"; // message smphSignalThreadToMain.release(); } int main() { std::thread thrWorker(ThreadProc); std::cout << "[main] Send the signal\n"; // message smphSignalMainToThread.release(); smphSignalThreadToMain.acquire(); std::cout << "[main] Got the signal\n"; // response message thrWorker.join(); }5.atomic
std::atomic<T>是模板类,用于定义原子类型,确保对其的操作是原子的(不可分割),避免多线程访问时的数据竞争,实现无锁编程,但仅适用于int等大多数基本类型,也可以应用于大多数指针atomic<T *>,CAS是其核心硬件基础。
std::atomic<T>要求类型T是 “可平凡复制”(trivially copyable)的。即T不能有自定义的复制构造函数、移动构造函数、赋值运算符,且其内存布局可通过原始字节拷贝(如memcpy)复制。
除了原子类型的一些运算重载,还有原子操作 load、store、fetch_add、feach_sub,可以搭配memory_order使用定义内存序
常见方法:
| load | int r=k.load(memory_order_acquire); |
| store | k.store(1,memory_order_release); |
| compare_exchange_weak | lock_flag.compare_exchange_weak(expect, true, acquire, relaxed) |
适用于非强竞争的场景
// 极简自旋锁:完全依赖std::atomic实现 class SpinLock { private: // 1. 用atomic<bool>做锁标记(普通bool会有线程安全问题) std::atomic<bool> lock_flag{false}; // false=未锁,true=已锁 public: // 加锁:自旋抢锁,依赖atomic的CAS原子操作 void lock() { bool expect = false; // CAS:原子地检查并修改锁标记,失败则循环(自旋) while (!lock_flag.compare_exchange_weak(expect, true)) { expect = false; // 重置期望,准备下次重试 } } // 解锁:依赖atomic的原子写操作 void unlock() { lock_flag.store(false); // 原子重置锁标记,避免多线程混乱 } };6.memory_order
atomic保证操作的原子性,memory_order保证可见性和顺序性
memory_order是 C++11 引入的枚举类型,是原子变量执行操作时的行为规则,用于控制原子操作(std::atomic)的内存可见性和指令重排序约束,是无锁编程中保证多线程数据一致性的核心机制。
| memory_order_seq_cst(默认) | 顺序一致性,最严格的约束,所有线程看到的原子操作顺序完全一致,但性能开销大 |
| memory_order_relaxed | 宽松操作:不施加任何同步或排序约束于其他读写,仅保证此操作的原子性 |
| memory_order_acquire | 获取语义:当前线程中,原子操作之后的所有读操作不会被重排序到原子操作之前;且能看到其他线程对同一原子变量执行release操作前的所有写入。 |
| memory_order_release | 释放语义:当前线程中,原子操作之前的所有写操作不会被重排序到原子操作之后;且这些写入对执行acquire操作的线程可见。 |
内存可见性:memory_order或mutex包括两个完全不同层级的 “缓存问题”—— 一个是编译器层面的 “寄存器缓存”,一个是硬件层面的 “CPU 缓存(L1/L2/L3)一致性”;而 C++volatile只盯着前者,对后者完全 “视而不见”。C++ 中的volatile并非为多线程同步设计(这是最关键的认知),其核心作用是约束编译器对 “易变内存” 的优化行为。程序与硬件 / 外部信号交互的场景,天然不会触发「跨核心的 CPU 缓存一致性问题」
7.moodycamel::ConcurrentQueue
ConcurrentQueue是一个第三方无锁队列,它专为多线程环境设计,允许多个线程同时进行入队、出队操作,而无需开发者手动实现同步机制,从而简化了多线程数据共享的复杂度。使用以下四个方法实现线程安全:
ConcurrentQueue(size_t initialSizeEstimate)构造函数,初始化队列大小enqueue(T&& item)入队,若空间不足会分配额外空间try_enqueue(T&& item)尝试入队,若有空间,入队成功,返回truetry_dequeue(T& item)尝试出队,item=出队值,出队成功返回true
msvc提供了默认的concurrent_queue ,gcc中可使用moodycamel::ConcurrentQueue。
以有界缓冲区为例理解mutex和condition_variable的用法,若将队列替换为ConcurrentQueue则无需mutex和condition_variable,并且可以实现并发。
同样也可以通过原子操作切换为无锁
#include <queue> #include <mutex> #include <condition_variable> template<typename T> class BoundedBuffer { public: explicit BoundedBuffer(size_t capacity) : capacity_(capacity) { if (capacity == 0) { throw std::invalid_argument("Buffer capacity must be greater than 0"); } } // 入队操作(阻塞直到有空间) void push(T element) { std::unique_lock<std::mutex> lock(mtx_); // 等待缓冲区不满(处理虚假唤醒) not_full_.wait(lock, [this] { return queue_.size() < capacity_; }); queue_.push(std::move(element)); // 使用移动语义避免拷贝 not_empty_.notify_one(); // 通知可能等待的出队操作 } // 出队操作(阻塞直到有元素) T pop() { std::unique_lock<std::mutex> lock(mtx_); // 等待缓冲区非空(处理虚假唤醒) not_empty_.wait(lock, [this] { return !queue_.empty(); }); auto val = std::move(queue_.front()); // 使用移动语义避免拷贝 queue_.pop(); not_full_.notify_one(); // 通知可能等待的入队操作 return val; } // 获取当前元素数量(线程安全) size_t size() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.size(); } private: std::queue<T> queue_; mutable std::mutex mtx_; // mutable允许在const成员函数中加锁 std::condition_variable not_full_; // 缓冲区不满条件变量 std::condition_variable not_empty_; // 缓冲区非空条件变量 const size_t capacity_; // 缓冲区最大容量 }; int main() { const size_t buffer_capacity = 5; const int data_count = 10; // 每个生产者生产10个数据 BoundedBuffer<int> buffer(buffer_capacity); // 生产者线程:向缓冲区推送数据 auto producer = [&buffer, data_count](int producer_id) { for (int i = 0; i < data_count; ++i) { buffer.push(producer_id * 100 + i); // 生产数据(ID+序号) std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟生产耗时 } }; // 消费者线程:从缓冲区取出数据 auto consumer = [&buffer, data_count](int consumer_id) { for (int i = 0; i < data_count; ++i) { int val = buffer.pop(); // 消费数据(阻塞直到有数据) std::cout << "Consumer " << consumer_id << " popped: " << val << std::endl; } }; // 创建线程,第二个参数为线程id std::vector<std::thread> threads; threads.emplace_back(producer, 1); // 生产者1 threads.emplace_back(producer, 2); // 生产者2 threads.emplace_back(consumer, 1); // 消费者1 threads.emplace_back(consumer, 2); // 消费者2 // 主线程等待所有线程完成(join) for (auto& t : threads) { if (t.joinable()) { // 避免重复join(如线程已detach) t.join(); // 阻塞主线程,直到所有生产者/消费者完成 } } std::cout << "All threads completed." << std::endl; return 0; }进程间通信方式:
1.管道:内核维护的一段环形内存缓冲区,半双工通信。需要两个管道实现双全工。
2.消息队列:内核维护的 “消息链表”,进程可按 “类型” 发送 / 接收消息(消息包含类型和数据)。单个队列即可实现双全工通信,内核会为不同类型的消息维护隐性索引,内核会精准定位并提取目标消息,非目标类型数据仍留在队列中。
3.共享内存:内核分配的一块物理内存,多个进程将其映射到自己的虚拟地址空间,直接读写。
mmap 实现共享内存:通过mmap将同一文件映射到不同进程的虚拟内存中,并且每个进程以可读写权限(PROT_READ | PROT_WRITE)和共享模式(MAP_SHARED)映射时,完全可以实现进程间通信。
#include <sys/mman.h> void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void* addr, size_t length);4.信号: 操作系统向进程发送的 “事件通知”(如异常、中断)。信号不是 “数据传输工具”,而是 “事件触发器”,仅传递 “发生了某类事件” 的通知(最多附带少量附加数据)。
5.嵌套字:通过网络协议(如 TCP/UDP)或本地协议(如 AF_UNIX)实现进程通信的接口。
进程安全:
1. 互斥量(Mutex):需要一个全局唯一的互斥量名称,适合单机内、高频、短耗时的进程间临界区同步,pthread_mutex_t
进程锁初始化
pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setpshared( &attr, PTHREAD_PROCESS_SHARED ); pthread_mutex_init(&shared->mutex, &attr); pthread_mutexattr_destroy(&attr);2. 文件锁(跨进程的文件级同步):适合与文件强相关或跨机器的低频次同步
3. 信号量(进程间并发控制)
读写锁
读写锁(Read-Write Lock)的核心是“读写分离、允许多读单写”,核心特性:
- 读锁(共享锁):多个线程可同时获取,互不阻塞(读 - 读兼容);
- 写锁(排他锁):仅一个线程可获取,会阻塞所有读锁和其他写锁(读 - 写、写 - 写互斥);
- 适用场景:读多写少(如缓存查询、配置读取、日志统计),比普通互斥锁(Mutex)更高效(避免读操作互相阻塞)。
包括了读锁的线程数、写锁占用、写锁等待数
普通std::mutex是 “无脑独占”(不管读写,先抢锁再说),而读写锁是 “先看标记再决定怎么锁”(用标记区分 “读共享” 和 “写排他”),标记是 “规则”,锁是 “执行规则的工具”。
- 读线程的 notify 是 “精准唤醒写线程”—— 一个就够,用
notify_one(); - 写线程的 notify 是 “广播唤醒所有等待线程”—— 既要让读线程并发,又要让写线程有机会,用
notify_all()。
class ReadWriteLock { private: std::mutex mutex_; // 保护内部状态的互斥锁(门卫) std::condition_variable cond_; // 阻塞/唤醒线程的条件变量(传令官) int read_count_ = 0; // 当前持有读锁的线程数 bool write_flag_ = false; // 写锁是否被占用(true=占用) int write_wait_count_ = 0; // 等待写锁的线程数(保证写优先级) public: // -------------------------- 读锁操作 -------------------------- void lock_read() { // 1. 锁定 mutex,保护状态不被并发修改 std::unique_lock<std::mutex> lock(mutex_); // 2. 循环等待:没有写锁占用 + 没有写线程等待(写优先级) while (write_flag_ || write_wait_count_ > 0) { cond_.wait(lock); // 释放 mutex,阻塞当前线程;唤醒后重新获取 mutex } // 3. 读计数+1:标记当前线程持有读锁 read_count_++; // 4. unique_lock 析构时自动解锁 mutex(无需手动 unlock) } void unlock_read() { std::unique_lock<std::mutex> lock(mutex_); // 1. 读计数-1:当前线程释放读锁 read_count_--; // 2. 若为最后一个读线程,唤醒一个等待的写线程(写线程需要读计数=0) if (read_count_ == 0) { cond_.notify_one(); // 只唤醒一个写线程(避免多个写线程竞争) } } // -------------------------- 写锁操作 -------------------------- void lock_write() { std::unique_lock<std::mutex> lock(mutex_); // 1. 写等待数+1:标记有写线程在等,让后续读线程阻塞 write_wait_count_++; // 2. 循环等待:没有读线程占用 + 没有其他写锁占用 while (read_count_ > 0 || write_flag_) { cond_.wait(lock); // 释放 mutex,阻塞当前线程 } // 3. 标记写锁被当前线程占用 write_flag_ = true; // 4. 写等待数-1:当前线程已获取锁,移除等待标记 write_wait_count_--; } void unlock_write() { std::unique_lock<std::mutex> lock(mutex_); // 1. 标记写锁释放 write_flag_ = false; // 2. 唤醒所有等待的线程(读+写),让它们重新竞争锁 cond_.notify_all(); } }