C ++ 0x没有信号量?如何同步线程?


135

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首先锁定互斥锁(鉴于同一线程应该锁定和解锁互斥锁,因此您也不能在thread0和thread1启动之前锁定event1)。

因此,由于boost也没有信号量,实现以上目标的最简单方法是什么?


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

Answers:


180

您可以通过互斥量和条件变量轻松构建一个:

#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;
    }
};

96
有人应该向标准

7
最初让我感到困惑的一条评论是等待中的锁,有人可能会问,如果锁是由等待持有的,那么线程如何能够过去通知?记录得不太清楚的答案是condition_variable.wait触发了该锁,从而允许另一个线程以原子方式通过通知,至少我是这样理解的

31
有人故意从加速排除的基础上,一个信号是程序员上吊太多的绳子。条件变量据说更易于管理。我明白他们的观点,但感到有点光顾。我假设相同的逻辑适用于C ++ 11 -程序员应以“自然”使用condvars或其他批准的同步技术的方式编写程序。提供信号量将与之相反,无论它是在condvar之上还是本机实现。
史蒂夫·杰索普

5
注意-有关循环背后的原理,请参见en.wikipedia.org/wiki/Spurious_wakeupwhile(!count_)
Dan Nissenbaum 2012年

3
@Maxim对不起,我认为您是对的。sem_wait和sem_post也仅在争用时进行系统调用(请检查sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c),因此此处的代码最终会复制libc实现,并可能存在错误。如果要在任何系统上实现可移植性,则可以采用解决方案,但如果仅需要Posix兼容性,请使用Posix信号灯。
xryl669 2014年

107

基于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;
};

34
您可以将wait()也设置为三层:cv.wait(lck, [this]() { return count > 0; });
Domi 2013年

2
本着lock_guard的精神添加另一个类也很有帮助。以RAII方式,将信号量作为参考的构造函数将调用信号量的wait()调用,而析构函数将调用其notify()调用。这样可以防止异常无法释放信号量。
Jim Hunziker 2014年

没有死锁,如果说N个线程叫做wait()并且count == 0,则cv.notify_one(); 因为mtx尚未发布,所以永远不会调用?
Marcello

1
@Marcello等待的线程不持有锁。条件变量的全部目的是提供原子的“解锁并等待”操作。
David Schwartz

3
您应该在调用notify_one()之前释放锁定,以避免立即阻止唤醒...请参阅此处:en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

