使用平台提供的同步操作是一种UB吗?

jac*_*k X 2 c++ atomic synchronous language-lawyer c++20

#include <pthread.h>     
#include <thread>
int main(){
  pthread_mutex_t mut;
  if (pthread_mutex_init(&mut, NULL) != 0){
     return 0;
  }
  int a = 0; 
  auto t1 = std::thread([&](){
     for(int c = 0; c<=10000;c++){
        pthread_mutex_lock(&mut);
        a += 1;  //#1 suppose this non-atomic object is protected by the mutex
        pthread_mutex_unlock(&mut);
     }
  });
  auto t2 = std::thread([&](){
     int r = 0;
     for(;;){
        pthread_mutex_lock(&mut);
        r = a;
        std::cout<< a <<std::endl;  //#2 suppose this non-atomic object is protected by the mutex
        pthread_mutex_unlock(&mut);
        if(r>=10000){
           return;
        }
     }
  });
  t1.join();
  t2.join();
}
Run Code Online (Sandbox Code Playgroud)

根据[intro.races] p10

如果满足以下条件,则评估 A 在评估 B 之前发生(或者等效地,B 在 A 之后发生):

  • [...]
  • A 线程间发生在 B 之前。

[种族简介] p9

评估 A 线程间发生在评估 B 之前,如果

  • A同步于,或者
  • [...]

[介绍.races] p21

如果程序的执行包含两个潜在并发冲突的操作,并且至少其中一个操作不是原子操作,并且两者都发生在另一个操作之前,则该程序的执行将包含数据争用,除了下面描述的信号处理程序的特殊情况之外。 任何此类数据竞争都会导致未定义的行为

这段代码是否使用了实现保证的功能,但它是 C++ 标准中的 UB

更新:

之前的内容无法揭示为什么这个代码是UB。更新的部分给出了我的解释:首先,#1是对非原子对象的修改,是对该对象的读取,并且它们发生在不同的线程中,当且仅当一个操作发生在另一个操作之前时,#2它们才不会发生数据竞争,简而言之,这意味着in 中的操作必须与之前发生的in同步pthread_mutex_unlock(&mut)t1pthread_mutex_lock(&mut)t2#1#2,反之亦然。

但是C++标准并没有说操作pthread_mutex_unlock(&mut)可以与操作同步pthread_mutex_lock(&mut),因此违反了[intro.races] p21,从而导致UB。

宣称

我不知道为什么这个问题被标记为“未指定、未定义与实现定义”的重复。首先,通过评论来看,这个问题有一个争论点是代码是否未定义。其次,这个问题与 UB、未指定行为和实现定义行为之间的区别无关。

Pet*_*des 5

这不是 UB。当程序在具有 pthread 工作实现的任何真实系统上执行时,并发访问int a实际上不会发生,因为 pthread 函数会阻止这种情况发生。
\n它们具有明确定义的外部可见行为,由 ISO C++ 以外的标准(特别是 POSIX)定义。在提供这些功能的所有无错误实现上,行为都是明确定义的。

\n

如果没有声明/定义,程序将格式错误,需要诊断,而不是 UB 1

\n

这里的关键概念是,当 ISO C++ 说“否则,行为未定义”时,这只意味着标准没有定义它。 它并不禁止真正的 C++ 实现定义更多情况的行为1,2

\n

标准中存在的几种创建同步的方法(包括std::mutexstd::atomicstd::thread创建和 .join)也不禁止实现定义其他创建同步的方法,例如手写的 asm 和/或系统调用。和/或定义对象上数据竞争的行为volatile,并提供针对与之配合的编译时和运行时重新排序的屏障。

\n

C++ 旨在通过特定于平台的函数甚至语言扩展进行扩展。标准明确这样说:[intro.compliance.general] /8;唯一的限制是扩展不能破坏 ISO C++ 标准所说的格式良好的程序。

\n
\n

如果我们在根本没有 pthread 的系统上讨论这个程序(因为它只提供 ISO C++ 标准规定必须提供的东西),则该程序是“格式错误的”并且可以跑;它甚至不会达到数据竞争 UB 的程度。 这显然是无趣的;我正在讨论具有正确 pthread 实现的系统的情况。(也不只是那些不执行任何操作就返回的名称的存根。)

