在多线程编程中,std::thread
、std::mutex
和信号量(例如在这个上下文中提到的Semaphore::Ptr
)是三种常用的并发控制机制,它们各自扮演着不同的角色。下面是它们之间关系的解释:
std::thread
std::thread
是C++11引入的一个库类,用于管理线程。它允许程序员创建和控制线程的执行,每个std::thread
对象都代表一个单独的执行线程。通过std::thread
,你可以指定线程开始时执行的函数或者可调用对象。
std::mutex
std::mutex
(互斥量)是一种基本的同步原语,用于控制对共享资源的访问。在多线程环境中,当多个线程需要访问同一资源(如共享内存区)时,std::mutex
可以保证在任意时刻只有一个线程可以访问该资源。通过锁定(lock)和解锁(unlock)互斥量,它帮助避免了数据竞争和条件竞争等问题。
Semaphore::Ptr
- 信号量(Semaphore)是另一种同步机制,用于控制对资源的访问数量。不同于互斥量在任意时刻只允许一个线程访问资源,信号量允许多个线程根据信号量当前的计数值访问共享资源。信号量的计数值表示了可用资源的数量。线程通过增加(signal或release)或减少(wait或acquire)信号量的计数来获取或释放资源。
- 在给出的上下文中,
Semaphore::Ptr
可能是一个信号量对象的智能指针,表示该对象是通过某种智能指针(如std::shared_ptr
)管理的,这有助于自动管理信号量对象的生命周期。
它们之间的关系
- 协作关系:在多线程程序中,
std::thread
用于创建和管理线程,而std::mutex
和Semaphore::Ptr
则用于线程之间的同步和互斥,确保线程安全地访问共享资源。 - 使用场景:
std::mutex
通常用于保护数据结构或代码段,防止同时由多个线程修改。信号量(如Semaphore::Ptr
所示)则用于限制对资源的并发访问数量,它可以是任意正数,非常适合管理有限数量的资源,如连接池、任务队列等。
目标
创建两个线程:一个生产者(producer)和一个消费者(consumer)。生产者生成数据并存储到共享缓冲区,消费者从共享缓冲区读取数据。我们将使用互斥量保护共享数据以避免竞态条件,并使用信号量来同步生产者和消费者的操作。
实现自定义信号量
首先,我们需要一个信号量类。C++标准库没有直接提供信号量,但我们可以利用std::mutex
和std::condition_variable
来模拟它:
class Semaphore {
public:
// 构造函数初始化信号量的计数器count_。参数count默认为0,意味着信号量在创建时没有资源可用。这个计数器表示信号量当前可用的资源数。
Semaphore(int count = 0) : count_(count) {}
//函数用于增加信号量的计数器,表示释放或添加一个资源
void notify() {
//函数首先通过std::mutex创建一个std::unique_lock对象lock,确保对count_修改的操作是线程安全的。std::unique_lock在构造时自动锁定互斥量,并在析构时自动解锁,提供了一种方便的RAII(资源获取即初始化)风格的同步方法。
std::unique_lock<std::mutex> lock(mutex_);
//++count_操作将信号量的计数值增加1。
++count_;
//调用std::condition_variable的notify_one方法,唤醒一个等待(wait())在该条件变量上的线程。如果有多个线程在等待,只有一个线程会被随机选择并唤醒。
cv_.notify_one();
}
//用于减少信号量的计数器,表示获取一个资源。如果没有可用资源(即count_为0),调用此函数的线程将阻塞,直到count_变为非零。
void wait() {
std::unique_lock<std::mutex> lock(mutex_);
while (count_ == 0) {
//cv_.wait(lock)将会原子性地解锁互斥量并阻塞当前线程
cv_.wait(lock);
}
//操作将信号量的计数值减少1,表示一个资源已经被成功获取。
--count_;
}
private:
std::mutex mutex_;
std::condition_variable cv_;
int count_;
};
示例代码
实现生产者-消费者模型:
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <vector>
#include <thread>
int shared_data = 0;
std::mutex mtx;
//这个Semaphore类提供了一个基本的信号量实现,利用互斥量(std::mutex)和条件变量(std::condition_variable)实现线程之间的同步。通过notify()和wait()函数的调用,多个线程可以安全地共享资源,
//而不会发生数据竞争。这种信号量实现特别适用于需要控制对数量有限的资源访问的场景。
class Semaphore {
public:
// 构造函数初始化信号量的计数器count_。参数count默认为0,意味着信号量在创建时没有资源可用。这个计数器表示信号量当前可用的资源数。
Semaphore(int count = 0) : count_(count) {}
//函数用于增加信号量的计数器,表示释放或添加一个资源
void notify() {
//函数首先通过std::mutex创建一个std::unique_lock对象lock,确保对count_修改的操作是线程安全的。std::unique_lock在构造时自动锁定互斥量,并在析构时自动解锁,提供了一种方便的RAII(资源获取即初始化)风格的同步方法。
std::unique_lock<std::mutex> lock(mutex_);
//++count_操作将信号量的计数值增加1。
++count_;
//调用std::condition_variable的notify_one方法,唤醒一个等待(wait())在该条件变量上的线程。如果有多个线程在等待,只有一个线程会被随机选择并唤醒。
cv_.notify_one();
}
//用于减少信号量的计数器,表示获取一个资源。如果没有可用资源(即count_为0),调用此函数的线程将阻塞,直到count_变为非零。
void wait() {
std::unique_lock<std::mutex> lock(mutex_);
while (count_ == 0) {
//cv_.wait(lock)将会原子性地解锁互斥量并阻塞当前线程
cv_.wait(lock);
}
//操作将信号量的计数值减少1,表示一个资源已经被成功获取。
--count_;
}
private:
std::mutex mutex_;
std::condition_variable cv_;
int count_;
};
// empty信号量用于控制生产者可以生产的数据数量,避免过度生产导致资源浪费或溢出。full信号量则用于指示消费者可消费的数据数量,确保消费者不会在没有数据可用时尝试消费。
// Semaphore& empty:一个引用传递的Semaphore对象,代表空闲资源的数量。在生产者视角中,它表示可以用于生产的空间数量。
// Semaphore& full:另一个引用传递的Semaphore对象,代表已填充资源的数量。在生产者视角中,它表示已经生产但尚未被消费的产品数量。
void producer(Semaphore& empty, Semaphore& full) {
for (int i = 0; i < 10; ++i) {
empty.wait(); //等待空闲资源
std::unique_lock<std::mutex> lock(mtx);
//生产数据
shared_data = i;
//显示生产者在生产什么
std::cout << "produced:" << shared_data << std::endl;
// 生产完毕后,生产者通过调用full.notify()来增加full信号量的计数,表示新增了一个可供消费的数据项。这一操作实际上是通知等待在full信号量上的消费者线程有新数据可用。
full.notify();
}
}
void consumer(Semaphore& empty, Semaphore& full) {
for (int i = 0; i < 10; i++) {
full.wait(); //等待数据可用
// lock: 是一个std::unique_lock<std::mutex>类型的对象实例,通过构造时传入的mtx互斥锁,自动对mtx进行锁定。lock对象的生命周期管理着mtx的锁定状态,当lock对象被销毁(即离开作用域时),它会自动解锁mtx。
std::unique_lock<std::mutex> lock(mtx);
//消费数据
std::cout << "Consumed:" << shared_data << std::endl;
empty.notify(); //通知生产者有空闲资源
}
}
int main() {
Semaphore empty(1); //初始时有一个空闲资源
Semaphore full(0); //初始时没有数据可用
std::thread producerThread(producer, std::ref(empty),std::ref(full));
std::thread consumereThread(consumer, std::ref(empty),std::ref(full));
producerThread.join();
consumereThread.join();
}
运行结果
produced:0
Consumed:0
produced:1
Consumed:1
produced:2
Consumed:2
produced:3
Consumed:3
produced:4
Consumed:4
produced:5
Consumed:5
produced:6
Consumed:6
produced:7
Consumed:7
produced:8
Consumed:8
produced:9
Consumed:9