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

10

假设我有一个任务,可能会被另一个线程取消。这个任务在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

看起来它可以工作,但我没有看到任何保证。我想知道是否有更好(正确)的方法。


1
@R.. - 为什么这是“明显错误”的?当然,如果没有查看完整代码,就无法确定是否需要在此处使用原子操作,但通常情况下,“取消”操作不需要除relaxed之外的任何原子或内存顺序。只需要使用“volatile”。 - RbMm
1
@phön - 你指的是哪种同步方式 - 具体的例子呢?如果一个线程读取一个变量,另一个线程写入这个变量,这并不意味着需要在此处进行同步。在cancelled使用的典型场景中 - 不需要同步。我会在自己的回答中更详细地描述这一点。看起来你在使用“同步”这个词时并不理解它的含义,以及何时需要使用它,何时不需要。 - RbMm
1
在这个具体的案例中,@phön - 互斥锁有什么用?它在这里绝对不需要。在典型的“已取消”使用情况下,在此只有两个值 - 0 和非0。当非0时跳出循环。在这种情况下,即使假设读取或写入不是原子性的 - 在另一个线程向“canceled”写入非0后,我们仍然会读取非0(当然,硬件对布尔类型的读写操作是原子性的,与RMW操作不同,但即使在这种情况下也不需要)。 - RbMm
1
@phön - 即使完全忽略存储或加载 也需要 volatile 来防止该问题,并且这里的未定义行为是什么- 您可以展示具体的例子并解释如果我们在一个线程中使用循环 do {.. } while (!cancelled);,并在另一个线程中使用 cancelled = true会导致什么具体的未定义。我甚至在抽象语言层面上都看不到任何问题。关于原子性- 我想说的是,即使我们只使用值的两个状态- 0和非0,也不需要原子性。您能否用具体的例子展示 UB 或类似问题? - RbMm
1
@curiousguy,我可以重复我已经说过的话:如果你查看C++标准,它是未定义的行为。也许现在在我们所知道的所有平台上都可以工作,也可能不行。也许在未来几十年中,我们会发明一些疯狂的CPU基础设施,你的“实际上它可以工作”的说法就不再成立了(也许这不会发生,但你懂我的意思)。使用最适合你的方法。我不在乎。我只是传播这个消息。你知道风险。使用它。或者在你的代码中务实。但不要教授错误的东西并说:“这是完美的解决方案”。我的两分钱。 - phön
显示剩余10条评论
5个回答

10
在C中,atomic_bool类型和在C++中的std::atomic<bool>类型(typedefed as std::atomic_bool)是两种不相关的类型。把std::atomic_bool传递给期望C的atomic_bool的函数是未定义的行为。它能工作的原因是这些类型的简单定义是兼容的,加上一点运气。
如果C++代码需要调用一个期望C的atomic_bool的函数,那么必须使用这个类型。然而,在C ++中,<stdatomic.h>头文件不存在。您需要提供一种方法,让C++代码调用C代码以获取指向所需原子变量的指针,并隐藏该类型的方法。(可能声明一个包含原子布尔值的结构体,C++只知道该类型存在并且只知道指向它的指针。)

1
原子类型在这两种语言之间是完全互操作的,因此可以开发程序,在语言边界上共享原子类型的对象。 - Manny_Mar
1
@ALX23z:“虽然它们不同,但通过暴力转换从一个转换到另一个没有问题。不存在未定义的行为。” 真的吗? - Lightness Races in Orbit
4
“从 CPU 角度来看”并不重要。这并不是未定义行为的工作方式。 - user2357112
1
@curiousguy 但是您首先必须达到“编译”和“链接”阶段。违反严格别名规则可能会让编译器做出意想不到的事情,因为您在欺骗它! - Lightness Races in Orbit
1
@curiousguy 这与ABI、代码生成或任何相关的东西无关。这是关于您与C++编译器所达成的合约。您编写了一个具有未定义行为的程序,就是这样。 - Lightness Races in Orbit
显示剩余15条评论

9
为了避免所有ABI问题,您可能希望实现一个从C++调用的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函数。


谢谢,这是一个明智的解决方案,但在我的特定情况下,“do_task”可能会从多个线程中调用,因此使用全局变量不起作用。我想我会像@1201ProgramAlarm建议的那样将其包装在结构体中。 - grepcake