\n
\n

就 C++ 抽象机而言,其中std::mutexstd::atomic是原始操作,您可以将 pthread 视为不透明函数的第三方库,其工作方式就好像它们使用std::mutex和/或std::atomic内部来实现pthread_mutex_lock. 实际上,您可以编写具有相似名称的函数并以这种方式调用它们来生成线程安全程序,例如 withpthread_mutex_t = std::mutexpthread_mutex_lockas m->lock()

\n

因此 pthreads 库的外部可见行为并不“特殊”;它不会做任何你在纯 ISO C++ 中做不到的事情。(至少不是在您在这里展示的最简单的互斥函数中。)

\n
\n

然而,C++ 标准并没有规定操作 pthread_mutex_unlock(&mut) 可以与操作 pthread_mutex_lock(&mut) 同步,因此违反了 [intro.races] p21,从而导致 UB。

\n
\n

如果您编写了自己的库来在线程之间创建同步(std::atomic例如,外部具有不透明类型,但内部分配数组),那么在不提及内部实现的细节的情况下记录同步并不奇怪。

\n

C++ 标准没有规定您的函数可以调用foo并且bar可以同步,或者唯一的库是标准中记录的库!如上所述,ISO C++ 明确表示实现可以提供更多的库函数。

\n
\n

实际上,对于具有 pthread 的实现,这些函数是原始操作,是std::mutexstd::thread的构建块。他们经常使用特定于实现的东西,例如asm语句或手写的汇编函数。尽管您可能很难找到每个步骤的足够正式的文档来证明事情自始至终都是正确的,但在许多实现中都有关于此类扩展如何工作的正式文档。例如,单独编写的 asm 函数与 C 和 C++ 编译器生成的代码交互的方式对于编译器开发人员来说是如此众所周知,以至于它可能并未全部在内存模型方面正式指定,因此可能存在某种程度的如果您想深入了解特定 libpthread 的内部实现,“当然可以”。

\n

std::atomic是根据主流编译器中的一流语言功能实现的,例如 GCC 和 Clang 具有内置函数,例如__atomic_load_n__atomic_exchange_n,编译器知道如何内联并理解如何根据 memory_order 参数在原子操作之前/之后优化内存访问。

\n

pthread_mutex大多数东西都可以使用 来实现std::atomic,但 pthreads 的实际实现早于 C++11 / C11。

\n

出于这个原因和其他原因,它们通常具有以 asm 手写的函数,同样的方式strlenmemcmp并且许多其他标准库函数都是实际实现的。(它们实现的行为就好像用 C 或 C++ 编写一样;我不认为 GCC 手册明确阐明了调用用 asm 编写的函数的语言律师方面,因为它远没有用术语来思考GNU C / C++ 确实为诸如asm("" ::: "memory")clobbers 之类的东西定义了规则,这些规则可用作编译器内存屏障,例如std::atomic_signal_fence(seq_cst),但实际的定义是基于更基于 asm 的内存模型,内存中的变量具有与抽象机器说他们应该这样做。)

\n

在实现所需的排序保证方面,手写的 asm 函数对优化器来说是完全不透明的,并且必须假设执行诸如mutex->lock()、atomic seq_cst load、store、atomic_thread_fence(seq_cst)read + write 所有全局可访问对象之类的操作。

\n

请参阅互斥锁定和解锁函数如何防止 CPU 重新排序?为什么非内联就足够了。如果您pthread_mutex_lock在标头中定义了它可以内联,那么您可以使用__atomic/std::atomic或 GNU C 内联asm语句中的足够排序来编写它,使用低级实现定义的行为来实现与如何兼容的必要高级排序此实现一般处理原子和内存排序。

\n

使用内联或独立 asm 的任何给定目标 ISA 的内部实现将根据硬件内存顺序规则在该 ISA 上创建同步,可能使用 asm 栅栏指令或获取加载/释放存储,至少与ISO C++ 要求。如果 ISA 没有办法只执行acquireRMW 和release存储,则可能会更强,例如 x86,其中任何原子 RMW 都必须是完整的屏障(lock cmpxchg例如)。结合实现为 asm 定义的规则,这可以创建高级排序。

