文章目录
1. 前言
The GNU C Library Reference Manual for version 2.35
2. 线程
Threads
本章介绍用于管理线程的函数。GNU C 库提供了两种线程实现:ISO C 线程和 POSIX 线程。
2.1. ISO C 线程
ISO C Threads
本节描述 GNU C 库 ISO C 线程实现。要更深入地了解此 API,强烈建议阅读 ISO/IEC 9899:2011 第 7.26 节,其中最初指定了 ISO C 螺纹。所有类型和函数原型都在头文件threads.h中声明。
2.1.1. 返回值
Return Values
ISO C 线程规范为 API 中函数的返回值提供了以下枚举常量:
thrd_timedout
在未获取请求的资源(通常是互斥锁或条件变量)的情况下到达指定时间。
thrd_success
请求的操作成功。
thrd_busy
请求的操作失败,因为请求的资源已在使用中。
thrd_error
请求的操作失败。
thrd_nomem
请求的操作失败,因为它无法分配足够的内存。
2.1.2. 创建和控制
Creation and Control
GNU C 库实现了一组允许用户轻松创建和使用线程的函数。提供了附加功能来控制线程的行为。
为管理线程定义了以下数据类型:
数据类型:thrd_t
标识线程的唯一对象。
数据类型:thrd_start_t
此数据类型是 int (*) (void *) typedef,在创建新线程时传递给 thrd_create。它应该指向线程将运行的第一个函数。
以下函数用于处理线程:
函数:int thrd_create (thrd_t *thr, thrd_start_t func, void *arg)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
thrd_create 创建一个将执行函数 func 的新线程。arg 指向的对象将用作 func 的参数。如果成功,则将 thr 设置为新的线程标识符。
此函数可能返回 thrd_success、thrd_nomem 或 thrd_error。
函数:thrd_t thrd_current (void)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
该函数返回调用线程的标识符。
函数:int thrd_equal (thrd_t lhs, thrd_t rhs)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
thrd_equal 检查 lhs 和 rhs 是否引用同一个线程。如果 lhs 和 rhs 是不同的线程,这个函数返回 0;否则,返回值非零。
函数:int thrd_sleep (const struct timespec *time_point, struct timespec *remaining)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
thrd_sleep 至少会阻塞当前线程的执行,直到达到 time_point 指向的经过时间。这个函数不需要一个绝对的时间,而是线程需要被阻塞的持续时间。请参阅时间基础和时间类型。
如果接收到未被忽略的信号,线程可能会提前唤醒。在这种情况下,如果剩余不为 NULL,则剩余持续时间存储在剩余所指向的对象中。
如果 thrd_sleep 至少阻塞了 time_point 中的时间量,则返回 0,如果被信号中断,则返回 -1,或者失败时返回负数。
函数:void thrd_yield (void)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
thrd_yield 为实现提供了一个提示,以重新安排当前线程的执行,允许其他线程运行。
函数:_Noreturn void thrd_exit (int res)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
thrd_exit 终止调用线程的执行并将其结果代码设置为 res。
如果从单线程进程调用此函数,则该调用等效于使用 EXIT_SUCCESS 调用 exit(请参阅正常终止)。另请注意,从启动线程的函数返回等效于调用 thrd_exit。
函数:int thrd_detach (thrd_t thr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
thrd_detach 将 thr 标识的线程与当前控制线程分离。一旦线程退出,被分离线程持有的资源将被自动释放。任何 thr 信号都不会通知父线程。
在先前被另一个线程分离或加入的线程上调用 thrd_detach 会导致未定义的行为。
此函数返回 thrd_success 或 thrd_error。
函数:int thrd_join (thrd_t thr, int *res)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
thrd_join 阻塞当前线程,直到 thr 标识的线程完成执行。如果 res 不为 NULL,则将线程的结果代码放入 res 指向的位置。线程的终止与该函数的完成同步,这意味着两个线程在执行过程中都到达了一个共同点。
在先前被另一个线程分离或加入的线程上调用 thrd_join 会导致未定义的行为。
此函数返回 thrd_success 或 thrd_error。
2.1.3. 调用一次
Call Once
为了保证对函数的单一访问,GNU C 库实现了一次调用函数,以确保在存在多个可能调用的线程时只调用一次函数。
数据类型:once_flag
一个完整的对象类型,能够保存 call_once 使用的标志。
宏:ONCE_FLAG_INIT
该值用于初始化 once_flag 类型的对象。
函数:void call_once (once_flag *flag, void (*func) (void))
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
call_once 只调用一次函数 func,即使是从多个线程调用的。函数 func 的完成与使用相同标志变量的所有先前或后续调用 call_once 同步。
2.1.4. 互斥体
Mutexes
为了更好地控制资源以及线程如何访问它们,GNU C 库实现了一个互斥对象,它可以帮助避免竞争条件和其他并发问题。术语“互斥”是指互斥。
互斥锁的基本数据类型是 mtx_t:
数据类型:mtx_t
mtx_t 数据类型唯一标识一个互斥对象。
ISO C 标准定义了几种类型的互斥体。它们由以下符号常量表示:
mtx_plain
不支持超时或测试并返回的互斥锁。
mtx_recursive
支持递归锁定的互斥体,这意味着拥有线程可以多次锁定它而不会导致死锁。
mtx_timed
支持超时的互斥锁。
以下函数用于使用互斥锁:
函数:int mtx_init (mtx_t *mutex, int type)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
mtx_init 创建一个类型为 type 的新互斥对象。mutex 指向的对象被设置为新创建的 mutex 的标识符。
并非所有互斥类型的组合都对 type 参数有效。类型参数的互斥类型的有效用途是:
mtx_plain
不支持超时的非递归互斥锁。
mtx_timed
支持超时的非递归互斥锁。
mtx_plain | mtx_recursive
不支持超时的递归互斥锁。
mtx_timed | mtx_recursive
一个支持超时的递归互斥锁。
此函数返回 thrd_success 或 thrd_error。
函数:int mtx_lock (mtx_t *mutex)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
mtx_lock 阻塞当前线程,直到 mutex 指向的 mutex 被锁定。如果当前线程已锁定互斥锁且互斥锁不是递归的,则行为未定义。
在同一个互斥体上对 mtx_unlock 的先前调用与此操作同步(如果此操作成功),并且任何给定互斥体上的所有锁定/解锁操作形成一个单一的总顺序(类似于原子的修改顺序)。
此函数返回 thrd_success 或 thrd_error。
函数:int mtx_timedlock (mtx_t *restrict mutex, const struct timespec *restrict time_point)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
mtx_timedlock 阻塞当前线程,直到 mutex 指向的 mutex 被锁定或者直到 time_point 指向的日历时间已经到达。由于此函数需要一个绝对时间,如果需要持续时间,则必须手动计算日历时间。请参阅时间基础知识和日历时间。
如果当前线程已经锁定了互斥体并且互斥体不是递归的,或者互斥体不支持超时,则该函数的行为是未定义的。
在同一个互斥体上对 mtx_unlock 的先前调用与此操作同步(如果此操作成功),并且任何给定互斥体上的所有锁定/解锁操作形成一个单一的总顺序(类似于原子的修改顺序)。
此函数返回 thrd_success 或 thrd_error。
函数:int mtx_trylock (mtx_t *mutex)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
mtx_trylock 尝试不阻塞地锁定 mutex 指向的 mutex。如果互斥锁已被锁定,它会立即返回。
在同一个互斥体上对 mtx_unlock 的先前调用与此操作同步(如果此操作成功),并且任何给定互斥体上的所有锁定/解锁操作形成一个单一的总顺序(类似于原子的修改顺序)。
如果获得锁,此函数返回 thrd_success,如果互斥锁已被锁定,则返回 thrd_busy,失败时返回 thrd_error。
函数:int mtx_unlock (mtx_t *mutex)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
mtx_unlock 解锁互斥锁指向的互斥锁。如果互斥锁未被调用线程锁定,则行为未定义。
此函数与同一互斥体上的后续 mtx_lock、mtx_trylock 和 mtx_timedlock 调用同步。任何给定互斥体上的所有锁定/解锁操作形成一个单一的总顺序(类似于原子的修改顺序)。
此函数返回 thrd_success 或 thrd_error。
函数:void mtx_destroy (mtx_t *mutex)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
mtx_destroy 销毁互斥锁指向的互斥锁。如果有任何线程在互斥体上等待,则行为未定义。
2.1.5. 条件变量
Condition Variables
互斥锁不是唯一可用的同步机制。对于一些更复杂的任务,GNU C 库还实现了条件变量,这让程序员在解决复杂的同步问题时可以更高层次地思考。它们用于同步等待特定条件发生的线程。
条件变量的基本数据类型是 cnd_t:
数据类型:cnd_t
cnd_t 唯一标识一个条件变量对象。
以下函数用于处理条件变量:
函数:int cnd_init (cnd_t *cond)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
cnd_init 初始化一个新的条件变量,由 cond 标识。
此函数可能返回 thrd_success、thrd_nomem 或 thrd_error。
函数:int cnd_signal (cnd_t *cond)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
cnd_signal 解除阻塞当前正在等待 cond 指向的条件变量的一个线程。如果线程成功解除阻塞,则此函数返回 thrd_success。如果没有线程被阻塞,这个函数什么也不做并返回 thrd_success。否则,此函数返回 thrd_error。
函数:int cnd_broadcast (cnd_t *cond)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
cnd_broadcast 取消阻塞当前正在等待 cond 指向的条件变量的所有线程。此函数在成功时返回 thrd_success。如果没有线程被阻塞,这个函数什么也不做并返回 thrd_success。否则,此函数返回 thrd_error。
函数:int cnd_wait (cnd_t *cond, mtx_t *mutex)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
cnd_wait 以原子方式解锁 mutex 指向的 mutex 并阻塞 cond 指向的条件变量,直到线程由 cnd_signal 或 cnd_broadcast 发出信号。在函数返回之前,互斥锁再次被锁定。
此函数返回 thrd_success 或 thrd_error。
函数:int cnd_timedwait (cnd_t *restrict cond, mtx_t *restrict mutex, const struct timespec *restrict time_point)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
cnd_timedwait 原子地解锁 mutex 指向的 mutex 并阻塞 cond 指向的条件变量,直到线程由 cnd_signal 或 cnd_broadcast 发出信号,或者直到达到 time_point 指向的日历时间。在函数返回之前,互斥锁再次被锁定。
至于mtx_timedlock,由于这个函数需要一个绝对时间,如果需要一个duration,则必须手动计算日历时间。请参阅时间基础知识和日历时间。
此函数可能返回 thrd_success、thrd_nomem 或 thrd_error。
函数:void cnd_destroy (cnd_t *cond)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
cnd_destroy 销毁 cond 指向的条件变量。如果有线程在等待 cond,则行为未定义。
2.1.6. 线程本地存储
Thread-local Storage
GNU C 库实现了提供线程本地存储的函数,这是一种机制,通过该机制,变量可以定义为具有唯一的每个线程存储、与线程生命周期匹配的生命周期以及清理唯一的每个线程存储的析构函数。
存在几种用于线程本地存储的数据类型和宏:
数据类型:tss_t
tss_t 数据类型标识特定于线程的存储对象。即使共享,每个线程也会有自己的变量实例,具有不同的值。
数据类型:tss_dtor_t
tss_dtor_t 是 void (*) (void *) 类型的函数指针,用作特定于线程的存储析构函数。该函数将在当前线程调用 thrd_exit 时调用(但在调用 tss_delete 或 exit 时绝不会调用)。
宏:thread_local
thread_local 用于标记具有线程存储持续时间的变量,即线程启动时创建,线程结束时清理。
注意:对于 C++,需要 C++11 或更高版本才能使用 thread_local 关键字。
宏:TSS_DTOR_ITERATIONS
TSS_DTOR_ITERATIONS 是一个整数常量表达式,表示线程终止时所有线程局部析构函数的最大迭代次数。该值对线程本地存储的破坏提供了有限的限制;例如,考虑创建更多线程本地存储的析构函数。
以下函数用于管理线程本地存储:
函数:int tss_create (tss_t *tss_key, tss_dtor_t destructor)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
tss_create 创建一个新的线程特定的存储键并将其存储在 tss_key 指向的对象中。尽管不同的线程可以使用相同的键值,但通过 tss_set 绑定到键的值是在每个线程的基础上维护的,并在调用线程的生命周期内保持不变。
如果 destructor 不为 NULL,则将设置一个析构函数,并在线程通过调用 thrd_exit 完成其执行时调用。
如果 tss_key 成功设置为线程的唯一值,则此函数返回 thrd_success;否则,返回 thrd_error 并且 tss_key 的值未定义。
函数:int tss_set (tss_t tss_key, void *val)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
tss_set 将当前线程的 tss_key 标识的线程特定存储的值设置为 val。不同的线程可能会为同一个键设置不同的值。
此函数返回 thrd_success 或 thrd_error。
函数:void * tss_get (tss_t tss_key)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
tss_get 返回由当前线程的线程特定存储中保存的 tss_key 标识的值。不同的线程可能会得到由相同键标识的不同值。失败时,tss_get 返回零。
函数:void tss_delete (tss_t tss_key)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
tss_delete 销毁由 tss_key 标识的特定于线程的存储。
2.2. POSIX 线程
POSIX Threads
本节描述 GNU C 库 POSIX 线程实现。
2.2.1. 线程特定数据
Thread-specific Data
GNU C 库实现了允许用户创建和管理特定于线程的数据的功能。如果提供了析构函数,这些数据可能会在线程退出时被销毁。定义了以下函数:
函数:int pthread_key_create (pthread_key_t *key, void (*destructor)(void*))
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
为调用线程创建一个线程特定的数据键,由 key 引用。
使用 C++11 thread_local 关键字声明的对象在特定于线程的数据之前被销毁,因此它们不应该在特定于线程的数据析构函数中使用,甚至不应该作为特定于线程的数据的成员,因为后者作为参数传递给析构函数。
函数:int pthread_key_delete (pthread_key_t key)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
在调用线程中销毁线程特定的数据键。线程特定数据的析构函数不会在销毁期间调用,也不会在线程退出期间调用。
函数:void *pthread_getspecific (pthread_key_t key)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
返回与调用线程中的键关联的线程特定数据。
函数:int pthread_setspecific (pthread_key_t key, const void *value)
Preliminary: | MT-Safe | AS-Unsafe corrupt heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.
将线程特定的值与调用线程中的键相关联。
2.2.2. 非 POSIX 扩展
Non-POSIX Extensions
除了为线程实现 POSIX API 之外,GNU C 库还提供了额外的函数和接口来提供标准中未指定的功能。
2.2.2.1. 为线程属性设置进程范围的默认值
Setting Process-wide defaults for thread attributes
GNU C 库提供了非标准 API 函数来设置和获取在进程中创建线程时使用的默认属性。
函数:int pthread_getattr_default_np (pthread_attr_t *attr)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
获取默认属性值并将 attr 设置为匹配。此函数在成功时返回 0,在失败时返回非零错误代码。
函数:int pthread_setattr_default_np (pthread_attr_t *attr)
Preliminary: | MT-Safe | AS-Unsafe heap lock | AC-Unsafe lock mem | See POSIX Safety Concepts.
设置默认属性值以匹配 attr 中的值。该函数在成功时返回 0,在失败时返回非零错误代码。为此函数定义了以下错误代码:
EINVAL
attr 中的至少一个值对属性无效,或者在属性中设置了堆栈地址。
ENOMEM
系统内存不足。
2.2.2.2. 控制新线程的初始信号掩码
Controlling the Initial Signal Mask of a New Thread
GNU C 库提供了一种方法来指定使用 pthread_create 创建的线程的初始信号掩码,并传递为此目的配置的线程属性对象。
函数:int pthread_attr_setsigmask_np (pthread_attr_t *attr, const sigset_t *sigmask)
Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.
更改 attr 指定的初始信号掩码。如果 sigmask 不为 NULL,则使用 attr 创建的新线程的初始信号掩码设置为 *sigmask。如果 sigmask 为 NULL,attr 将不再指定显式的信号掩码,以便新线程的初始信号掩码继承自调用 pthread_create 的线程。
此函数在成功时返回零,在内存分配失败时返回 ENOMEM。
函数:int pthread_attr_getsigmask_np (const pthread_attr_t *attr, sigset_t *sigmask)
Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.
检索存储在 attr 中的信号掩码并将其复制到 *sigmask。如果尚未设置信号掩码,则返回特殊常量 PTHREAD_ATTR_NO_SIGMASK_NP,否则返回零。
仅当信号掩码先前已由 pthread_attr_setsigmask_np 存储时,才能获得信号掩码。例如,pthread_getattr_np函数没有获取到指定线程当前的信号掩码,pthread_attr_getsigmask_np随后会报告信号掩码为未设置。
宏:int PTHREAD_ATTR_NO_SIGMASK_NP
pthread_attr_setsigmask_np 返回的特殊值,表示没有为该属性设置信号掩码。
可以在不使用这些函数的情况下创建具有特定信号掩码的新线程。在调用 pthread_create 的线程上,一般情况所需的步骤是:
- 屏蔽所有信号,并使用 pthread_sigmask 保存旧的信号屏蔽。这确保了新线程将在所有信号被屏蔽的情况下创建,因此在设置所需的信号掩码之前不能将任何信号传递给线程。
- 调用 pthread_create 创建新线程,将所需的信号掩码传递给线程启动例程(这可能是实际线程启动例程的包装函数)。可能需要在堆上制作所需信号掩码的副本,以便副本的生命周期延长到启动例程需要访问信号掩码的时间点。
- 将线程的信号掩码恢复到第一步中保存的集合。
创建线程的启动例程需要找到所需的信号掩码并使用 pthread_sigmask 将其应用于线程。如果信号掩码被复制到堆分配中,则应释放该副本。
2.2.2.3. 根据特定时钟等待的函数
Functions for Waiting According to a Specific Clock
GNU C 库提供了几个等待函数,它们需要一个显式的 clockid_t 参数。
函数:int sem_clockwait (sem_t *sem, clockid_t clockid, const struct timespec *abstime)
行为类似于 sem_timedwait,除了时间 abstime 是根据 clockid 指定的时钟而不是 CLOCK_REALTIME 测量的。目前,clockid 必须是 CLOCK_MONOTONIC 或 CLOCK_REALTIME。
函数:int pthread_cond_clockwait (pthread_cond_t *cond, pthread_mutex_t *mutex, clockid_t clockid, const struct timespec *abstime)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
行为类似于 pthread_cond_timedwait,除了时间 abstime 是根据 clockid 指定的时钟而不是调用 pthread_cond_init 时指定或默认的时钟来测量的。目前,clockid 必须是 CLOCK_MONOTONIC 或 CLOCK_REALTIME。
函数:int pthread_rwlock_clockrdlock (pthread_rwlock_t *rwlock, clockid_t clockid, const struct timespec *abstime)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
行为类似于 pthread_rwlock_timedrdlock,除了时间 abstime 是根据 clockid 而不是 CLOCK_REALTIME 指定的时钟测量的。当前,clockid 必须是 CLOCK_MONOTONIC 或 CLOCK_REALTIME,否则返回 EINVAL。
函数:int pthread_rwlock_clockwrlock (pthread_rwlock_t *rwlock, clockid_t clockid, const struct timespec *abstime)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
行为类似于 pthread_rwlock_timedwrlock,除了时间 abstime 是根据 clockid 而不是 CLOCK_REALTIME 指定的时钟测量的。当前,clockid 必须是 CLOCK_MONOTONIC 或 CLOCK_REALTIME,否则返回 EINVAL。
函数:int pthread_tryjoin_np (pthread_t *thread, void **thread_return)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
行为与 pthread_join 类似,但如果 thread 指定的线程尚未终止,它将立即返回 EBUSY。
函数:int pthread_timedjoin_np (pthread_t *thread, void **thread_return, const struct timespec *abstime)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
行为类似于 pthread_tryjoin_np,只是它会阻塞,直到达到针对 CLOCK_REALTIME 测量的绝对时间 abstime,如果此时线程尚未终止并返回 EBUSY。如果 abstime 等于 NULL,那么函数将永远等待,就像 pthread_join 一样。
函数:int pthread_clockjoin_np (pthread_t *thread, void **thread_return, clockid_t clockid, const struct timespec *abstime)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock | See POSIX Safety Concepts.
行为类似于 pthread_timedjoin_np,只是 abstime 中的绝对时间是根据 clockid 指定的时钟测量的。目前,clockid 必须是 CLOCK_MONOTONIC 或 CLOCK_REALTIME。
2.2.2.4. 检测单线程执行
Detecting Single-Threaded Execution
多线程程序需要线程之间的同步。即使只有一个线程并且多个处理器之间没有共享数据,这种同步也可能代价高昂。GNU C 库提供了一个接口来检测进程是否处于单线程模式。应用程序可以使用此信息来避免同步,例如通过使用常规指令而不是原子指令来加载和存储内存,或者使用宽松的内存排序而不是更强的内存排序。
变量:char __libc_single_threaded
如果当前进程肯定是单线程的,则此变量非零。如果为零,则该进程可能是多线程的,或者 GNU C 库在程序执行的这一点上无法确定该进程是否是单线程的。
应用程序绝不能写入此变量。
无论 __libc_single_threaded 是否为真,大多数应用程序都应该执行相同的操作,除非同步较少。如果遵循此规则,则随后变为多线程的进程已经处于一致状态。例如,为了增加引用计数,可以使用以下代码:
if (__libc_single_threaded)
atomic_fetch_add (&reference_count, 1, memory_order_relaxed);
else
atomic_fetch_add (&reference_count, 1, memory_order_acq_rel);
这仍然需要在单线程分支上进行某种形式的同步,因此最好不要将引用计数声明为 _Atomic,并使用 GCC 的 __atomic 内置函数。请参阅使用 GNU 编译器集合 (GCC) 中的内存模型感知原子操作的内置函数。然后增加引用计数的代码如下所示:
if (__libc_single_threaded)
++reference_count;
else
__atomic_fetch_add (&reference_count, 1, __ATOMIC_ACQ_REL);
(根据与引用计数关联的数据,可能会在多线程分支上使用较弱的 __ATOMIC_RELAXED 内存排序。)
GNU C 库中的几个函数可以更改 __libc_single_threaded 变量的值。例如,使用 pthread_create 或 thrd_create 函数创建新线程会将变量设置为 false。这也可以间接发生,例如通过调用 dlopen。因此,如果在这样的函数调用之后,应用程序需要复制 __libc_single_thread 的值,则行为必须与调用之前的值相匹配,如下所示:
bool single_threaded = __libc_single_threaded;
if (single_threaded)
prepare_single_threaded ();
else
prepare_multi_thread ();
void *handle = dlopen (shared_library_name, RTLD_NOW);
lookup_symbols (handle);
if (single_threaded)
cleanup_single_threaded ();
else
cleanup_multi_thread ();
由于 __libc_single_threaded 的值可以在程序执行期间从 true 变为 false,因此对于在 IFUNC 解析器中选择优化的函数实现没有用处。
原子操作也可以用于单线程进程之间共享的映射。这意味着编译器不能使用 __libc_single_threaded 来优化原子操作,除非它能够证明内存不是共享的。
实现说明:__libc_single_threaded 变量未声明为 volatile,因为编译器将一系列单线程检查优化为一个检查,例如,如果更新了多个引用计数。如果进程再次变为单线程,则 GNU C 库中的当前实现不会将 __libc_single_threaded 变量设置为真值。GNU C 库的未来版本可能会这样做,但只是作为函数调用的结果,这意味着获取(编译器)障碍。(一些编译器假定诸如 malloc 之类的知名函数不会写入全局变量,并且设置 __libc_single_threaded 会引入数据竞争和未定义的行为。)无论如何,应用程序不得写入 __libc_single_threaded,即使它已加入最后一个应用程序创建的线程,因为 GNU C 库的未来版本可能会在创建第一个线程后创建后台线程,并且应用程序无法知道这些线程是否存在。
2.2.2.5. 可重启序列
Restartable Sequences
本节介绍 GNU C 库的可重新启动序列集成。此功能仅在 Linux 上可用。
数据类型:struct rseq
可重启序列区域的类型。Linux 的未来版本可能会在此结构的末尾添加额外的字段。
用户需要使用线程指针和 __rseq_offset 变量来获取可重启序列区域的地址,如下所述。
可重启序列区域的一种用途是从其 cpu_id 字段读取当前 CPU 编号,作为 sched_getcpu 的内联版本。如果注册失败或被明确禁用,GNU C 库会将 cpu_id 字段设置为 RSEQ_CPU_ID_REGISTRATION_FAILED。
此外,用户可以将 struct rseq_cs 对象的地址存储到 struct rseq 的 rseq_cs 字段中,从而通知内核线程进入可重新启动的序列临界区。该指针及其本身指向的代码区域不得指向已释放或重用的内存区域。有几种方法可以保证这一点。如果应用程序或库可以保证用于保存 struct rseq_cs 的内存和它引用的代码区域永远不会被释放或重用,则不必采取特殊措施。否则,在重新使用释放的内存之前,应用程序负责将每个线程的可重新启动序列区域中的 rseq_cs 字段设置为 NULL,以保证它不会泄漏悬空引用。因为应用程序通常不知道库使用可重启序列的知识,所以建议使用可重启序列的库在从使用可重启序列的库函数返回之前将 rseq_cs 字段设置为 NULL。.
rseq 系统调用的手册可以在 https://git.kernel.org/pub/scm/libs/librseq/librseq.git/tree/doc/man/rseq.2 找到。
变量:ptrdiff_t __rseq_offset
此变量包含线程指针(由 __builtin_thread_pointer 或体系结构的线程指针寄存器定义)和可重新启动序列区域之间的偏移量。该值对于进程中的所有线程都是相同的。如果可重新启动的序列区域位于比线程指针指向的位置低的地址,则该值为负。
变量:unsigned int __rseq_size
此变量为零(如果可重新启动序列注册失败或已被禁用)或可重新启动序列注册的大小。如果内核扩展了注册的大小,这可能与 struct rseq 的大小不同。如果注册成功,__rseq_size 至少为 32(struct rseq 的初始大小)。
变量:unsigned int __rseq_flags
在向内核注册可重新启动序列期间使用的标志。目前为零。
宏:int RSEQ_SIG
每个支持的体系结构都在 sys/rseq.h 中提供了一个包含签名的 RSEQ_SIG 宏。在每个可重新启动的序列中止处理程序之前,该签名预计将出现在代码中。未能提供预期的签名可能会因分段错误而终止进程。