在并发编程中,同步的本质是在保证数据安全的前提下,让线程访问资源具有可预测的顺序性。想象一下银行柜台办理业务的情景:多个客户(线程)需要访问同一个柜台(共享资源),如果没有排队机制(同步),所有人都会一拥而上争夺服务,导致混乱。
同步机制的关键特点:
注意:同步不是替代互斥,而是在互斥基础上增加顺序控制。就像银行既要防止多人同时操作同一个账户(互斥),又要让客户按顺序办理业务(同步)。
互斥和同步是并发控制的两个核心维度:
mermaid复制graph TD
A[并发控制] --> B[互斥]
A --> C[同步]
B --> D[解决同时访问问题]
B --> E[工具:互斥锁、读写锁]
C --> F[解决执行顺序问题]
C --> G[工具:条件变量、信号量]
互斥关注的是"能不能访问"的问题,而同步解决的是"谁先谁后"的问题。在实际开发中,我们通常需要同时使用这两种机制:
条件变量可以理解为线程间的通知机制,它包含两个核心组件:
工作流程示例:
pthread_cond_wait(&cond, &mutex)的内部操作是原子的,这保证了:
这三个操作要么全部完成,要么都不执行,避免了竞态条件。如果这三个操作不是原子的,可能会出现以下问题:
c复制// 错误示例:非原子操作导致的问题
pthread_mutex_lock(&mutex);
while(条件不满足){
pthread_mutex_unlock(&mutex); // 非原子操作点1
// 这里如果发生线程切换...
pthread_cond_wait(&cond); // 非原子操作点2
// 可能导致信号丢失
}
| 函数 | 行为特点 | 适用场景 |
|---|---|---|
| pthread_cond_signal | 唤醒至少一个等待线程 | 资源有限,只需唤醒一个消费者 |
| pthread_cond_broadcast | 唤醒所有等待线程 | 状态变化影响所有等待者 |
经验法则:在不确定有多少线程需要被唤醒时优先使用broadcast,但要注意可能引发的"惊群效应"。
正确的条件变量使用模板:
c复制pthread_mutex_lock(&mutex);
while(条件不满足){ // 必须用while而不是if
pthread_cond_wait(&cond, &mutex);
}
// 处理临界区
pthread_mutex_unlock(&mutex);
关键注意事项:
生产者-消费者模型解决的是有界缓冲区问题,其核心约束可以总结为:
模型优势体现在:
cpp复制template <class T>
class BlockQueue {
private:
std::queue<T> q_;
int maxcap_;
pthread_mutex_t mutex_;
pthread_cond_t c_cond_; // 消费者条件变量
pthread_cond_t p_cond_; // 生产者条件变量
public:
void push(const T& item) {
pthread_mutex_lock(&mutex_);
while(q_.size() == maxcap_) {
pthread_cond_wait(&p_cond_, &mutex_);
}
q_.push(item);
pthread_cond_signal(&c_cond_);
pthread_mutex_unlock(&mutex_);
}
T pop() {
pthread_mutex_lock(&mutex_);
while(q_.empty()) {
pthread_cond_wait(&c_cond_, &mutex_);
}
T item = q_.front();
q_.pop();
pthread_cond_signal(&p_cond_);
pthread_mutex_unlock(&mutex_);
return item;
}
};
在多生产者和多消费者场景下,需要注意:
cpp复制template<class T>
class RingQueue {
private:
std::vector<T> ringqueue_;
int cap_;
int c_step_; // 消费者位置
int p_step_; // 生产者位置
sem_t cdata_sem_; // 数据信号量
sem_t pspace_sem_; // 空间信号量
pthread_mutex_t c_mutex_;
pthread_mutex_t p_mutex_;
public:
void Push(const T& item) {
sem_wait(&pspace_sem_); // P(空间)
pthread_mutex_lock(&p_mutex_);
ringqueue_[p_step_] = item;
p_step_ = (p_step_ + 1) % cap_;
pthread_mutex_unlock(&p_mutex_);
sem_post(&cdata_sem_); // V(数据)
}
void Pop(T* item) {
sem_wait(&cdata_sem_); // P(数据)
pthread_mutex_lock(&c_mutex_);
*item = ringqueue_[c_step_];
c_step_ = (c_step_ + 1) % cap_;
pthread_mutex_unlock(&c_mutex_);
sem_post(&pspace_sem_); // V(空间)
}
};
信号量初始化:
PV操作顺序:
锁与信号量的配合:
一个完整的线程池通常包含以下部分:
cpp复制static void* handler(void* args) {
Thread_pool* tp = static_cast<Thread_pool*>(args);
while(true) {
tp->lock();
while(tp->is_empty()) {
tp->sleep();
}
Task t = tp->pop();
tp->unlock();
t(); // 执行任务
}
return nullptr;
}
STL容器默认不是线程安全的,使用时需要注意:
cpp复制// 不安全示例
if(!queue.empty()) { // 竞态条件
Item item = queue.front();
queue.pop();
}
// 安全写法
pthread_mutex_lock(&mutex);
if(!queue.empty()) {
Item item = queue.front();
queue.pop();
}
pthread_mutex_unlock(&mutex);
智能指针的线程安全特性:
cpp复制// 安全使用示例
std::shared_ptr<Data> global_ptr;
void thread_func() {
std::shared_ptr<Data> local_ptr;
{
std::lock_guard<std::mutex> lock(global_mutex);
local_ptr = global_ptr; // 引用计数原子增加
}
// 使用local_ptr不需要锁
}
cpp复制template <typename T>
class Singleton {
volatile static T* inst;
static std::mutex lock;
public:
static T* GetInstance() {
if(inst == nullptr) { // 第一次检查
std::lock_guard<std::mutex> guard(lock);
if(inst == nullptr) { // 第二次检查
inst = new T();
}
}
return inst;
}
};
cpp复制// C++11后推荐的线程安全单例
template<typename T>
class Singleton {
public:
static T& GetInstance() {
static T instance;
return instance;
}
};