void thread0(...)
{
doSomething0();
event1.wait();
...
}
void thread1(...)
{
doSomething1();
event1.post();
...
}
如果我要使用互斥锁来做到这一点:
void thread0(...)
{
doSomething0();
event1.lock(); event1.unlock();
...
}
void thread1(...)
{
event1.lock();
doSomethingth1();
event1.unlock();
...
}
问题:很丑陋,并且不能保证thread1首先锁定互斥锁(鉴于同一线程应该锁定和解锁互斥锁,因此您也不能在之前锁定event1
所以既然boost也没有信号量,那么实现上述目标的最简单方法是什么?
#1 楼
您可以通过互斥量和条件变量轻松构建一个变量:#include <mutex>
#include <condition_variable>
class semaphore
{
private:
std::mutex mutex_;
std::condition_variable condition_;
unsigned long count_ = 0; // Initialized as locked.
public:
void notify() {
std::lock_guard<decltype(mutex_)> lock(mutex_);
++count_;
condition_.notify_one();
}
void wait() {
std::unique_lock<decltype(mutex_)> lock(mutex_);
while(!count_) // Handle spurious wake-ups.
condition_.wait(lock);
--count_;
}
bool try_wait() {
std::lock_guard<decltype(mutex_)> lock(mutex_);
if(count_) {
--count_;
return true;
}
return false;
}
};
评论
有人应该向标准受委提交建议
–user90843
2012年6月6日23:36
最初让我感到困惑的一条评论是等待中的锁,有人可能会问,如果锁是等待中的话,线程如何才能通过通知?不太清楚地记录在案的答案是condition_variable.wait脉冲锁定,允许另一个线程以原子方式通过通知,至少我是这样理解的
–user90843
2012年6月14日下午6:53
有意将信号从Boost中排除是因为信号量对于程序员来说实在是太棘手了。条件变量据说更易于管理。我明白他们的意思,但感到有点光顾。我假设相同的逻辑适用于C ++ 11 -程序员应以“自然”使用condvars或其他批准的同步技术的方式编写程序。提供信号量将与之相反,而不管它是在condvar之上还是本机实现。
–史蒂夫·杰索普(Steve Jessop)
2012年8月31日15:31
注意-有关while(!count_)循环的原理,请参阅en.wikipedia.org/wiki/Spurious_wakeup。
–丹·尼森鲍姆(Dan Nissenbaum)
2012年11月16日7:49
@Maxim对不起,我认为您是对的。 sem_wait和sem_post也仅在争用时进行系统调用(请检查sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c),因此此处的代码最终会复制libc实现,并可能存在错误。如果打算在任何系统上实现可移植性,这可能是一个解决方案,但是如果仅需要Posix兼容性,请使用Posix信号量。
–xryl669
2014年8月5日15:58
#2 楼
根据Maxim Yegorushkin的回答,我尝试以C ++ 11风格制作示例。#include <mutex>
#include <condition_variable>
class Semaphore {
public:
Semaphore (int count_ = 0)
: count(count_) {}
inline void notify()
{
std::unique_lock<std::mutex> lock(mtx);
count++;
cv.notify_one();
}
inline void wait()
{
std::unique_lock<std::mutex> lock(mtx);
while(count == 0){
cv.wait(lock);
}
count--;
}
private:
std::mutex mtx;
std::condition_variable cv;
int count;
};
评论
您可以将wait()也设置为三层:cv.wait(lck,[this](){return count> 0;});
–多米
2013年12月6日13:14
本着lock_guard的精神添加另一个类也很有帮助。以RAII方式,以信号量为参考的构造函数调用信号量的wait()调用,而析构函数调用其notify()调用。这样可以防止异常无法释放信号量。
– Jim Hunziker
14-10-24在16:58
没有死锁,如果说N个线程称为wait()且count == 0,则cv.notify_one();因为mtx尚未发布,所以永远不会调用?
–马塞洛
15年5月18日在21:26
@Marcello等待的线程不持有锁。条件变量的全部目的是提供原子的“解锁并等待”操作。
– David Schwartz
15年10月19日在21:39
您应在调用notify_one()之前释放锁定,以避免立即阻止唤醒...请参阅此处:en.cppreference.com/w/cpp/thread/condition_variable/notify_all
– kylefinn
19/12/20在18:31
#3 楼
我决定尽我所能以标准的样式编写最健壮/通用的C ++ 11信号量(请注意using semaphore = ...
,您通常使用的名称semaphore
类似于通常使用string
而不是basic_string
): br /> template <typename Mutex, typename CondVar>
class basic_semaphore {
public:
using native_handle_type = typename CondVar::native_handle_type;
explicit basic_semaphore(size_t count = 0);
basic_semaphore(const basic_semaphore&) = delete;
basic_semaphore(basic_semaphore&&) = delete;
basic_semaphore& operator=(const basic_semaphore&) = delete;
basic_semaphore& operator=(basic_semaphore&&) = delete;
void notify();
void wait();
bool try_wait();
template<class Rep, class Period>
bool wait_for(const std::chrono::duration<Rep, Period>& d);
template<class Clock, class Duration>
bool wait_until(const std::chrono::time_point<Clock, Duration>& t);
native_handle_type native_handle();
private:
Mutex mMutex;
CondVar mCv;
size_t mCount;
};
using semaphore = basic_semaphore<std::mutex, std::condition_variable>;
template <typename Mutex, typename CondVar>
basic_semaphore<Mutex, CondVar>::basic_semaphore(size_t count)
: mCount{count}
{}
template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::notify() {
std::lock_guard<Mutex> lock{mMutex};
++mCount;
mCv.notify_one();
}
template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::wait() {
std::unique_lock<Mutex> lock{mMutex};
mCv.wait(lock, [&]{ return mCount > 0; });
--mCount;
}
template <typename Mutex, typename CondVar>
bool basic_semaphore<Mutex, CondVar>::try_wait() {
std::lock_guard<Mutex> lock{mMutex};
if (mCount > 0) {
--mCount;
return true;
}
return false;
}
template <typename Mutex, typename CondVar>
template<class Rep, class Period>
bool basic_semaphore<Mutex, CondVar>::wait_for(const std::chrono::duration<Rep, Period>& d) {
std::unique_lock<Mutex> lock{mMutex};
auto finished = mCv.wait_for(lock, d, [&]{ return mCount > 0; });
if (finished)
--mCount;
return finished;
}
template <typename Mutex, typename CondVar>
template<class Clock, class Duration>
bool basic_semaphore<Mutex, CondVar>::wait_until(const std::chrono::time_point<Clock, Duration>& t) {
std::unique_lock<Mutex> lock{mMutex};
auto finished = mCv.wait_until(lock, t, [&]{ return mCount > 0; });
if (finished)
--mCount;
return finished;
}
template <typename Mutex, typename CondVar>
typename basic_semaphore<Mutex, CondVar>::native_handle_type basic_semaphore<Mutex, CondVar>::native_handle() {
return mCv.native_handle();
}
评论
可以进行较小的编辑。带有谓词的wait_for和wait_until方法调用返回布尔值(不是`std :: cv_status)。
– jdknight
15年3月4日在20:17
很抱歉在比赛这么晚才挑剔。 std :: size_t是无符号的,因此将其递减到零以下是UB,它将始终> =0。恕我直言,计数应为int。
–理查德·霍奇斯(Richard Hodges)
15年11月29日在18:44
@RichardHodges无法将值减小到零以下,所以没有问题,对信号量进行负计数意味着什么? IMO甚至都没有道理。
–大卫
2015年12月1日15:39
@David如果一个线程不得不等待其他人来初始化该怎么办?例如,一个读取器线程要等待4个线程,我将用-3调用信号量构造函数,以使读取器线程等待,直到所有其他线程都发帖为止。我想还有其他方法可以做到,但这不合理吗?我认为这实际上是OP提出的问题,但带有更多的“ thread1”。
– jmmut
16年6月20日在13:48
@RichardHodges非常学究,将无符号整数类型减至0以下不是UB。
– jcai
17年6月2日在19:29
#4 楼
根据posix信号量,我会添加class semaphore
{
...
bool trywait()
{
boost::mutex::scoped_lock lock(mutex_);
if(count_)
{
--count_;
return true;
}
else
{
return false;
}
}
};
而且,我更喜欢在方便的抽象级别使用同步机制,而不是总是复制并粘贴缝合在一起版本使用更多基本运算符。
#5 楼
您还可以签出多核cpp11-它具有可移植且最佳的信号量实现。存储库还包含其他补充c ++ 11线程的线程工具。
#6 楼
您可以使用互斥量和条件变量。您可以使用互斥锁获得独占访问权,请检查是否要继续还是需要等待另一端。如果您需要等待,则可以在某种情况下等待。当另一个线程确定您可以继续时,它会发出信号。boost :: thread库中有一个简短的示例,您很可能只可以复制(C ++ 0x和boost线程库非常相似)。
评论
条件信号仅发送给正在等待的线程吗?那么,如果线程1发出信号时,线程0不在那里等待,它将在以后被阻塞吗?另外:我不需要条件附带的附加锁-这是开销。
–牛磺酸
2011-1-25在10:49
是的,条件仅表示等待线程。常见的模式是在状态和条件中包含变量,以防您需要等待。考虑生产者/消费者,缓冲区中的项目将有一个计数,生产者锁定,添加元素,增加计数和信号。使用者锁定,检查计数器,如果消耗非零,则在条件中是否等待零。
–DavidRodríguez-dribeas
2011年1月25日上午11:13
您可以通过以下方式模拟信号量:用您将给信号量的值初始化变量,然后将wait()转换为“锁定,检查计数是否为非零减数并继续;如果条件为零则等待”,同时发布为“锁定,递增计数器,如果为0,则发出信号”
–DavidRodríguez-dribeas
2011-1-25在11:17
是的,听起来不错。我想知道posix信号量是否以相同的方式实现。
–牛磺酸
2011-1-25在11:25
@tauran:我不确定(这可能取决于哪个Posix OS),但我认为不太可能。传统上,信号量比互斥量和条件变量是“低级”同步原语,并且从原理上讲,信号量可以比在condvar上实现的效率更高。因此,在给定的OS中,更有可能的是,所有用户级同步原语都建立在与调度程序交互的一些常用工具之上。
–史蒂夫·杰索普(Steve Jessop)
2012年8月31日15:28
#7 楼
C ++ 20最终有信号量-std::counting_semaphore<max_count>
。这些(至少)具有以下方法:
acquire()
(阻止)try_acquire()
(非阻塞,立即返回)try_acquire_for()
(非阻塞,需要一定时间)try_acquire_until()
(非阻塞,需要一段时间才能停止尝试)release()
尚未在cppreference上列出,但是您可以阅读这些CppCon 2019演示文稿幻灯片或观看视频。还有官方建议书P0514R4,但我不确定这是最新版本。
#8 楼
也可以在线程中有用的RAII信号量包装器:class ScopedSemaphore
{
public:
explicit ScopedSemaphore(Semaphore& sem) : m_Semaphore(sem) { m_Semaphore.Wait(); }
ScopedSemaphore(const ScopedSemaphore&) = delete;
~ScopedSemaphore() { m_Semaphore.Notify(); }
ScopedSemaphore& operator=(const ScopedSemaphore&) = delete;
private:
Semaphore& m_Semaphore;
};
多线程应用程序中的用法示例:
boost::ptr_vector<std::thread> threads;
Semaphore semaphore;
for (...)
{
...
auto t = new std::thread([..., &semaphore]
{
ScopedSemaphore scopedSemaphore(semaphore);
...
}
);
threads.push_back(t);
}
for (auto& t : threads)
t.join();
#9 楼
我发现shared_ptr和weak_ptr长了一个列表,完成了我需要的工作。我的问题是,我有几个客户想要与主机的内部数据进行交互。通常,主机自行更新数据,但是,如果客户端请求,主机需要停止更新,直到没有客户端访问主机数据为止。同时,一个客户端可以请求独占访问,这样其他客户端或主机都无法修改该主机数据。我是怎么做的,我创建了一个结构:
struct UpdateLock
{
typedef std::shared_ptr< UpdateLock > ptr;
};
每个客户端都有一个这样的成员:
UpdateLock::ptr m_myLock;
然后主机将具有一个weak_ptr成员,用于排他性和非排他锁的weak_ptrs列表:
std::weak_ptr< UpdateLock > m_exclusiveLock;
std::list< std::weak_ptr< UpdateLock > > m_locks;
有一个启用锁定的功能,以及另一个检查主机是否被锁定的功能:<我在LockUpdate,IsUpdateLocked和主机的Update例程中定期测试锁的锁定。测试锁就像检查weak_ptr是否过期,然后从m_locks列表中删除所有过期的内容一样简单(我仅在主机更新期间执行此操作),我可以检查列表是否为空。同时,当客户端重置挂起的shared_ptr时,我会自动解锁,当客户端被自动销毁时也会发生这种情况。
总体效果是,因为客户端很少需要排他性(通常仅保留给添加和删除操作),大多数情况下,只要(!m_exclusiveLock),对LockUpdate(false)的请求(即非排他性的)都会成功。仅当(!m_exclusiveLock)和(m_locks.empty())都成功时,LockUpdate(true)才是排他性的请求。
可以添加一个队列来缓解排他性和非排他性锁,但是到目前为止,我还没有碰撞,因此我打算等到碰巧添加解决方案时(主要是因为我有一个真实的测试条件)。
到目前为止,这可以很好地满足我的需求;我可以想象需要扩展它,并且可能会因扩展使用而出现一些问题,但是,这实现起来很快,并且需要很少的自定义代码。
#10 楼
如果有人对原子版本感兴趣,这里是实现。预期性能要优于互斥和条件变量版本。class semaphore_atomic
{
public:
void notify() {
count_.fetch_add(1, std::memory_order_release);
}
void wait() {
while (true) {
int count = count_.load(std::memory_order_relaxed);
if (count > 0) {
if (count_.compare_exchange_weak(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
break;
}
}
}
}
bool try_wait() {
int count = count_.load(std::memory_order_relaxed);
if (count > 0) {
if (count_.compare_exchange_strong(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
return true;
}
}
return false;
}
private:
std::atomic_int count_{0};
};
评论
我希望性能会更差。此代码几乎使每个可能的错误。只是最明显的例子,假设等待代码必须循环几次。当它最终解除阻塞时,它将使用所有错误预测的分支之母,因为CPU的循环预测肯定会预测它将再次循环。我可以用此代码列出更多问题。
– David Schwartz
17年2月13日在4:33
这是另一个明显的性能杀手:等待循环将在旋转时消耗CPU微执行资源。假设它与应该通知它的线程在同一物理核心中-它将使该线程的运行速度大大降低。
– David Schwartz
17年2月13日在4:34
这里还有一个:在x86 CPU(当今最流行的CPU)上,compare_exchange_weak操作始终是写操作,即使操作失败(如果比较失败,它也会写回读取的相同值)。因此,假设两个内核都在等待同一信号量的循环中。它们都全速写入同一高速缓存行,这会通过使内核间总线饱和来减慢其他内核的爬行速度。
– David Schwartz
17年2月13日在4:35
@DavidSchwartz很高兴看到您的评论。不确定了解“ ... CPU的循环预测...”部分。同意第二个。显然,您的第三种情况可能发生,但是与导致用户模式切换到内核模式切换和系统调用的互斥锁相比,内核间同步不会更糟。
–杰弗里(Jeffery)
17年2月13日在21:49
没有诸如无锁信号量之类的东西。释放锁的整个想法不是在不使用互斥体的情况下编写代码,而是在线程根本不会阻塞的情况下编写代码。在这种情况下,信号量的本质就是阻塞调用wait()函数的线程!
–卡洛·伍德(Carlo Wood)
18年2月21日,下午1:32
评论
也许使用条件互斥锁和std :: promise和std :: future?