我决定尽我所能以标准样式编写最健壮/通用的C ++ 11信号量(请注意using semaphore = ...,您通常会使用semaphorestringnot 相似的名称basic_string):

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_forwait_until方法调用返回布尔值(不是`std :: cv_status)。
jdknight

抱歉,在比赛这么晚才采取行动。std::size_t是无符号的,因此将其递减到零以下是UB,它将始终是>= 0。恕我直言,count应该是int
理查德·霍奇斯

3
@RichardHodges无法将值减小到零以下,所以没有问题,对信号量进行负计数意味着什么?IMO甚至都没有道理。
大卫

1
@David如果某个线程不得不等待其他人来初始化该怎么办?例如,一个读取器线程要等待4个线程,我将使用-3调用信号量构造函数,以使读取器线程等待,直到所有其他线程都发帖为止。我想还有其他方法可以做到,但这不合理吗?我认为这实际上是OP提出的问题,但带有更多的“ thread1”。
jmmut

2
@RichardHodges非常学究,将无符号整数类型减至0以下不是UB。
jcai

15

根据posix信号量,我将添加

class semaphore
{
    ...
    bool trywait()
    {
        boost::mutex::scoped_lock lock(mutex_);
        if(count_)
        {
            --count_;
            return true;
        }
        else
        {
            return false;
        }
    }
};

而且我更喜欢在方便的抽象级别上使用同步机制,而不是始终使用更多基本运算符来复制粘贴粘贴在一起的版本。


9

您还可以签出多核cpp11-它具有可移植且最佳的信号量实现。

该存储库还包含其他补充c ++ 11线程的线程功能。


8

您可以使用互斥和条件变量。您可以使用互斥锁获得独占访问权,请检查您是否要继续还是需要等待另一端。如果您需要等待,则可以在某种情况下等待。当另一个线程确定您可以继续时,它表明情况。

boost :: thread库中有一个简短的示例,您很可能只需复制即可(C ++ 0x和boost线程库非常相似)。


条件信号仅发送给正在等待的线程吗?那么,如果在线程1发出信号时线程0不在那里等待,它将在以后被阻塞吗?另外:我不需要条件附带的附加锁-这是开销。
tauran 2011年

是的,条件仅表示等待线程。常见的模式是在状态和条件中包含变量,以防您需要等待。考虑生产者/消费者,缓冲区中的项目将有一个计数,生产者锁定,添加元素,增加计数和信号。使用者锁定,检查计数器,如果消耗非零,则在条件中是否等待零。
DavidRodríguez-dribeas 2011年

2
您可以通过以下方式模拟信号量:使用您将给信号量的值初始化变量,然后wait()将其转换为“锁定,检查计数是否非零递减并继续;如果零等待条件,post则将变为”锁定,增量计数器,信号是否为0“
大卫·罗德里格斯-dribeas 2011年

是的,听起来不错。我想知道posix信号量是否以相同的方式实现。
tauran 2011年

@tauran:我不确定(这可能取决于哪个Posix OS),但我认为不太可能。传统上,信号量比互斥量和条件变量是“较低级别”的同步原语,并且从原理上讲,信号量可以比在condvar上实现的情况下更有效。因此,在给定的OS中,更有可能的是,所有用户级同步原语都建立在与调度程序交互的一些常用工具之上。
史蒂夫·杰索普

3

也可以在线程中有用的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();

3

C ++ 20最终将有信号量- std::counting_semaphore<max_count>

这些将具有(至少)以下方法:

  • acquire() (阻止)
  • try_acquire() (非阻塞,立即返回)
  • try_acquire_for() (非阻塞,需要一段时间)
  • try_acquire_until() (非阻塞,需要一段时间才能停止尝试)
  • release()

它尚未在cppreference上列出,但是您可以阅读这些CppCon 2019演示幻灯片或观看视频。还有官方建议书P0514R4,但我不确定这是最新版本。


2

我发现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;

有一个启用锁定的功能,以及另一个检查主机是否被锁定的功能:

UpdateLock::ptr LockUpdate( bool exclusive );       
bool IsUpdateLocked( bool exclusive ) const;

我测试LockUpdate,IsUpdateLocked中的锁,并定期在主机的Update例程中测试锁。测试锁就像检查weak_ptr是否过期,然后从m_locks列表中删除任何过期的内容一样简单(我仅在主机更新期间执行此操作),我可以检查列表是否为空;同时,当客户端重置挂起的shared_ptr时,我会自动解锁,当客户端自动销毁时也会发生这种情况。

总体而言,由于客户端很少需要排他性(通常仅保留用于添加和删除),因此大多数情况下,只要(!m_exclusiveLock),对LockUpdate(false)的请求(即非排他性的)都会成功。仅当(!m_exclusiveLock)和(m_locks.empty())都成功时,LockUpdate(true)才是排他性的请求。

可以添加一个队列来缓解排他锁和非排他锁之间的冲突,但是到目前为止,我还没有发生冲突,因此我打算等到添加解决方案时再去(大多数情况下,我有一个真实的测试条件)。

到目前为止,这可以很好地满足我的需求;我可以想象需要扩展它,并且可能会因扩展使用而产生一些问题,但是,这实现起来很快,并且只需要很少的自定义代码。


-4

如果有人对原子版本感兴趣,这里是实现。预期性能要优于互斥体和条件变量版本。

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};
};

4
我希望性能会差。此代码几乎使每个可能的错误。只是最明显的例子,假设wait代码必须循环几次。当它最终解除阻塞时,它将使用所有错误预测的分支之母,因为CPU的循环预测肯定会预测它将再次循环。我可以用此代码列出更多问题。
David Schwartz

1
这是另一个明显的性能杀手:wait循环旋转时将消耗CPU微执行资源。假设它与应该使用的线程位于同一物理核心中notify-它将使该线程的运行速度大大降低。
David Schwartz

1
而且,这里还有一个:在x86 CPU(当今最流行的CPU)上,compare_exchange_weak操作始终是写操作,即使操作失败(如果比较失败,它也会写回读取的相同值)。因此,假设两个内核都wait针对同一个信号量处于循环中。它们都全速写入同一高速缓存行,这会通过使内核间总线饱和来减慢其他内核的爬行速度。
David Schwartz

@DavidSchwartz很高兴看到您的评论。不确定了解“ ... CPU的循环预测...”部分。同意第二个。显然,您的第三种情况可能发生,但是与导致用户模式向内核模式切换和系统调用的互斥锁相比,内核间同步不会更糟。
杰弗里(Jeffery)

1
没有诸如无锁信号量之类的东西。释放锁的整个想法不是在不使用互斥体的情况下编写代码,而是在线程根本不会阻塞的情况下编写代码。在这种情况下,信号量的本质就是阻塞调用wait()函数的线程!
卡罗·伍德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.