如何声明和使用"一个作家,多个读者,一个过程,简单类型"变量?

jav*_*red 15 c++ multithreading low-latency

我有一个非常简单的问题.我有简单的类型变量(如int).我有一个进程,一个编写器线程,几个"只读"线程.我该如何声明变量?

  • volatile int
  • std::atomic<int>
  • int

我希望当"writer"线程修改值时,所有"读者"线程应该尽快看到新的值.

可以同时读取和写入变量,但我希望读者获得旧值或新值,而不是某些"中间"值.

我正在使用单CPU Xeon E5 v3机器.我不需要是可移植的,我只在这台服务器上运行代码,我编译-march=native -mtune=native.性能非常重要,所以除非绝对需要,否则我不想添加"同步开销".


如果我只是使用int并且一个线程写入值,那么在另一个线程中我可能暂时没有看到"新鲜"值吗?

Mik*_*our 9

只是用std::atomic.

不要使用volatile,不要使用它; 这没有给出必要的同步.在一个线程中修改它并在没有同步的情况下从另一个线程访问它将给出未定义的行为.


Nat*_*ica 5

如果您对具有一个或多个编写器的变量具有不同步的访问权限,那么您的程序具有未定义的行为.一些如何保证在写入发生时不会发生其他写入或读取.这称为同步.如何实现此同步取决于应用程序.

对于这样的事情,我们有一个编写器和几个读者,并使用TriviallyCopyable数据类型,然后一个std::atomic<>将工作.原子变量将确保只有一个线程可以同时访问变量.

如果你没有TriviallyCopyable类型或者你不想使用a std::atomic你也可以使用常规std::mutex和a std::lock_guard来控制访问

{ // enter locking scope
    std::lock_guard lock(mutx); // create lock guard which locks the mutex
    some_variable = some_value; // do work
} // end scope lock is destroyed and mutx is released
Run Code Online (Sandbox Code Playgroud)

使用这种方法时要记住的一件重要事情是,您希望保持该// do work部分尽可能短,因为当互斥锁被锁定时,其他线程无法进入该部分.

另一个选择是使用std::shared_timed_mutex(C++ 14)或std::shared_mutex(C++ 17)允许多个读者共享互斥锁,但是当你需要编写时,你仍然可以查看互斥锁并写入数据.

您不希望在此答案中使用jalf状态volatile来控制同步:

对于对共享数据的线程安全访问,我们需要保证:

  • 读/写实际发生(编译器不会仅将值存储在寄存器中,而是延迟更新主存储器直到很久以后)
  • 没有重新排序.假设我们使用volatile变量作为标志来指示是否准备好读取某些数据.在我们的代码中,我们只是在准备数据后设置标志,所以看起来都很好.但是如果指令被重新排序以便首先设置标志呢?

volatile确保第一点.它还保证在不同的volatile读/写之间不会发生重新排序.所有 volatile内存访问都将按照指定的顺序进行.这就是我们所需要的所有内容volatile:操作I/O寄存器或内存映射硬件,但它对多线程代码没有帮助,其中volatile对象通常仅用于同步对非易失性数据的访问.这些访问仍然可以相对于那些访问重新排序volatile.

与往常一样,如果您测量性能并且性能不足,那么您可以尝试不同的解决方案,但确保在更改后重新测量和比较.

最后香草萨特有极好的表现,他在做C++和2012年之后被称为原子武器是:

这是一个由两部分组成的演讲,内容涵盖C++内存模型,锁和原子和围栏如何交互并映射到硬件等等.即使我们谈论C++,其中大部分也适用于具有类似内存模型的Java和.NET,但不适用于C++的所有功能(例如轻松原子).


JCx*_*JCx 0

我有简单类型变量(如 int)。我有一个进程,一个编写器线程,几个“只读”线程。我应该如何声明变量?

易失性 int std::atomic int

使用 std::atomic 和 memory_order_relaxed 进行存储和加载

它很快,而且从你对问题的描述来看,很安全。例如

void func_fast()
{
    std::atomic<int> a; 
    a.store(1, std::memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)

编译为:

func_fast():
    movl    $1, -24(%rsp)
    ret
Run Code Online (Sandbox Code Playgroud)

这假设您不需要保证在更新整数之前看到任何其他数据被写入,因此不需要更慢且更复杂的同步。

如果你像这样天真地使用原子:

void func_slow()
{
    std::atomic<int> b;
    b = 1; 
}
Run Code Online (Sandbox Code Playgroud)

您得到的 MFENCE 指令没有 memory_order* 规范,速度要慢得多(多了 100 个周期,而裸 MOV 仅需要 1 或 2 个周期)。

func_slow():
    movl    $1, -24(%rsp)
    mfence
    ret
Run Code Online (Sandbox Code Playgroud)

参见http://goo.gl/svPpUa

(有趣的是,在 Intel 上,此代码使用 memory_order_release 和 _acquire 会产生相同的汇编语言。Intel 保证在使用标准 MOV 指令时写入和读取按顺序发生)。