如何用CAS或pthread实现共享内存进程间同步?三类技术疑问求解
我想用如下shmLock结构体控制共享内存的进程间同步:
struct shmLock { std::atomic_uint32_t user_count; pthread_mutex_t mutex; };
使用逻辑是:当user_count == 0时初始化mutex,再通过CAS操作增加user_count;释放锁时,通过CAS操作减少user_count,当user_count降至0时销毁mutex。我写了对应的myLock类实现:
class myLock { public: myLock(shmLock* lock) : m_lock(lock) { uint32_t user_count = m_lock->user_count.load(); if (user_count == 0) { pthread_mutexattr_t mutex_attr{}; pthread_mutexattr_init(&mutex_attr); pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(&m_lock->mutex, &mutex_attr); } m_lock->user_count.compare_exchange_strong(user_count, user_count + 1); pthread_mutex_lock(&m_lock->mutex); } ~myLock() { pthread_mutex_unlock(&m_lock->mutex); uint32_t user_count = m_lock->user_count.load(); m_lock->user_count.compare_exchange_strong(user_count, user_count - 1); if (user_count == 0) { pthread_mutex_destroy(&m_lock->mutex); } } private: shmLock* m_lock; };
现在有三个技术疑问:
- 能否不借助
user_count控制mutex的初始化与销毁,直接在构造函数中初始化、析构函数中销毁? - 若
compare_exchange_strong执行失败,应如何处理? - 当前实现方式存在不足,是否有更优的共享内存进程间同步控制方案?
1. 能否不借助user_count控制mutex的初始化与销毁?
不行。直接在构造函数初始化、析构函数销毁会触发严重的竞态问题:
- 多个进程同时进入构造函数时,会重复初始化同一个
pthread_mutex_t,这属于未定义行为,大概率导致程序崩溃。 - 析构函数同理,当还有其他进程持有锁时,一个进程销毁mutex会让其他进程在操作锁时触发未定义行为(比如解锁已销毁的mutex)。
user_count的核心作用就是跟踪当前使用锁的进程/线程数,保证mutex只被初始化一次、销毁一次。
2. compare_exchange_strong执行失败的处理方式
compare_exchange_strong失败意味着:在你读取user_count到执行CAS的这段时间里,其他线程/进程修改了user_count的值。此时必须重新读取最新的user_count,然后重试整个逻辑。
以构造函数为例,正确的循环处理逻辑如下:
myLock(shmLock* lock) : m_lock(lock) { uint32_t user_count; do { user_count = m_lock->user_count.load(); if (user_count == 0) { pthread_mutexattr_t mutex_attr{}; pthread_mutexattr_init(&mutex_attr); pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED); // 二次检查user_count,避免其他进程已经完成初始化 if (m_lock->user_count.load() == 0) { pthread_mutex_init(&m_lock->mutex, &mutex_attr); } pthread_mutexattr_destroy(&mutex_attr); // 别忘了销毁属性对象 } } while (!m_lock->user_count.compare_exchange_strong(user_count, user_count + 1)); pthread_mutex_lock(&m_lock->mutex); }
析构函数里的CAS失败同理,需要循环重试,且注意判断销毁条件的逻辑错误(原代码判断减之前的user_count == 0是错的,应该判断减之前的值为1,这样减后才是0):
~myLock() { pthread_mutex_unlock(&m_lock->mutex); uint32_t user_count; do { user_count = m_lock->user_count.load(); } while (!m_lock->user_count.compare_exchange_strong(user_count, user_count - 1)); if (user_count == 1) { // 减之前为1,减后为0,此时可以销毁mutex pthread_mutex_destroy(&m_lock->mutex); } }
3. 更优的共享内存进程间同步方案
当前实现的核心问题是初始化mutex时的竞态(多个进程同时看到user_count=0,都会尝试初始化),以及CAS循环的效率问题。可以考虑以下几种更可靠的方案:
方案一:用pthread_once_t保证mutex仅初始化一次
把shmLock里的user_count拆分为引用计数和初始化标记,用pthread_once_t跨进程保证初始化的原子性:
struct shmLock { std::atomic_uint32_t ref_count; pthread_once_t init_flag; pthread_mutex_t mutex; }; // 单独的mutex初始化函数,由pthread_once触发 void init_shm_mutex(void* arg) { shmLock* lock = static_cast<shmLock*>(arg); pthread_mutexattr_t mutex_attr{}; pthread_mutexattr_init(&mutex_attr); pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(&lock->mutex, &mutex_attr); pthread_mutexattr_destroy(&mutex_attr); } class myLock { public: myLock(shmLock* lock) : m_lock(lock) { // 保证init_shm_mutex只被执行一次(跨进程安全) pthread_once(&m_lock->init_flag, init_shm_mutex, m_lock); m_lock->ref_count.fetch_add(1); pthread_mutex_lock(&m_lock->mutex); } ~myLock() { pthread_mutex_unlock(&m_lock->mutex); // 最后一个使用者销毁mutex if (m_lock->ref_count.fetch_sub(1) == 1) { pthread_mutex_destroy(&m_lock->mutex); // 重置标记,方便后续重新使用(可选) pthread_once_init(&m_lock->init_flag, PTHREAD_ONCE_INIT); } } private: shmLock* m_lock; };
pthread_once_t本身支持共享内存场景下的跨进程原子执行,能完美解决初始化竞态问题,代码逻辑更简洁可靠。
方案二:直接使用进程间原生同步原语
如果系统支持,可直接用更成熟的跨进程同步工具:
- POSIX进程间互斥量:提前在共享内存中初始化好带有
PTHREAD_PROCESS_SHARED属性的mutex,不需要动态初始化逻辑。 - POSIX信号量:支持共享内存或文件系统两种方式,API简洁,适合进程间同步。
- 文件锁(fcntl):不需要共享内存,通过文件描述符实现同步,使用简单但性能略低。
方案三:C标准库跨进程同步(C11+)
如果编译器支持,std::mutex配合共享内存也能实现跨进程同步,但需要手动设置进程共享属性(类似pthread的PTHREAD_PROCESS_SHARED),不同编译器实现可能有差异,稳定性不如直接使用pthread接口。
内容的提问来源于stack exchange,提问作者quanquanquan97