\n
\n

脚注 1:缺少的定义不是 UB,而是格式不正确的程序

\n

http://eel.is/c++draft/intro.compliance#general-1

\n
\n

可诊断规则集由本文档中的所有句法和语义规则组成,但包含明确表示法\xe2\x80\x9cno 诊断需要\xe2\x80\x9d 或被描述为导致\xe2\x80 的规则除外\x9c 未定义的行为\xe2\x80\x9d。

\n
\n

http://eel.is/c++draft/basic.lookup#general-1

\n
\n

除非另有说明,否则如果未找到声明,则程序格式错误。

\n
\n

因此,调用未定义的函数需要警告或错误。我想如果一个实现想要的话,它可以在发出警告后“尝试”运行一个格式不正确的程序,而不能保证行为?但我们已经脱离了 C++ 领域。要悄悄地破坏此代码,实现必须提供pthread_mutex_lock实际上并未执行锁定的定义。

\n

(或者pthread_mutex_lock与运行程序的核心不兼容std::thread?它仅由 POSIX 保证适用于由 启动的线程pthread_create,并且我猜想假设有一个系统可以在其中创建一个线程方法在一组核心上运行线程,需要一种 asm 指令才能正确锁定,这不适用于使用不同接口创建的线程。)

\n

http://eel.is/c++draft/intro.defs#defns.undefined - UB 的定义

\n
\n

本文档没有提出要求的行为

\n

[注 1:\xe2\x80\x82 当本文档省略任何明确的行为定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的未定义行为包括完全忽略结果不可预测的情况,到在翻译或程序执行期间以环境特有的记录方式进行行为(无论是否发出诊断消息([defns.diagnostic])),到终止翻译或执行(发出诊断消息)。许多错误的程序构造不会产生未定义的行为;他们需要接受诊断。常量表达式 ([expr.const]) 的求值永远不会表现出在 [intro] 到 [cpp] 中明确指定为未定义的行为。\xe2\x80\x94 尾注]

\n
\n

因此,在根本没有这些函数的系统上使用这些函数并不是 UB,它只是一个格式错误的程序。

\n

另请注意定义如何显式授予实现定义它们想要定义的任何行为的权限,ISO C++ 未定义该行为,如下一个脚注所示。 以记录的方式意味着这包括适合在这些实现上用于生产的东西。

\n

脚注 2:定义更多行为

\n

例如,GCC 语言扩展允许typedef uint32_t aliasing_u32 __attribute__((aligned(1),may_alias))您解引用aliasing_u32*任何对象的指向,而不考虑正常的别名规则,就像使用unsigned char. 也不考虑正常的对齐规则。

\n

另一个例子是使用 进行编译g++ -fwrapvg++ -fwrapv是一个 C++ 实现,定义有符号整数溢出的行为(作为 2 的补码环绕)。不仅仅是你从 UB 得到的结果,而是真正定义良好的结果,因此优化器必须尊重它。并且-fwrapv -fsanitize=undefined不会检查有符号整数溢出。

\n

如果使用仅定义 ISO C++ 要求定义的最低限度的实现来编译此类代码,则此类代码将或可能会遇到未定义的行为,而不是在它们所编写的实现上。

\n

ISO C++ 否决了实现定义各种操作行为的能力,包括内联汇编、单独编译的汇编以及 GNU C 等语言扩展__atomic

\n

  • @xmh0511:在我看来,你正在寻找不存在的问题,每次有人说任何东西是 UB 时,你就认为他们同意你的观点,即标准措辞中存在某种问题。[intro.compliance.general] /8 明确允许实现提供可以执行任何操作的库,包括同步。作为 C++ 实现的一部分提供的库允许具有仅在该目标系统上工作的内部结构,这就是 C++ 抽象机与外部世界交互的方式。 (3认同)
  • 请参阅此[评论](/sf/ask/5445870041/?noredirect=1#comment137153163_77798143):*如果它们不是用 C++ 编写的 - 它们是一个扩展,并且由于标准没有记录它们,因此它们是 UB。* 据推测,`pthread*` 是用 C 或 ASM 编写的,但不是 C++。实现当然可以定义C++中指定为UB的行为。 (2认同)