【问题标题】:Using a mutex to block execution from outside the critical section使用互斥锁从临界区外部阻止执行
【发布时间】:2016-10-25 02:06:24
【问题描述】:

我不确定我的术语是否正确,但这里是 - 我有这个函数被多个线程用来写入数据(使用 cmets 中的伪代码来说明我想要什么)

//these are initiated in the constructor
int* data; 
std::atomic<size_t> size;

void write(int value) {
    //wait here while "read_lock"
    //set "write_lock" to "write_lock" + 1
    auto slot = size.fetch_add(1, std::memory_order_acquire);
    data[slot] = value;
    //set "write_lock" to "write_lock" - 1
}

写入的顺序并不重要,我只需要让每个写入都进入一个唯一的插槽

不过,每隔一段时间,我需要一个线程来使用此函数读取数据

int* read() {
    //set "read_lock" to true
    //wait here while "write_lock"
    int* ret = data;
    data = new int[capacity];
    size = 0;
    //set "read_lock" to false
    return ret;
}

所以它基本上换出缓冲区并返回旧缓冲区(我已删除容量逻辑以使 sn-ps 更短)

理论上这应该导致两种操作场景:

1 - 只是一堆线程写入容器

2 - 当某个线程执行读取函数时,所有新的写入者都必须等待,读取者将等待所有现有写入完成,然后执行读取逻辑,场景1可以继续。

问题部分是我不知道锁使用什么样的屏障-

自旋锁会很浪费,因为有很多这样的容器,它们都需要 cpu 周期

我不知道如何应用 std::mutex,因为我只希望写入函数在读取函数被触发时处于临界区。将整个写入函数包装在互斥体中会导致操作场景 1 不必要的减速。

那么这里的最佳解决方案是什么?

【问题讨论】:

  • 我想这个问题可能会有所帮助stackoverflow.com/questions/14306797/…
  • 所以您不需要在编写器线程之间同步它们,因为它们总是访问数据的唯一部分?
  • @Galik 在这种情况下,作者线程是池化的 AI、UI 和世界模拟效果,以各种频率运行,我需要将它们的输出一起传送到 GPU,GPU 以自己的更新间隔运行,但它们在运输过程中彼此无关
  • @user81993 在您的第一个代码 sn-p 中过度使用标识符 data 是否可能出现拼写错误?局部变量正在遮蔽全局变量。
  • @NirFriedman 哦,是的,哎呀。

标签: c++ multithreading


【解决方案1】:

如果您有C++14 功能,那么您可以使用std::shared_timed_mutex 来分隔读取器和写入器。在这种情况下,您似乎需要为您的编写器线程共享访问权限(同时允许其他编写器线程)和您的阅读器线程唯一访问(踢出所有其他线程)。

所以你可能需要这样的东西:

class MyClass
{
public:
    using mutex_type = std::shared_timed_mutex;
    using shared_lock = std::shared_lock<mutex_type>;
    using unique_lock = std::unique_lock<mutex_type>;

private:
    mutable mutex_type mtx;

public:

    // All updater threads can operate at the same time
    auto lock_for_updates() const
    {
        return shared_lock(mtx);
    }

    // Reader threads need to kick all the updater threads out
    auto lock_for_reading() const
    {
        return unique_lock(mtx);
    }
};

// many threads can call this
void do_writing_work(std::shared_ptr<MyClass> sptr)
{
    auto lock = sptr->lock_for_updates();

    // update the data here
}

// access the data from one thread only
void do_reading_work(std::shared_ptr<MyClass> sptr)
{
    auto lock = sptr->lock_for_reading();

    // read the data here
}

shared_locks 允许其他线程同时获得shared_lock,但阻止unique_lock 获得同时访问。当读者线程试图获得unique_lock 时,所有shared_locks 将在unique_lock 获得独占控制权之前腾出。

【讨论】:

  • 是的。 RW 锁似乎是一种方法。不过,它引发了一堆公平问题。
【解决方案2】:

您也可以使用常规互斥锁和条件变量而不是共享来执行此操作。据说shared_mutex 的开销更高,所以我不确定哪个会更快。使用 Gallik 的解决方案,您可能需要在每次 write 调用时锁定共享互斥锁;我从您的帖子中得到的印象是,write 被称为比阅读更多,所以这可能是不可取的。

int* data; // initialized somewhere
std::atomic<size_t> size = 0;
std::atomic<bool> reading = false;
std::atomic<int> num_writers = 0;
std::mutex entering;
std::mutex leaving;
std::condition_variable cv;

void write(int x) {
    ++num_writers;
    if (reading) {
        --num_writers;
        if (num_writers == 0)
        {
            std::lock_guard l(leaving);
            cv.notify_one();
        }          
        { std::lock_guard l(entering); }
        ++num_writers;
    }
    auto slot = size.fetch_add(1, std::memory_order_acquire);
    data[slot] = x;
    --num_writers;
    if (reading && num_writers == 0)
    {
        std::lock_guard l(leaving);
        cv.notify_one();
    }
}

int* read() {
    int* other_data = new int[capacity];
    {
        std::unique_lock enter_lock(entering);
        reading = true;
        std::unique_lock leave_lock(leaving);
        cv.wait(leave_lock, [] () { return num_writers == 0; });
        swap(data, other_data);
        size = 0;
        reading = false;
    }
    return other_data;
}

这有点复杂,我花了一些时间来锻炼,但我认为这应该很好地达到目的。

在只发生写入的常见情况下,读取总是错误的。所以你照常做,并支付两个额外的原子增量和两个未使用的分支。所以公共路径不需要锁定任何互斥锁,不像涉及共享互斥锁的解决方案,这应该是昂贵的:http://permalink.gmane.org/gmane.comp.lib.boost.devel/211180

现在,假设调用了 read。昂贵的、缓慢的堆分配首先发生,同时写入继续不间断。接下来,获取进入锁,没有立即生效。现在,reading 设置为 true。立即,任何新的 write 调用进入第一个分支,并最终命中他们无法获取的进入锁(因为它已经被占用了),然后这些线程进入睡眠状态。

同时,读取线程现在正在等待写入者的数量为 0。如果幸运的话,这实际上可以立即通过。但是,如果在递增和递减 num_writers 之间的两个位置中的任何一个中都有线程正在写入,则不会。每次写入线程递减num_writers 时,它都会检查它是否已将该数字减少到零,并且当它减少时,它会向条件变量发出信号。因为num_writers 是原子的,它可以防止各种重新排序的恶作剧,所以保证最后一个线程会看到num_writers == 0;也可以多次通知它,但这没关系,不会导致不良行为。

一旦该条件变量发出信号,这表明所有写入器要么被困在第一个分支中,要么已完成对数组的修改。所以读线程现在可以安全地交换数据,然后解锁所有内容,然后返回它需要的内容。

如前所述,在典型操作中没有锁,只有增量和未使用的分支。即使确实发生了读取,读取线程也会有一个锁和一个条件变量等待,而典型的写入线程将有大约一个互斥锁的锁定/解锁,仅此而已(一个或少量写入线程将也执行条件变量通知)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-10-22
    • 1970-01-01
    • 2015-10-08
    • 2013-08-07
    • 1970-01-01
    • 2021-04-16
    • 2010-10-27
    相关资源
    最近更新 更多