使用互斥锁阻止从关键部分

时间:2016-06-22 23:17:50

标签: c++ multithreading

我不确定我的术语是否正确但是这里有 - 我有这个函数,多个线程用来写数据(在评论中使用伪代码来说明我想要的东西)

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

因此它基本上交换缓冲区并返回旧缓冲区(我已删除容量逻辑以缩短代码段)

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

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

2 - 当某个线程执行读取功能时,所有新写入器都必须等待,读取器将等待所有现有写入完成,然后它将执行读取逻辑,方案1可以继续。

问题部分是我不知道用于锁的障碍是什么 -

螺旋锁是浪费的,因为有很多像这样的容器,它们都需要cpu循环

我不知道如何应用std :: mutex,因为我只希望写入函数在触发读取函数时处于关键部分。将整个写入函数包含在互斥锁中会导致操作方案1不必要地减速。

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

2 个答案:

答案 0 :(得分:2)

如果您具有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_lock允许其他线程同时获得shared_lock但阻止unique_lock获得同时访问权限。当读者线程试图获得unique_lock所有shared_lock时,unique_lock将获得独占控制权。

答案 1 :(得分:-1)

您也可以使用常规互斥锁和条件变量而不是共享来执行此操作。据说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

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

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

一旦发出该条件变量的信号,就表明所有的编写器都被捕获在第一个分支中或者修改了数组。因此,读取线程现在可以安全地交换数据,然后解锁所有内容,然后返回它所需的内容。

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