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



假设我有一个任务可能会被另一个线程取消。该任务在一个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_bool类型和C++中的std::atomic<bool>类型(类型定义为std::atomic_bool)是两种不相关的不同类型。将std::atomic_bool传递给期望C的atomic_bool的C函数是未定义的行为。它的工作原理是运气和这些类型的简单定义兼容的结合。

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

为了回避所有ABI问题,您可能希望实现一个从C++调用并在该atomic_bool上操作的C函数。这样,你的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的代码。

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

遵循C++的指导,以及描述C11标准中多线程程序的要求和语义在语言此更改使编写可移植程序成为可能高效操作对象的多线程软件不可分割且没有数据竞赛。原子类型完全两种语言之间的互操作性,使得程序可以开发为跨语言共享原子类型的对象边界本文考察了设计中的一些权衡,指出了它的一些缺点,并概述了解决方案简化两种语言中原子对象的使用。

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

编辑

c11 中的另一个源多线程支持

我如何理解您的代码通常是(必须是)下一个

// 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为原子吗?我们这里需要原子弹吗?

三种情况下的原子需求:

  • 我们做Read-M修改-W写入操作。如果我们需要设置将cancelled设置为true,并检查它是否已经是true。例如,如果几个线程将CCD_ 16设置为true,并且首先执行此操作的线程需要释放一些资源,则可能需要这样做。

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

  • 类型的读取或写入操作需要是原子操作。当然在所有当前和未来可能的实现bool类型(尽管未定义形式)。但即便如此重要的我们在这里只检查cancelled的2个值-0(false)和所有其他。所以即使写和读操作都在取消假设不是原子,在一个线程写入非零到之后已取消,另一个线程迟早会读取修改后的非零值来自CCD_ 20。即使它将是另一个值,而不是相同的第一个线程写入:例如,如果cancelled = true转换为mov cancelled, -1; mov cancelled, 1-两个硬件,非原子操作-第二个线程可以读取-1,而不是最终的1(true)来自已取消,但如果我们只检查非零-all,则这不会起作用另一个值中断循环-while (!*cancelled);如果我们在这里使用原子操作进行写/读cancelled-这里没有任何变化-在一个线程原子写入它之后,另一个线程迟早会从取消的中读取修改后的非零值-原子操作与否-内存是常见的-如果一个线程写入内存(原子或否),另一线程迟早会查看此内存修改。

  • 我们需要将另一个读/写与cancelled同步。所以我们需要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)的视图修改。但我怀疑你是否真的使用/需要这个
场景

如果这三样东西都不需要,那么这里就不需要原子。您真正需要的是,一个线程只向cancelled写入true,另一个线程始终读取cancelled的值(而不是只执行一次并缓存结果)。通常在大多数代码的情况下,这将自动完成,但确切地说,您需要声明cancelled为volatile

然而,如果您出于某种原因需要完全原子化的(atomic_bool),因为您在这里跨越了语言的边界,您需要了解atomic_bool在两种语言中的具体实现,并且它是相同的吗(类型声明、操作(加载、存储等))。事实上CCD_ 37对于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

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


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

1) 我们将从基础知识开始。原子论是什么,为什么是?记忆是如何工作的?

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

为什么我们需要原子操作?

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

当使用原子操作时,使用这样的内存操作来确保与RAM/L3现金(或所需的任何东西)同步——确保不同的内核可以访问相同的变量,而不会有各种不同版本的变量。

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

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

互斥体倾向于与操作系统一起工作,并有一个队列,允许哪个线程下一次执行。它们比原子论更严格地执行内存同步。使用原子论,可以根据您调用的请求/函数,只同步变量本身或多个变量。

3) 假设我有atomic_bool,它能在不同的语言(C/C++11)上互换工作吗?

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

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

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

最新更新