1
我在网上搜索到了这个https://developers.redhat.com/blog/2016/01/14/toward-a-better-use-of-c11-atomics-part-1/
引领C++的脚步,C11标准采用了一种内存模型来描述多线程程序的要求和语义,并将一组原子类型和操作提案纳入语言中。这一变化使得编写可移植的多线程软件成为可能,可以无数据竞争地高效地操作对象。原子类型在两种语言之间完全可互操作,因此可以开发出跨语言边界共享原子类型对象的程序。本文考察了设计的一些权衡,指出了一些缺点,并概述了简化两种语言中使用原子对象的解决方案。
我现在才开始学习原子操作,但看起来它与C和CPP兼容。
编辑

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


1
你有比“某个人的博客说可以”更权威的来源吗? - user2357112
@user2357112,我在阅读资料时在stackoverflow上找到了另一个来源。我会编辑我的帖子。 - Manny_Mar

-2

我对你的代码的理解是(必须是)下一个

// 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-Modify-Write操作。比如说,如果我们需要将cancelled设置为true并检查它是否已经是true了,那么这可能是必要的,例如如果有几个线程都将cancelled设置为true,而首先完成此操作的线程需要释放一些资源。

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

  • 类型的读或写操作需要是原子的。当然,在当前和所有可能的未来实现中,对于bool类型,这是真实的(尽管没有正式定义)。但即使如此也不重要。我们在这里只检查cancelled的两个值 - 0(false)和所有其他的。因此,即使在cancelled上进行的读写操作都不是原子的,当一个线程将非零值写入canceled之后,另一个线程迟早会从canceled中读取修改后的非零值。即使它将是另一个值,而不是第一个线程写入的相同值:例如如果cancelled = true翻译成mov cancelled, -1; mov cancelled, 1 - 两个硬件操作,不是原子操作 - 第二个线程可以从canceled中读取-1而不是最终的1true),但如果我们只检查非零值-所有其他值都会中断循环- while(!*cancelled)如果我们在这里使用原子操作进行写/读cancelled - 在此处什么也不会改变 - 在一个线程原子写入它之后,另一个线程迟早会从中读取修改后的非零值-无论是原子操作还是不是 - 内存是公共的 - 如果一个线程对内存进行写入(原子或无)另一个线程迟早会查看此内存修改。

  • 我们需要将另一个读/写与cancelled同步。因此,我们需要在canceled周围的两个线程之间设置同步点,并且其中的内存顺序不是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)的视图。但我怀疑您实际上使用/需要这个
场景。

如果这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


-8

操作的原子性是由硬件而非软件引起的(好吧,在C++中也有“原子”变量,但这些仅在名称上是原子的,实际上是通过互斥锁和锁来实现的)。因此,基本上,C++原子和C原子做的事情是完全相同的。因此,只要类型兼容,就不会出现问题。并且C++11和C11原子类被设计为兼容。


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

1)我们将从基础知识开始。什么是原子操作?为什么需要使用它?内存是如何工作的?

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

为什么需要原子操作?

如果不使用原子操作,则每个处理器可能都有自己的变量“x”的版本,并且通常不会同步。无法确定它们何时会与RAM/L3缓存执行同步。

使用原子操作时,使用这样的内存操作,以确保与RAM/L3缓存(或所需的任何内容)同步——确保不同的核心可以访问相同的变量,并且不会具有各种不同的版本。

无论您使用C、C++还是其他语言-只要确保内存同步(读取、写入和修改),就不会出现任何问题。

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

Mutexes通常与操作系统一起使用,并有队列,以确定下一个要执行的线程。相比原子操作,它们强制执行更严格的内存同步。通过原子操作,可以仅同步变量本身或更多,具体取决于请求/调用哪个功能。

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

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

逻辑原子(任何T类型为错误大小/对齐的std::atomic )通过锁进行同步。在这种情况下,不太可能不同的语言可以交替使用它们-如果它们对这些锁使用不同的使用方法,或者由于某种原因一个决定使用锁,而另一个则得出结论它可以使用原子硬件内存同步......那么就会出现问题。

如果您在任何支持C/C ++的现代计算机上使用atomic_bool,则无疑可以在没有锁的情况下同步。


8
“C++11 和 C11 的原子类被设计为兼容。”你能引用一些支持这个说法的来源吗?此外,与 std::atomic_flag 不同,不能保证 std::atomic_bool 是无锁的。 - idmean
@idmean 原子布尔类型(atomic_bool)在标准中并不保证是无锁的,因为标准并没有要求它必须是无锁的。它是否无锁取决于硬件是否支持原子操作。锁也可以看作是原子操作,因为它们强制进行更严格的内存同步,而原子操作允许更松散的内存同步。 - ALX23z
它读起来像是一个新手的怒斥,因为专家告诉他们错了。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接