【问题标题】:Doing saturation aritmetic with atomics用原子做饱和算术
【发布时间】:2017-08-04 10:50:53
【问题描述】:

我一直在寻找一种方法来保护整数以原子方式递增使用绑定检查。 我四处寻找其他帖子,但似乎没有一个好的解决方案(有些是 C++11 之前的版本)。

我需要一个类似如下的库:

class bounded_atomic_uint
{
    private:
    uint32_t ctr;
    uint32_t max;
    mutex mtx;

    public:
    bounded_atomic_uint(uint32_t max = UINT32_MAX) : ctr(0), max(max) {}
    ~bounded_atomic_uint() = default;
    // make in uncopyable and un-movable
    bounded_atomic_uint(bounded_atomic_uint&&) = delete;
    bounded_atomic_uint& operator=(bounded_atomic_uint&&) = delete;

    bool inc();
    bool dec();
    uint32_t get();
};

bool bounded_atomic_uint::inc() {
    lock_guard<mutex> lck (mtx);
    if (ctr < max) {
        ctr++;
        return true;
    }
    else
    {
        cout << "max reached (" << max << ")" << endl; // to be removed!
        return false; // reached max value
    }
}

bool bounded_atomic_uint::dec() {
    lock_guard<mutex> lck (mtx);
    if (ctr > 0) {
        ctr--;
        return true;
    }
    else {
        cout << "min reached (0)" << endl; // to be removed!
        return false; // reached min value
    }
}

uint32_t bounded_atomic_uint::get() {
    lock_guard<mutex> lck (mtx);
    return ctr;
}

使用如下:

#include <iostream>
#include <mutex>
#include <cstdint>

using namespace std;

int main() {

    bounded_atomic_uint val(3);

    if (val.dec())
        cout << "error: dec from 0 succeeded !!" << endl;
    cout << val.get() << endl; // make sure it prints 0
    val.inc();
    val.inc();
    cout << val.get() << endl;
    if (!val.inc())
        cout << "error: havent reached max but failed!!" << endl;

    if (val.inc())
        cout << "error max not detected!!" << endl;

    cout << val.get() << endl;

    return 0;
}

有没有更简单(或更正确)的方法? std::atomic 和 boost::atomic 似乎都没有办法避免越界检查(在锁内)。

如果不是,这个 simplistic 类是否足够好? 还是我在这里遗漏了什么?

注意库上的 couts 在实际使用时将被删除!

【问题讨论】:

  • 仅供参考,您正在寻找的术语是“饱和算术”。

标签: c++ multithreading c++11 atomic


【解决方案1】:

你的问题is off-topic 的这部分是否存在图书馆,所以让我来回答一下我们如何实现这个问题,这是一个主题且非常有趣的问题.

让我们首先从您的示例中移除锁并将普通的ints 替换为atomics

class bounded_atomic_uint
{
    private:
    atomic<uint32_t> ctr;
    uint32_t max;

    public:
    bounded_atomic_uint(uint32_t max = UINT32_MAX) : ctr(0), max(max) {}
    ~bounded_atomic_uint() = default;
    // make in uncopyable and un-movable
    bounded_atomic_uint(bounded_atomic_uint&&) = delete;
    bounded_atomic_uint& operator=(bounded_atomic_uint&&) = delete;

    bool inc();
    bool dec();
    uint32_t get();
};

bool bounded_atomic_uint::inc() {
    if (ctr < max) {
        ctr++;
        return true;
    }
    else
    {
        cout << "max reached (" << max << ")" << endl; // to be removed!
        return false; // reached max value
    }
}

这段代码的问题在于,在边界检查和增量之间,值可能已经改变。所以你只能保证在没有争用的情况下不越界。

您可以通过确保在增量时值在两者之间没有变化来轻松解决此问题。这正是compare_exchange 提供的:

bool bounded_atomic_uint::inc() {
    while(true) {
        auto ctr_old = ctr.load();
        if (ctr_old < max) {
            if(ctr.compare_exchange_weak(ctr_old, ctr_old + 1)) {
                return true;
            }
        }
        else
        {
            cout << "max reached (" << max << ")" << endl; // to be removed!
            return false; // reached max value
        }
    }
}

现在如果计数器在边界检查和增量写入之间发生变化,compare_exchange_weak 将失败,所以我们必须重试。如果同时数量超出了界限,我们将在下一次循环迭代中检测到这一点并相应地退出。请注意,如果您忽略 compare_exchange 的虚假故障*,则只有在实际并发写入原子时才需要循环,因此此实现实际上是 lock-free

我们可以通过将原子重复加载到compare_exchange 中来稍微提高效率(请记住,compare_exchange 将原子的实际值写回第一个参数):

bool bounded_atomic_uint::inc() {
    auto ctr_old = ctr.load();
    do {
        if (ctr_old >= max) {
            cout << "max reached (" << max << ")" << endl; // to be removed!
            return false; // reached max value
        }
    } while(!ctr.compare_exchange_weak(ctr_old, ctr_old + 1));
    return true;
}

*我们可以改用compare_exchange_strong 来消除虚假故障,但由于无论如何我们都必须循环,这里实际上没有充分的理由这样做。

【讨论】:

  • 谢谢你的答案。我的理解是atomics 不能保证无锁。在实现实际上是无锁的情况下,这实际上是一个更好的解决方案(虽然更丑:P),但在原子要同步的情况下,这个解决方案至少需要两次获取锁(一个用于load(),另一个用于compare_exchange()),不是吗?
  • @Pacheco -- 在任何合理的硬件上原子操作都是无锁的。如果您担心,请使用atomic_is_lock_free 进行检查。
  • @Pacheco 首先,请注意无锁编程does not refer to a literal lock (like a mutex) 中的lock 部分。其次,我们访问原子两次的事实被我们只在必要的最短时间访问它的事实所抵消,这可以显着提高整个系统的吞吐量。它也可能产生相反的效果。唯一可以确定的方法是同时尝试并在所需的目标平台上进行测量。是的,多线程很难......
猜你喜欢
  • 2015-09-23
  • 2023-03-24
  • 1970-01-01
  • 1970-01-01
  • 2021-11-20
  • 2012-05-18
  • 2011-01-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多