C 和 C++ 原子之间的互操作性

Interoperabilty between C and C++ atomics

假设我有一个任务可能会被另一个线程取消。该任务在 C 函数中执行,另一个线程运行 C++ 代码。我该怎么做?

粗略示例。

C:

void do_task(atomic_bool const *cancelled);

C++:

std::atomic_bool cancelled;
…
do_task(&cancelled);

现在,我创建了一个包含以下内容的文件 atomics.h

#ifdef __cplusplus
#include <atomic>
using std::atomic_bool;
#else
#include <stdatomic.h>
#endif

它似乎有效,但我看不到任何保证。请问有没有更好(正确)的方法

操作的原子性是由硬件而不是软件引起的(好吧,在 C++ 中也有 "atomic" 变量只是名义上的原子性,它们是通过互斥锁和锁实现的)。所以,基本上,C++ 原子和 C 原子做同样的事情。因此,只要类型兼容,就不会有问题。并且 C++11 和 C11 原子 类 兼容。


显然,人们不理解原子和锁是如何工作的,需要进一步解释。查看当前内存模型以获取更多信息。

1) 我们将从基础开始。什么是原子,为什么是原子?内存是如何工作的?

内存模型:将处理器视为几个独立的内核,每个内核都有自己的内存(缓存 L1、L2 和 L3;事实上,L3 缓存很常见,但并不重要)。

为什么需要原子操作?

如果您不使用原子,那么每个处理器可能都有自己的变量版本 'x',并且它们通常不同步。不知道他们什么时候会用 RAM/L3 现金进行同步。

当使用原子操作时,使用这样的内存操作来确保与 RAM/L3 现金(或任何需要的)同步 - 确保不同的内核可以访问相同的变量并且没有各种不同的版本其中。

没有人关心它是 C、C++ 还是您使用的任何语言 - 只要确保内存同步(读、写和修改)就永远不会没有问题。

2) 好吧,锁和互斥锁呢?

互斥体倾向于与 OS 一起工作,并且有一个队列,应该允许下一个线程执行。而且它们比原子执行更严格的内存同步。使用原子,可以仅同步变量本身或更多,具体取决于请求/您调用的函数。

3) 假设我有 atomic_bool,它可以在不同语言 (C/C++11) 上互换使用吗?

通常情况下,布尔值可以通过内存操作同步(从他们的角度来看,您只是在同步一个字节的内存)。 如果编译器知道硬件可以执行此类操作,那么只要您使用标准,他们肯定会使用它们。

逻辑原子(任何 std::atomic< T >,其中 T 有错误 size/alignment)通过锁同步。在这种情况下,不同的语言不太可能可以互换使用它们——如果它们对这些锁有不同的使用方法,或者出于某种原因,一种语言决定使用一种锁,而另一种语言得出的结论是它可以与原子硬件内存一起工作同步...然后就会有问题。

如果你在任何带有 C/C++ 的现代机器上使用 atomic_bool,它肯定能够无锁同步。

C中的atomic_bool类型和C++中的std::atomic<bool>类型(类型定义为std::atomic_bool)是两个不相关的不同类型。将 std::atomic_bool 传递给期望 C 的 atomic_bool 的 C 函数是未定义的行为。它能起作用是运气和这些类型的简单定义兼容的结合。

如果 C++ 代码需要调用期望 C 的 atomic_bool 的 C 函数,那么它必须使用它。但是,C++ 中的<stdatomic.h> header does not exist。您必须为 C++ 代码提供一种方法来调用 C 代码,以隐藏类型的方式获取指向您需要的原子变量的指针。 (可能声明一个包含原子 bool 的结构,C++ 只会知道该类型存在并且只知道指向它的指针。)

我在网上搜索到这个https://developers.redhat.com/blog/2016/01/14/toward-a-better-use-of-c11-atomics-part-1/

Following the lead of C++, along with a memory model describing the requirements and semantics of multithreaded programs, the C11 standard adopted a proposal for a set of atomic types and operations into the language. This change has made it possible to write portable multi-threaded software that efficiently manipulates objects indivisibly and without data races. The atomic types are fully interoperable between the two languages so that programs can be developed that share objects of atomic types across the language boundary. This paper examines some of the trade-offs of the design, points out some of its shortcomings, and outlines solutions that simplify the use of atomic objects in both languages.

我现在正在学习原子学,但看起来它在 C 和 CPP 之间是兼容的。

编辑

另一个来源Multi-Threading support in c11

对于 side-step 所有 ABI 问题,您可能希望实现一个从 C++ 调用并对其进行操作的 C 函数 atomic_bool。这样,您的 C++ 代码就不需要知道有关该全局变量及其类型的任何信息:

.h 文件中:

#ifdef __cplusplus
extern "C" {
#endif

void cancel_my_thread(void);
int is_my_thread_cancelled(void);

#ifdef __cplusplus
}
#endif

然后在 .c 文件中:

#include <stdatomic.h>

static atomic_bool cancelled = 0;

void cancel_my_thread(void) {
    atomic_store_explicit(&cancelled, 1, memory_order_relaxed);
}
int is_my_thread_cancelled(void) {
    return atomic_load_explicit(&cancelled, memory_order_relaxed);
}

C++ 代码将包含标题并调用 cancel_my_thread

我对您的代码的总体理解是(必须是)下一个

// c code

void _do_task();

void do_task(volatile bool *cancelled)
{
  do {
    _do_task();
  } while (!*cancelled);
}

// c++ code

volatile bool g_cancelled;// can be modify by another thread
do_task(&cancelled);

void some_proc()
{
  //...
  g_cancelled = true;
}

我要问问题 - 我们需要在这里声明 cancelled 为 atomic 吗?我们这里需要原子吗?

3 种情况下的原子需求:

  • 我们做Read-Modify-Write手术。说我们是否需要设置 cancelled 为真并检查它是否已经 true。例如,如果多个线程将 cancelled 设置为 true 并且首先执行此操作的人需要释放一些资源,则可能需要这样做。

    if (!cancelled.exchange(true)) { free_resources(); }

  • 类型的读取或写入操作需要是原子的。当然在 所有当前和所有可能的未来实现都是如此 bool 类型(尽管正式未定义)。但即使这样也不是 重要的。我们在这里只检查 cancelled 的 2 个值 - 0 (false) 和 都是另一个。所以即使写和读操作都在 取消假设不是原子的,在一个线程写入非零之后 取消,另一个线程迟早会读取修改后的非零值 来自 canceled 。即使它将是另一个值,也不相同 第一个线程写:例如如果 cancelled = true 翻译成 mov cancelled, -1; mov cancelled, 1 - 两个硬件,不是原子的 操作 - 第二个线程可以读取 -1 而不是最终的 1 (true) 从取消,但如果我们只检查非零 - 全部 另一个值 break loop - while (!*cancelled); 如果我们在这里使用 write/read cancelled 的原子操作 - 这里没有任何变化 - 在一个线程原子写入它之后,另一个线程迟早会读取修改后的非零来自取消的值 - 原子操作与否 - 内存是常见的 - 如果一个线程写入内存(原子或没有)另一个线程迟早会查看此内存修改。

  • 我们需要同步另一个 read/writes 已取消。所以我们需要 canceled 左右的 2 个线程与内存之间的同步点 memory_order_relaxed 以外的顺序例如下一个代码:

//

void _do_task();

int result;

void do_task(atomic_bool *cancelled)
{
    do {
        _do_task();
    } while (!g_cancelled.load(memory_order_acquire));

    switch(result)
    {
    case 1:
        //...
        break;
    }
}

void some_proc()
{
    result = 1;
    g_cancelled.store(true, memory_order_release);
}

所以我们这里不是简单的把g_cancelled设置为true,而是在这之前
写一些共享数据(result)并希望另一个线程之后 g_cancelled的视图修改,也将是
的视图修改 共享数据 (result)。但我怀疑你真的 use/need 这个
场景

if none 这 3 件事是需要的 - 你在这里不需要原子。您真正需要的是 - 一个线程只对 cancelled 写入 true 而另一个线程始终读取 cancelled 的值(而不是执行一次并缓存结果)。通常在大多数代码情况下,这将自动完成,但确切地说,您需要声明取消为 volatile

如果你出于某种原因需要完全原子的(atomic_bool),因为你在这里跨越了语言的边界,你需要理解atomic_bool在两种语言中的具体实现并且它是否相同(类型声明、操作(加载、存储等))。事实上 atomic_bool 对于 cc++ 是一样的。

或(更好)而不是使可见和共享类型 atomic_bool 使用像

这样的接口函数

bool is_canceled(void* cancelled);

所以代码可以是下一个

// c code
void _do_task();

bool is_canceled(void* cancelled);

void do_task(void *cancelled)
{
    do {
        _do_task();
    } while (!is_canceled(cancelled));
}

// c++ code

atomic_bool g_cancelled;// can be modify by another thread

bool is_canceled(void* cancelled)
{
    return *reinterpret_cast<atomic_bool*>(cancelled);
}

void some_proc()
{
    //...
    g_cancelled = true;
}

do_task(&g_cancelled);

但我再次怀疑在你的任务中你需要 atomic_bool 语义。你需要 volatile bool