【问题标题】:Non-blocking semaphores in C++11?C ++ 11中的非阻塞信号量?
【发布时间】:2017-02-16 09:19:42
【问题描述】:

本网站上的一些问题涉及 C++11 中引入的多线程支持中缺少信号量对象。很多人建议implementing semaphores using mutexes or condition variables or a combination of both

然而,这些方法都不允许在保证调用线程不被阻塞的同时增加和减少信号量,因为通常必须在读取信号量的值之前获取锁。例如,POSIX 信号量具有函数sem_post()sem_trywait(),它们都是非阻塞的。

是否可以仅使用 C++11 多线程支持来实现非阻塞信号量?或者我是否必须为此使用依赖于操作系统的库?如果是这样,为什么 C++11 版本不包含信号量对象?

similar question 已 3 年未得到答复。 (注意:我相信我要问的问题要广泛得多,除了生产者/消费者之外,非阻塞信号量对象肯定还有其他用途。如果尽管如此,有人认为我的问题是重复的,那么请告诉我如何我可以重新关注旧问题,因为这仍然是一个悬而未决的问题。)

【问题讨论】:

  • 你为什么关心信号量是否阻塞?如果您因为认为非阻塞信号量比阻塞信号量表现更好而关心,那么这是一个 XY 问题。您真正想要的是具有您认为非阻塞信号量具有的性能水平的信号量,而您实际上并不关心它是如何实现的。
  • 我认为实现信号量没有问题。使用 C++11 atomics 和 mutextes 应该是可能的。我在这里找到了一个实现:stackoverflow.com/questions/4792449/…
  • @SingerOfTheFall 你对锁有一个常见的误解。锁不是“浪费时间等待”,而是允许竞争线程被取消调度,以便非竞争线程可以运行。
  • @SingerOfTheFall 不,一点也不。当其他线程使用 CPU 时,线程不是“浪费时间”。它被阻止并允许系统做有用的工作。最糟糕的情况是当您使用无锁算法时,它允许两个竞争线程同时运行,当它们争夺缓存线时,整个系统都会慢下来。线程并不处于战争状态,每个线程都只为自己工作。当竞争线程不并发运行并且争用最小化时,它们都会受益,因此它们可以快速完成并允许其他任务也可以全速完成。
  • 当使用sem_post 时,如果等待,线程会被唤醒。这意味着它必须处于阻塞状态。要求非阻塞信号量是……不寻常的。如果你跳过sem_wait,只使用sem_trywait,那么使用原子非常容易。

标签: c++ multithreading c++11


【解决方案1】:

我认为实现信号量没有问题。使用 C++11 atomics 和 mutextes 应该是可能的。

class Semaphore
{
private:
    std::atomic<int> count_;

public:
    Semaphore() :
        count_(0) // Initialized as locked.
    {

    }
    void notify() {
        count_++;
    }

    void wait() {
        while(!try_wait()) {
            //Spin Locking
        }
    }

    bool try_wait() {
        int count = count_;
        if(count) {
            return count_.compare_exchange_strong(count, count - 1);
        } else {
            return false;
        }
    }
};

下面是一个小例子:

#include <iostream>
#include "Semaphore.hpp"
#include <thread>
#include <vector>

Semaphore sem;
int counter;

void run(int threadIdx) {
    while(!sem.try_wait()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    //Alternative use wait
    //sem.wait()
    std::cout << "Thread " << threadIdx << " enter critical section" << std::endl;
    counter++;
    std::cout << "Thread " << threadIdx << " incresed counter to " << counter << std::endl;

    // Do work;
    std::this_thread::sleep_for(std::chrono::milliseconds(30));

    std::cout << "Thread " << threadIdx << " leave critical section" << std::endl;
    sem.notify();
}
int main() {
    std::vector<std::thread> threads;
    for(int i = 0; i < 15; i++) {
        threads.push_back(std::thread(run, i));
    }

    sem.notify();


    for(auto& t : threads) {
        t.join();
    }
    std::cout << "Terminate main." << std::endl;
    return 0;
}

当然,等待是一个阻塞操作。但是 notify 和 try_wait 都是非阻塞的,如果比较和交换操作是非阻塞的(可以检查)。

【讨论】:

  • 它会工作得非常非常糟糕。在典型的现代 CPU 上这种方法的许多问题中,一个线程在等待获取信号量时旋转会消耗核心微执行资源。如果资源由运行在同一物理内核中的另一个线程持有,则该关键线程将缓慢爬行。 不要这样做!请参阅我的 cmets 。
  • 感谢您再次链接到我在问题中已链接到的线程,并解释了为什么这些实现与我想要的不同。
  • @DavidSchwartz 据我了解,没有自旋锁。它使用条件变量休眠,因为资源无法分配
  • @OutOfBound 如果你的意思是这个答案,它不是非阻塞的,因为它阻塞了条件变量。非阻塞的有我指出的所有问题。
  • 问题是,在不严重影响性能的情况下,你不能让它便携地旋转。例如,正确旋转必须考虑您是否与另一个线程共享一个物理内核,以及当比较失败时原子比较和交换操作是否会弄脏缓存行。您不希望自旋线程独占稀缺的 CPU 资源并减慢 其他 线程的爬行速度。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多