集中式公平RW自旋锁

时间:2019-05-16 19:55:07

标签: c++ multithreading concurrency locking

在1991年发表的论文《 Scalable Reader-Writer Synchronization for 共享内存多处理器”(John M. Mellor-Crummey和Michael L.Scott)提供了一种基于票证锁定算法(https://www.cs.rochester.edu/u/scott/papers/1991_PPoPP_read_write.pdf)的公平RW锁定。

在伪代码中,它看起来像这样(http://www.cs.rochester.edu/research/synchronization/pseudocode/rw.html):

type counter = unsigned integer
  // layout of counter
  //  31    ...   16 15    ...     0        
  // +------------------------------+
  // | reader count | writer count  |
  // +------------------------------+

const RC_INCR = 0x10000 // to adjust reader count
const WC_INCR = 0x1     // to adjust writer count
const W_MASK  = 0xffff  // to extract writer count

// mask bit for top of each count
const WC_TOPMSK = 0x8000      
const RC_TOPMSK = 0x80000000 

type lock = record
    requests : counter := 0
    completions : counter := 0

procedure start_write (L : ^lock)
    counter prev_processes := 
        fetch_and_clear_then_add (&L->requests, WC_TOPMSK, WC_INCR)
    repeat until completions = prev_processes

procedure start_read (L : ^lock)
    counter prev_writers := 
        fetch_and_clear_then_add (&L->requests, RC_TOPMSK, RC_INCR) & W_MASK
    repeat until (completions & W_MASK) = prev_writers

procedure end_write (L: ^lock)
    clear_then_add (&L->completions, WC_TOPMSK, WC_INCR)

procedure end_read (L : ^lock)
    clear_then_add (&L->completions, RC_TOPMSK, RC_INCR)

它使用fetch_and_clear_then_add和clear_then_add原语 没有在现代硬件中实现。显然有人可以效仿他们 使用CAS-loop软件。

但是我想到使用这些的理由 原语(防止溢出)来自于 使读者和作家的计数器分开且精确。但是因为 直觉可能是错误的。我相信溢出预防逻辑 可以报​​废而不会破坏底层算法。

我试图用Google搜索类似结果,但至今未发现任何结果。也许 我丢失了一些东西。

因此,让我更详细地说明我的想法。我认为标准的fetch_add 指令在此算法下也可以正常工作。

  1. 阅读器计数包含在单词的上半部分。如果 fetch_add(0x10000)导致溢出,多余的位将静默 从单词边界掉下来。没问题。

  2. 作家数量位于世界的下半部分。如果 fetch_add(1)导致溢出,多余的位将溢出到 读者人数。哎呀。 2 ^ 16名作家中有1名冒犯了我们 规则,这会破坏读者人数。但是,等等,这真的有问题吗? 之后我们拥有什么?让我们来看看。在requests一词中, 过渡之后:

    (0xYYYY,0xFFFF)->(0xYYYY + 1,0x0000)// 0xYYYY代表未知的读者数,0xFFFF代表溢出前的作家数

与此同时,completions单词包含的值小于或 等于( 0xYYYY, 0xFFFF )。然后“罪犯”线程开始轮询 completions字直到它等于所需值为止。在它之前开始的任何读者或作家都不会在completions字词中产生任何令人困惑的值。当他们前进并解锁时 始终会达到所需的值。之后,“罪犯” 线程开始。

另一方面,以后到达的任何新线程都看不到 令人困惑的价值。作家增加下半部分 单词,读者增加上半部分。他们仍然相互命令。并且直到“罪犯”线程完成工作并解锁后才能继续。

由“犯罪者”完成的解锁完全补偿了其犯罪。它 将导致completions字中的作者数出现对称溢出,并向其中的读者数溢出1。因此,下一个到达的线程将完全按计划启动,因为它一直在期望“计算错误”的值。整个过程将顺利进行。

因此,最终结果是,在2 ^ 16个写锁中,读锁计数将偏移1。

一个简单的非正式例子就是 根本没有读取器锁。在这种情况下,带有标准fetch_add()指令的RW锁的行为将完全类似于普通的票证锁。读者数量每2 ^ 16个作家锁将稳定增长1。但 这是我们在头脑中所做的区分。在机器级别,这是 仅增加2 ^ 32个字。这没有区别 用普通的票证锁定。

现在将读者添加到图片中。我什么也想不出来 安排到达读者的时间,仅增加 这个词,可以破坏整个画面。从一个角度来看 读者的到来似乎使作家突然来了2 ^ 16 作家。从现在起,它仍然可以用作普通的票锁 观点。

现在,从读者的角度来看,情况有所不同。他们看 仅按作者数计算,不计较任何溢出 这个词的上半部。当读者到达时,它会记住 最后到来的作家,并抵制任何即将到来的作家 2 ^ 16增量。然后,它会一直等到记忆中的作者完成并 继续自己的工作。完成后,它又变得巨大了 2 ^ 16增量,因此任何可能等待的毫无怀疑和天真的作家 可以得出结论,这是一堆2 ^ 16个并发作者 现在完成。

如果我的推理有误,请纠正我。

C ++实现如下:

class shared_ticket_lock : non_copyable
{
public:
    void lock() noexcept
    {
        base_type tail = tail_.fetch_add(exclusive_step, std::memory_order_relaxed);
        for (;;) {
            base_type head = head_.load(std::memory_order_acquire);
            if (tail == head)
                break;
        }
    }

    void unlock() noexcept
    {
        base_type head = head_.load(std::memory_order_relaxed);
        head_.store(head + exclusive_step, std::memory_order_release);
    }

    void lock_shared() noexcept
    {
        base_type tail = tail_.fetch_add(shared_step, std::memory_order_relaxed);
        for (tail &= exclusive_mask;;) {
            base_type head = head_.load(std::memory_order_acquire);
            if (tail == (head & exclusive_mask))
                break;
        }
    }

    void unlock_shared() noexcept
    {
        head_.fetch_add(shared_step, std::memory_order_release);
    }

private:
    using base_type = std::uint32_t;
    static constexpr base_type shared_step = 1 << 16;
    static constexpr base_type exclusive_mask = shared_step - 1;
    static constexpr base_type exclusive_step = 1;

    std::atomic<base_type> head_ = ATOMIC_VAR_INIT(0);
    std::atomic<base_type> tail_ = ATOMIC_VAR_INIT(0);
};

注意:原始论文指出,可以使用普通的fetch_add实现公平的RW锁定,但是代码会稍微复杂一些。目前尚不清楚作者的意思。我猜有些不同(例如用单独的单词维护读者和作家的计数器),因为据我所知,这绝对不复杂。

更新:可以在这里找到工作代码和简单测试:

0 个答案:

没有答案