将 std::atomic_flag 包装在 getter/setter 中会使其“原子性”无效吗?

Posted

技术标签:

【中文标题】将 std::atomic_flag 包装在 getter/setter 中会使其“原子性”无效吗?【英文标题】:Does wrapping a std::atomic_flag in a getter/setter void its "atomicity"? 【发布时间】:2017-05-31 07:50:51 【问题描述】:

假设我有一个包含 std::atomic_flag 作为私有成员的类,通过 getter 公开。类似于以下内容(伪代码):

class Thing

private:

    std::atomic_flag ready = ATOMIC_FLAG_INIT;

public:

    isReady()
    
        return ready.test_and_set(); 
    
 

我的天真的问题是:通过方法查询标志是否会将其变成非原子操作,成为非原子函数调用(或者是?)?我应该将我的ready 标记为公共成员并直接查询它吗?

【问题讨论】:

直接访问和通过函数调用访问不会影响ready 的原子性质。你能说一下你如何使用ready,因为我怀疑这可能是真正的问题? @RichardCritten 我想(我的只是一个好奇心,不是真正的程序)另一个线程可以通过循环 isReady 来获取 Thing 的就绪状态,例如 while (!isReady()) do_something()。有意义吗? 【参考方案1】:

不,它没有。 test_and_set() 操作本身是原子的,因此不同线程的调用堆栈有多深并不重要。

为了证明这一点,请考虑atomic_flag 对象直接“暴露”的基本情况:

static atomic_flag flag = ATOMIC_FLAG_INIT;

void threadMethod() 

    bool wasFirst = !flag.test_and_set();
    if( wasFirst ) cout << "I am thread " << this_thread::get_id() << ", I was first!"      << endl;
    else           cout << "I am thread " << this_thread::get_id() << ", I'm the runner-up" << endl;

如果两个线程进入 threadMethod - 一个线程 (t1) 稍早于另一个 (t2),那么我们可以预期控制台输出如下(以相同的顺序):

I am thread t1, I was first!
I am thread t2, I'm the runner-up

现在,如果两个线程同时进入,但 t2t1 早一微秒,但 t2 在写入标准输出时变得比 t1 慢,那么输出将是:

I am thread t1, I'm the runner-up
I am thread t2, I was first!

...所以对test_and_set 的调用仍然是原子的,即使输出不一定是预期的顺序。

现在,如果您要将 flag 包装在另一种方法中(不是内联,只是为了确定),就像这样......

__declspec(noinline)
bool wrap() 
    return !flag.test_and_set();


void threadMethod() 

    bool wasFirst = wrap();
    if( wasFirst ) cout << "I am thread " << this_thread::get_id() << ", I was first!"      << endl;
    else           cout << "I am thread " << this_thread::get_id() << ", I'm the runner-up" << endl;

...那么程序的行为不会有任何不同 - 因为来自test_and_set()falsetrue 返回bool 值仍将在每个线程的堆栈中。因此,包装 atomic_flag 不会改变其原子性。

【讨论】:

【参考方案2】:

C++ atomics 的 atomicity 属性保证了一个操作不能在中间被破坏。也就是说,对于观察原子的第二个线程,它将观察test_and_set 之前的状态或test_and_set 之后的状态。 不可能这样的线程在testset 部分之间潜入clear

但是,这仅适用于操作本身。一旦test_and_set 呼叫完成,所有赌注都将再次关闭。您应该始终假设执行test_and_set 的线程可能会在完成该指令后立即被抢占,因此您不能假设在test_and_set 之后执行的任何指令仍将观察到相同的状态.

因此,添加函数调用不会给您带来麻烦。原子指令之后的任何指令都必须假设原子变量的状态可能在此期间发生了变化。 Atomics 通过提供以特殊方式设计的接口来考虑这一点:例如,test_and_set 返回测试结果,因为通过单独的调用获取该信息将不再是原子的。

【讨论】:

【参考方案3】:

不,isReady() 方法的工作方式与直接调用 test_and_set() 完全相同,即原子方式。

【讨论】:

以上是关于将 std::atomic_flag 包装在 getter/setter 中会使其“原子性”无效吗?的主要内容,如果未能解决你的问题,请参考以下文章

std::atomic_flag 停止多个线程

std::atomic_flag 初始化结果

第31课 std::atomic原子变量

在没有 XCHG 的情况下实现自旋锁?

将 youtube 视频包装在静态图像“框架”中并保持响应式调整大小

使用 Ionic 包装 Angular2 应用程序 - 地理定位问题