C ++ 0x会没有信号量是真的吗?关于信号量的使用,在堆栈溢出方面已经存在一些问题。我一直使用它们(posix信号量)来让一个线程在另一个线程中等待某个事件:

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也没有信号量,那么实现上述目标的最简单方法是什么?

评论

也许使用条件互斥锁和std :: promise和std :: future?

#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