普通C库中有TestAndSet(volatile int *lock)函数吗?

Is there a TestAndSet(volatile int *lock) function in a common C library?

...还是我必须自己写? (顺便说一句,我在 C 语言中工作)

我正在编写一个类似于维基百科上的实现:

volatile int lock = 0;

void Critical() {
    while (TestAndSet(&lock) == 1);
    critical section // only one process can be in this section at a time
    lock = 0 // release lock when finished with the critical section
}

但我似乎找不到预建的 TestAndSet(volatile int *lock)

它们的外观示例包括:

#define LOCKED 1
int TestAndSet(volatile int* lockPtr) {
    int oldValue;
    oldValue = *lockPtr;
    *lockPtr = LOCKED;
    return oldValue;
}

理想情况下,我想要可以在 linux 和 windows 上工作的东西。同样,我读到原子指令的执行依赖于硬件。我不确定这会起到什么作用,也不确定如何判断硬件是否支持它,然后 运行 另一种选择。

谢谢!

其他上下文信息: 我问这个问题的原因是为了开发一组用于访问数据结构的函数(例如 add() fetch() delete() 等......)几个线程正在访问它以进行修改和实时显示某些元素。

互斥: 我投票反对互斥体(如果我的理由没有根据,请纠正我)因为关键区域不是整个哈希 table,而是给定函数访问的特定成员。所以使用互斥锁会导致整个数据结构的性能瓶颈。

替代方案: 促使我查看 TestAndSet() 的原因是在数据结构中的每个元素上放置一个 "beingAccessed" 标志更有意义。该标志将由想要访问它的函数检查,如果它为假,则设置为真,然后该函数将执行它必须执行的操作,然后释放该元素而不冻结整个结构。

评论@M.M: 由于@chux 和你们都提到的原因,示例实现感觉不对。 对于busy wait,我的理解是在低层使用它来开发更高层的同步机制。请参阅我上面的编辑:互斥体。 volatile 不是为了确保原子性,而是为了确保在原子函数检查时每次访问该值时都加载该值,因为多个线程可以随时修改该变量。 我 imagine/am 希望的原子性是由作用于相关变量的函数提供的。 针对您所写内容的特定问题:您的代码说“注意:不要使用 "volatile"”,但您提供的标准函数原型是易失性的,那么非易失性标志变量是否在原子函数中转换为易失性?谢谢。

在 Linux 上(通常是所有 *nix-es)你可以使用 pthread:

#include <pthread.h>
pthread_mutex_t mutex;
void init_mutex() {
    pthread_mutex_init(&mutex, NULL);
}
void lock_mutex() {
    pthread_mutex_lock(&mutex);
}
void release_mutex() {
    pthread_mutex_unlock(&mutex);
}
void delete_mutex() {
    pthread_mutex_destroy(&mutex);
}

您当然可以获取指向 mutex 的指针并将其转换为 int*,但这是无意义的!这四个功能做:

  • init_mutex - 在程序启动时调用,甚至可以在 main 之前调用初始化资源(在“void”关键字之前用 __attribute__((constructor)) 标记。
  • delete_mutex - 调用程序完成,释放资源。
  • lock_mutex - 将锁定您的锁,不允许其他 线程 访问此功能(无法在进程之间限制它)。
  • release_mutex - 调用它让运行时知道临界区已经结束并且准备好接受下一个线程。

这些函数将建立一个等待互斥锁的线程队列。请参阅此示例(六个线程):

A -> B -> C -> D -> E -> [MUTEX] -> F (critical) -> [MUTEX END]
[F] release_mutex
A -> B -> C -> D -> [MUTEX] -> E (critical) -> [MUTEX END] -> F

我不知道 Windows 上的方法。如果您考虑 Linux 和 Windows 的通用内容,那就不好了。如果你考虑硬件,我不得不说NO!线程由内核管理(有时由 C 库管理)。

这通常通过内联汇编语言或编译器扩展来完成。例如,您可以在 Linux 和 Windows 中使用 GCC Atomic Functions。 Microsoft C 编译器可能有一些等效的东西。

C11 包含新的 atomic library that provides this functionality. See the atomic_flag_test_and_set 功能;只需将 int* 替换为 atomic_flag*

您可能只想使用 C11 threads.h 中提供的 mutex (mtx_*) 函数,而不是滚动您自己的同步。

在 C11 标准中,您的标志看起来像:

#include <stdatomic.h>

// Lock-free atomic flag, initially unset. Note: do not use "volatile"
atomic_flag lock = ATOMIC_FLAG_INIT;

还有一个标准函数:

_Bool atomic_flag_test_and_set(volatile atomic_flag *object);

执行您需要的操作。

请注意,您的示例实现无效,因为该标志可能在 lock_ptr 的读取和写入之间被另一个线程更改。我用 clang 3.7 和 gcc 5.x 进行了测试,他们将 atomic_flag_test_and_set 实现为 XCHG 汇编指令。


此外,您的 Critical 函数实现了 "spin lock",即它会最大化 CPU 不断尝试更改标志。这只是一个好主意,适用于锁只持有少数 CPU 个周期的情况。有关此主题的进一步阅读,请参阅 this thread

对于受临界区保护的代码可能进行系统调用的正常使用,最好使用操作系统工具对标志进行非忙碌等待。

C11 线程库包含一个互斥体。 As of 2015,显然没有主要的 compiler/library 组合原生支持 C11 线程,尽管正如该线程的回答,有一个 github 项目在 POSIX 线程上实现 C11 线程。

当然你可以使用历史悠久的技术#if to select a CRITICAL_SECTION in Windows,以及所有其他操作系统中的pthread library mutex。


关于volatile的使用:这对于多线程来说既不必要也不充分

不够因为它不保证原子性。一个 volatile 对象可能仍然 read/written 同时被多个线程(数据竞争,UB)。您必须使用原子对象,使用 C11 原子或特定于编译器的原子功能。

这不是 必需的 因为,从 C11 开始,语言规范包括防止编译器对原子操作重新排序的内存排序规则。您可以在 C11 标准(或 N1570)中阅读有关各种支持的内存排序模型的更多信息。

volatile sig_atomic_t 在 C99 中被推荐为有点 hack,因为 C99 标准没有任何内存栅栏的概念。然而,向前看,现在实际的原子可用了,volatile hack 应该被扔进垃圾堆。