条件变量是日常开发中进行多线程同步的一个重要手段,使用条件变量,可以使得我们可以构建出生产者-消费者这样的模型。
本文将从glibc条件变量的源码出发,讲解其背后的实现原理。
pthread_cond_t是glibc的条件变量的结构,其___data字段比较重要,进一步我们查看__pthread_cond_s的定义。
typedef union
{
struct __pthread_cond_s __data;
char __size[__SIZEOF_PTHREAD_COND_T];
__extension__ long long int __align;
} pthread_cond_t;
__pthread_cond_s的定义如下所示,字段很多,比互斥锁复杂了很多。
struct __pthread_cond_s
{
__extension__ union
{
__extension__ unsigned long long int __wseq;
struct
{
unsigned int __low;
unsigned int __high;
} __wseq32;
};
__extension__ union
{
__extension__ unsigned long long int __g1_start;
struct
{
unsigned int __low;
unsigned int __high;
} __g1_start32;
};
unsigned int __g_refs[2] __LOCK_ALIGNMENT;
unsigned int __g_size[2];
unsigned int __g1_orig_size;
unsigned int __wrefs;
unsigned int __g_signals[2];
};
其各个字段的解释如下所示:
附录中有源码中对于这些字段的详细解释,也可以参考。
总之这些字段是比较复杂的,下面将会对pthread_cond_signal和pthread_cond_wait两个函数进行详解,届时将会理解这些字段的含义。
pthread_cond_signal是条件变量发送信号的方法,其过程如下所示:
这里开始涉及G1和G2的概念。这里给出其含义,即新的waiter将加入G2,signal将从G1中取waiter进行唤醒,如果G1没有waiter,再从G2中取waiter唤醒。
接下来通过源码分析其执行过程。
pthread_cond_signal首先将读取条件变量的等待任务的数量。 __wref >> 3 等同于__wref/8,wref每次是按照8递增的,在pthread_conf_wait函数中有相应实现。
__wref按照8递增的原因,在注释中也给出了,因为低3位有了其它用途。
__wrefs: Waiter reference counter.
Bit 2 is true if waiters should run futex_wake when they remove the last reference. pthread_cond_destroy uses this as futex word.Bit 1 is the clock ID (0 == CLOCK_REALTIME, 1 == CLOCK_MONOTONIC).Bit 0 is true iff this is a process-shared condvar.
如果没有waiter,就不用发送信号,于是直接返回。所谓waiter就是调用了pthread_cond_wait而陷入wait的任务。
unsigned int wrefs = atomic_load_relaxed (&cond->__data.__wrefs);
if (wrefs >> 3 == 0)
return 0;
接下来获取条件变量中的序列号,通过序列号来获取现在的G1数组的下标(0或者1)。
刚开始时wseq为偶数,因此G1的index为1。
unsigned long long int wseq = __condvar_load_wseq_relaxed (cond);
unsigned int g1 = (wseq & 1) ^ 1;
wseq >>= 1;
bool do_futex_wake = false;
接着检查G1中是否有waiter,如果有,向G1组中发送信号值(对应的signals+2),并将G1中剩余的waiter减去1。
如果G1已经没有剩余的waiter,那么就需要从G2中取waiter。这里通过__condvar_quiesce_and_switch_g1实现,实际上__condvar_quiesce_and_switch_g1是将G1和G2的身份做了调换。
if ((cond->__data.__g_size[g1] != 0)
|| __condvar_quiesce_and_switch_g1 (cond, wseq, &g1, private))
{
/* Add a signal. Relaxed MO is fine because signaling does not need to
establish a happens-before relation (see above). We do not mask the
release-MO store when initializing a group in
__condvar_quiesce_and_switch_g1 because we use an atomic
read-modify-write and thus extend that store's release sequence. */
atomic_fetch_add_relaxed (cond->__data.__g_signals + g1, 2);
cond->__data.__g_size[g1]--;
/* TODO Only set it if there are indeed futex waiters. */
do_futex_wake = true;
}
下面详细看看__condvar_quiesce_and_switch_g1都做了哪些事情,其定义在了nptl/pthread_cond_common.c文件中。
__condvar_quiesce_and_switch_g1首先检查G2是否有waiter,如果没有waiter,则不进行操作。即G1和G2不需要进行调整,新的waiter仍然记录在G2中。计算方法可以参考下图进行理解:
unsigned int old_orig_size = __condvar_get_orig_size (cond);
uint64_t old_g1_start = __condvar_load_g1_start_relaxed (cond) >> 1;
if (((unsigned) (wseq - old_g1_start - old_orig_size)
+ cond->__data.__g_size[g1 ^ 1]) == 0)
return false;
下面将G1的signal值和1进行与操作,标记此时g1已经被close。因为程序的并发性,在G1和G2切换的时候可能还会有新的waiter加入到旧的G1中。于是就给他们发送特殊的信号值,使得这些waiter可以感知。从这个点,也能联想到为什么条件变量会存在虚假唤醒。
atomic_fetch_or_relaxed (cond->__data.__g_signals + g1, 1);
接下来,将G1中剩下的waiter全部唤醒。实际上进入__condvar_quiesce_and_switch_g1方法时,G1的长度已经为0,这里G1又出现了waiter就是由于程序的并发生可能导致的问题。因此这里将G1剩下的waiter进行唤醒。这里__g_refs和已经调用futex_wait进行睡眠的waiter数量相关。
unsigned r = atomic_fetch_or_release (cond->__data.__g_refs + g1, 0);
while ((r >> 1) > 0)
{
for (unsigned int spin = maxspin; ((r >> 1) > 0) && (spin > 0); spin--)
{
r = atomic_load_relaxed (cond->__data.__g_refs + g1);
}
if ((r >> 1) > 0)
{
r = atomic_fetch_or_relaxed (cond->__data.__g_refs + g1, 1) | 1;
if ((r >> 1) > 0)
futex_wait_simple (cond->__data.__g_refs + g1, r, private);
r = atomic_load_relaxed (cond->__data.__g_refs + g1);
}
}
接下来,开始对G1和G2进行切换。换的过程很简单,就是将G1的index和G2的index做了切换。
切换之后,为了知道当前G1的一些信息,会计算其起始下标和长度。这个起始下标的含义起始时针对历史上所有的waiter而言的。这个点不是很好理解,可以参考下文中对于pthread_cond_signal和pthread_conf_wait的梳理。
wseq = __condvar_fetch_xor_wseq_release (cond, 1) >> 1;
g1 ^= 1;
*g1index ^= 1;
unsigned int orig_size = wseq - (old_g1_start + old_orig_size);
__condvar_set_orig_size (cond, orig_size);
/* Use and addition to not loose track of cancellations in what was
previously G2. */
cond->__data.__g_size[g1] += orig_size;//计算还有多少waiter没有唤醒
//如果waiter cacel了wait,可能会走到这个if语句中。
if (cond->__data.__g_size[g1] == 0)
return false;
return true;
__condvar_quiesce_and_switch_g1到此为止就结束了,实际上就是当旧的G1中所有的waiter都唤醒时,将老的G1和G2身份对调。于是老的G2就成为了G1。后续将从G1继续唤醒waiter。
回到pthread_cond_signal,最后一部分代码则将互斥锁进行释放,接着如果需要进入内核,则调用futex_wake对waiter进行唤醒。
__condvar_release_lock (cond, private);
if (do_futex_wake)
futex_wake (cond->__data.__g_signals + g1, 1, private);
pthread_cond_wait是等待条件变量的方法,其过程如下所示:
下面就对照源码进行解析。
pthread_cond_wait首先会获取一个等待的序列号。条件变量的结构体中有一个字段是__wseq,这个便是所谓的序列号,每次pthread_cond_wait都会将序列号加上2。
从条件变量的初始化可以知道,wseq初始值为0。而wseq每次原子地递增2,因此当前wseq是一个偶数。wseq的奇偶性不是一成不变的,当g1和g2发生切换时,wseq会发生变化。
#define PTHREAD_COND_INITIALIZER { { {0}, {0}, {0, 0}, {0, 0}, 0, 0, {0, 0} } }
接下来将wseq和1进行与操作,由于wseq为偶数,因此g等于0。
uint64_t wseq = __condvar_fetch_add_wseq_acquire (cond, 2);
unsigned int g = wseq & 1;
uint64_t seq = wseq >> 1;
接下来,使用原子函数atomic_fetch_add_relaxed将增加条件变量的等待数量,注意这里一次增加了8。这里使用了relaxed的memory order已经足够了,因为我们的目的仅仅为了将cond->__data.__wrefs增加8。
unsigned int flags = atomic_fetch_add_relaxed (&cond->__data.__wrefs, 8);
接下来调用__pthread_mutex_unlock_usercnt释放互斥锁。
err = __pthread_mutex_unlock_usercnt (mutex, 0);
if (__glibc_unlikely (err != 0))
{
__condvar_cancel_waiting (cond, seq, g, private);
__condvar_confirm_wakeup (cond, private);
return err;
}
首先自旋检查cond->__data.__g_signals+ ggroup中的信号数量,如果有信号,意味着不用进入内核态,而直接唤醒。这里也是条件变量出现虚假唤醒的原因。
unsigned int spin = maxspin;
while (signals == 0 && spin > 0)
{
/* Check that we are not spinning on a group that's already
closed. */
if (seq < (__condvar_load_g1_start_relaxed (cond) >> 1))
goto done;
/* TODO Back off. */
/* Reload signals. See above for MO. */
signals = atomic_load_acquire (cond->__data.__g_signals + g);
spin--;
}
接下来,如果signal的值是低位为1,意味着当前的组已经被closed,直接跳出wait方法。这个点和之前讲解pthread_signal是呼应的。
如果signals的值低位不是1,并且大于0,则认为获取到了有效的信号。跳过下面的逻辑。
if (signals & 1)
goto done;
/* If there is an available signal, don't block. */
if (signals != 0)
break;
如果逻辑没有走到这里,意味着自旋过程中,没有收到信号,于是尝试开始进行阻塞的动作。
首先将引用计数增加2,意味着将要进入内核wait。
atomic_fetch_add_acquire (cond->__data.__g_refs + g, 2);
if (((atomic_load_acquire (cond->__data.__g_signals + g) & 1) != 0)
|| (seq < (__condvar_load_g1_start_relaxed (cond) >> 1)))
{
/* Our group is closed. Wake up any signalers that might be
waiting. */
__condvar_dec_grefs (cond, g, private);
goto done;
}
下面开始调用futex_wait进行等待。注意这里调用的是__futex_abstimed_wait_cancelable64,看起来好像是可以传递时间参数的。但是___pthread_cond_wait传入的参数是NULL,因此等同于futex_wait。
int
___pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex)
{
/* clockid is unused when abstime is NULL. */
return __pthread_cond_wait_common (cond, mutex, 0, NULL);
}
struct _pthread_cleanup_buffer buffer;
struct _condvar_cleanup_buffer cbuffer;
cbuffer.wseq = wseq;
cbuffer.cond = cond;
cbuffer.mutex = mutex;
cbuffer.private = private;
__pthread_cleanup_push (&buffer, __condvar_cleanup_waiting, &cbuffer);
err = __futex_abstimed_wait_cancelable64 (
cond->__data.__g_signals + g, 0, clockid, abstime, private);
__pthread_cleanup_pop (&buffer, 0);
if (__glibc_unlikely (err == ETIMEDOUT || err == EOVERFLOW))
{
__condvar_dec_grefs (cond, g, private);
/* If we timed out, we effectively cancel waiting. Note that
we have decremented __g_refs before cancellation, so that a
deadlock between waiting for quiescence of our group in
__condvar_quiesce_and_switch_g1 and us trying to acquire
the lock during cancellation is not possible. */
__condvar_cancel_waiting (cond, seq, g, private);
result = err;
goto done;
}
else
__condvar_dec_grefs (cond, g, private);
/* Reload signals. See above for MO. */
signals = atomic_load_acquire (cond->__data.__g_signals + g);
下面的代码是针对并发问题的处理,这里可以自行研究。
uint64_t g1_start = __condvar_load_g1_start_relaxed (cond);
if (seq < (g1_start >> 1))
{
if (((g1_start & 1) ^ 1) == g)
{
unsigned int s = atomic_load_relaxed (cond->__data.__g_signals + g);
while (__condvar_load_g1_start_relaxed (cond) == g1_start)
{
if (((s & 1) != 0)
|| atomic_compare_exchange_weak_relaxed
(cond->__data.__g_signals + g, &s, s + 2))
{
futex_wake (cond->__data.__g_signals + g, 1, private);
break;
}
}
}
上面对两个函数进行了详解的分析,这里提供一张流程图,用以对上述过程加以理解。
在pthread_cond_signal中,首先会原子性地修改一个signal变量的值,如果此时一个waiter还没有进入内核wait,还在自旋检查该变量,那么这个waiter就会被直接唤醒,而不会调用futex_wait。
在修改完这个signal变量的值之后,将会调用futex_wait唤醒一个waiter。
如果此时有一个signal的A线程,一个已经调用futex_wait的B线程,和一个正在wait的C线程,signal线程调用pthead_cond_signal就可能同时将B线程和C线程全部唤醒。
从源码的注释中,导致虚假唤醒的场景还不止于此,但是上述是一个最经典的场景。
由于虚假唤醒的存在,就要求我们在写条件变量时一定要记得写循环判等,类似于下面的形式。
while(!flag)
{
cv.wait(mtx);
}
接下来,结合gdb,边运行边打印,观察条件变量内部数据的变化。
//g++ test.cpp -g -lpthread
#include
#include
#include
#include
pthread_t t1;
pthread_t t2;
pthread_t t3;
pthread_mutex_t mutex;
pthread_cond_t cond;
void* Signal(void* arg)
{
sleep(1);
while(1)
{
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
printf("Process1 signal
");
pthread_mutex_unlock(&mutex);
sleep(2);
}
}
void* Waiter(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
printf("Waiter start to wait
");
pthread_cond_wait(&cond,&mutex);
printf("Waiter awake
");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main(){
pthread_cond_init(&cond,NULL);
pthread_mutex_init(&mutex,NULL);
pthread_create(&t1,NULL,Signal,NULL);
pthread_create(&t2,NULL,Waiter,NULL);
pthread_create(&t3,NULL,Waiter,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
return 0;
}
使用gdb调试上述程序,并在源码中的第17行(pthread_cond_signal方法上)设置一个断点。
该程序有一个Signal线程和两个Waiter线程。
在程序中,创建了三个线程,两个waiter线程,一个signal线程。我们在signal线程的pthread_cond_signal方法中下了断点。
程序开始时G2的index = 0 , G1的index = 1。
首先我们分析 __wseq,在上文的解析中知道,pthread_cond_wait每次会首先获取一个序列号,并将该序列号加上1。 实际操作时,因为__wseq的LSB(最低位)代表了G2的下标,因此每个waiter会将序列号加2(1 << 1)。由于有两个waiter,因此__wseq应该为4。从下面的gdb的打印中的内容,确实如此。
接着分析 __g_refs, 由于其中两个waiter线程已经调用futex_wait进行sleep,而新的waiter总是加入到G2中,且目前G2的index是0,因此__g_refs = {4, 0}。 __g_refs中元素是4而不是2的原因和__wseq是类似的。
接着分析 __wrefs,其代表了waiter的总数量,目前有2个waiter,每个waiter会使得__wrefs增加8,因此__wrefs = 16。之所以增加8,是因为其低3位有了其它用途,这个点上面也提到过,这里再提及一次,下面的分析中将不再重复。
接着分析 __g_size,它表示G1和G2交换后,G1中剩余的waiter数量。由于目前还没有G1和G2的切换,因此__g_size = {0,0}。
最后分析 __g1_start和**__g1_orig_size**,这里没有出现G1和G2的切换,因此__g1_start和__g1_orig_size都还是初始值0。
此时,G1和G2的构成如下图所示:
[root@localhost test2]# gdb a.out -q
Reading symbols from a.out...
(gdb) b 17
Breakpoint 1 at 0x4011d6: file test.cpp, line 17.
(gdb) r
Starting program: /home/work/cpp_proj/test2/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff7a8e640 (LWP 22121)]
[New Thread 0x7ffff728d640 (LWP 22122)]
Waiter start to wait
[New Thread 0x7ffff6a8c640 (LWP 22123)]
Waiter start to wait
[Switching to Thread 0x7ffff7a8e640 (LWP 22121)]
Thread 2 "a.out" hit Breakpoint 1, Signal (arg=0x0) at test.cpp:17
17 pthread_cond_signal(&cond);
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.34-28.el9_0.2.x86_64 libgcc-11.2.1-9.4.el9.x86_64 libstdc++-11.2.1-9.4.el9.x86_64
(gdb) p cond
$1 = {__data = {{__wseq = 4, __wseq32 = {__low = 4, __high = 0}}, {__g1_start = 0, __g1_start32 = {__low = 0, __high = 0}}, __g_refs = {
4, 0}, __g_size = {0, 0}, __g1_orig_size = 0, __wrefs = 16, __g_signals = {0, 0}},
__size = " 04", ' 00' , " 04", ' 00' , " 20 00 00 00 00 00 00 00 00 00 00",
__align = 4}
接下来我们使用next,使得其中一个线程进行signal操作。 下面我们再一一分析条件变量的数据变化。
在Signal线程执行signal操作时,此时G1的长度为0(初始状态下,waiter都是加入G2的,G1为空),因此下面将会遇到G1和G2的切换。
首先分析 __wseq。__wseq在G1和G2切换时,奇偶性会发生变化。计算方法为4^1 = 5。因此__wseq = 5。
接着分析 __g_refs。 由于signal线程调用pthread_cond_signal对waiter进行了唤醒。因此__g_refs需要减去2,因此其等于{2,0}。
接着分析 __g_size。因为此前G2的waiter有2个,已经唤醒了一个,还剩下一个没有唤醒,因此这里g_size = {1,0}。
接着分析 __g1_start。__g1_start指的是当前的G1数组在历史waiter中的序号。毫无疑问,初始状态下,__g1_start = 1。
接着分析 __g1_orig_size, __g1_orig_size指的是当前的G1在历史waiter图中的长度。之前G2的waiter数量为2,切换后G1的原始长度也为2,因此__g1_orig_size = (2 << 2) = 8。
注意此时G1的index = 0, G2的index = 1,已经发生改变。
这个过程如下图所示:
(gdb) n
18 printf("Process1 signal
");
(gdb) p cond
$2 = {__data = {{__wseq = 5, __wseq32 = {__low = 5, __high = 0}}, {__g1_start = 1, __g1_start32 = {__low = 1, __high = 0}}, __g_refs = {
2, 0}, __g_size = {1, 0}, __g1_orig_size = 8, __wrefs = 8, __g_signals = {0, 0}},
__size = " 05 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00b 00 00 00b 00 00 00 00 00 00 00 00 00 00", __align = 5}
接着我们使用continue,继续程序的运行。由于此前Signal线程唤醒了一个waiter,于是该waiter继续执行,sleep 1s后又将调用pthread_cond_wait陷入等待。
注意此时G1的index = 0, G2的index = 1。
首先分析 __wseq。此前__wseq值为5。此时由于又加入了一个waiter,因此__wseq增加2,__wseq = 7。
接着分析 __g1_start。由于没有发生G1和G2的切换,因此其值保持不变,仍为1。
接着分析 __g_refs。此时G1仍然有一个waiter没有唤醒,而新的waiter会加入G2,因此其值为{2,2}。
接着分析 __g_size,G1中还剩下一个waiter没有唤醒,因此其值等于{1,0}。
接着分析 __g1_orig_size。由于没有发生G1和G2的切换,因此其值保持不变,仍为8。
接着分析 __wrefs,因为G1和G2总共有2个waiter,因此其值等于16。
__g_signals的值很难被捕获到,其值在pthread_cond_signal的内部发生改变。
(gdb) c
Continuing.
Process1 signal
Waiter awake
Waiter start to wait
Thread 2 "a.out" hit Breakpoint 1, Signal (arg=0x0) at test.cpp:17
17 pthread_cond_signal(&cond);
(gdb) p cond
$3 = {__data = {{__wseq = 7, __wseq32 = {__low = 7, __high = 0}}, {__g1_start = 1, __g1_start32 = {__low = 1, __high = 0}}, __g_refs = {
2, 2}, __g_size = {1, 0}, __g1_orig_size = 8, __wrefs = 16, __g_signals = {0, 0}},
__size = "a 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 02 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00b 00 00 00 20 00 00 00 00 00 00 00 00 00 00", __align = 7}
接着我们使用next,这会使得Signal线程调用pthread_cond_signal唤醒一个waiter。
首先分析 __wseq。由于没有新的waiter,因此__wseq值不变,仍为7。
接着分析 __g1_start。由于没有发生G1和G2的切换,因此其值保持不变,仍为1。
接着分析 __g_refs。Signal线程调用了pthread_conf_signal方法唤醒了一个waiter,因此其值为{0,2}。
接着分析 __g_size,Signal线程调用了pthread_conf_signal方法唤醒了一个waiter,因此其值等于{0,0}。
接着分析 __g1_orig_size。由于没有发生G1和G2的切换,因此其值保持不变,仍为8。
接着分析 __wrefs,因为G1和G2总共有1个waiter,因此其值等于8。
__g_signals的值很难被捕获到,其值在pthread_cond_signal的内部发生改变。
(gdb) n
18 printf("Process1 signal
");
(gdb) p cond
$4 = {__data = {{__wseq = 7, __wseq32 = {__low = 7, __high = 0}}, {__g1_start = 1, __g1_start32 = {__low = 1, __high = 0}}, __g_refs = {
0, 2}, __g_size = {0, 0}, __g1_orig_size = 8, __wrefs = 8, __g_signals = {0, 0}},
__size = "a 00 00 00 00 00 00 00 01", ' 00' , " 02", ' 00' , "b 00 00 00b 00 00 00 00 00 00 00 00 00 00", __align = 7}
接着我们使用continue,继续程序的运行。由于此前Signal线程唤醒了一个waiter,于是该waiter继续执行,sleep 1s后又将调用pthread_cond_wait陷入等待。
注意此时G1的index = 0, G2的index = 1
首先分析 __wseq。此时又加入了一个waiter,因此__wseq值为9。
接着分析 __g1_start。由于没有发生G1和G2的切换,因此其值保持不变,仍为1。
接着分析 __g_refs。此时G2又加入了一个waiter,因此其值为{0,4}(G2的index=1,因此4在第二个位置上)。
接着分析 __g_size。目前G1中没有waiter了,因此值等于{0,0}。
接着分析 __g1_orig_size。由于没有发生G1和G2的切换,因此其值保持不变,仍为8。
接着分析 __wrefs,因为G1和G2总共有2个waiter,因此其值等于16。
__g_signals的值很难被捕获到,其值在pthread_cond_signal的内部发生改变。
(gdb) c
Continuing.
Process1 signal
Waiter awake
Waiter start to wait
Thread 2 "a.out" hit Breakpoint 1, Signal (arg=0x0) at test.cpp:17
17 pthread_cond_signal(&cond);
(gdb) p cond
$5 = {__data = {{__wseq = 9, __wseq32 = {__low = 9, __high = 0}}, {__g1_start = 1, __g1_start32 = {__low = 1, __high = 0}}, __g_refs = {
0, 4}, __g_size = {0, 0}, __g1_orig_size = 8, __wrefs = 16, __g_signals = {0, 0}},
__size = " 00 00 00 00 00 00 00 01", ' 00' , " 04", ' 00' , "b 00 00 00 20 00 00 00 00 00 00 00 00 00 00", __align = 9}
接着我们使用next,使得Signal线程调用pthread_conf_signal方法。这个时候由于G1为0,因此会发生G1和G2的切换。
首先分析 __wseq。此时G1和G2发生了切换,__wseq的奇偶性会发生变化,计算方法为9 ^ 1 = 8,因此__wseq = 8。
接着分析 __g1_start。由于G1和G2发生了切换,当前G2中的第一个waiter属于历史上的第三个waiter,历史值是从0开始的,因此此时G1的起始waiter的序号为2,再进行偏移,就得到了4。
接着分析 __g_refs。目前G1还有一个waiter还没有被唤醒,且目前G1的index = 1,因此__g_refs = {0, 2}。
接着分析 __g_size,在G1和G2切换之前,G2有两个waiter,目前唤醒了一个,还剩下一个,因此其值等于{0, 1}。
接着分析 __g1_orig_size。发生G1和G2切换前,G2有两个任务,因此__g1_orig_size=8。
接着分析 __wrefs,因为G1和G2总共有2个waiter,因此其值等于16。
__g_signals的值很难被捕获到,其值在pthread_cond_signal的内部发生改变。
(gdb) n
18 printf("Process1 signal
");
(gdb) p cond
$6 = {__data = {{__wseq = 8, __wseq32 = {__low = 8, __high = 0}}, {__g1_start = 4, __g1_start32 = {__low = 4, __high = 0}}, __g_refs = {
0, 2}, __g_size = {0, 1}, __g1_orig_size = 8, __wrefs = 8, __g_signals = {0, 0}},
__size = "b 00 00 00 00 00 00 00 04", ' 00' , " 02 00 00 00 00 00 00 00 01 00 00 00b 00 00 00b 00 00 00 00 00 00 00 00 00 00", __align = 8}
下面继续执行,分析的情况是类似的,不再展开。
__wseq: Waiter sequence counter
* LSB is index of current G2.
* Waiters fetch-add while having acquire the mutex associated with the
condvar. Signalers load it and fetch-xor it concurrently.
__g1_start: Starting position of G1 (inclusive)
* LSB is index of current G2.
* Modified by signalers while having acquired the condvar-internal lock
and observed concurrently by waiters.
__g1_orig_size: Initial size of G1
* The two least-significant bits represent the condvar-internal lock.
* Only accessed while having acquired the condvar-internal lock.
__wrefs: Waiter reference counter.
* Bit 2 is true if waiters should run futex_wake when they remove the
last reference. pthread_cond_destroy uses this as futex word.
* Bit 1 is the clock ID (0 == CLOCK_REALTIME, 1 == CLOCK_MONOTONIC).
* Bit 0 is true iff this is a process-shared condvar.
* Simple reference count used by both waiters and pthread_cond_destroy.
(If the format of __wrefs is changed, update nptl_lock_constants.pysym
and the pretty printers.)
For each of the two groups, we have:
__g_refs: Futex waiter reference count.
* LSB is true if waiters should run futex_wake when they remove the
last reference.
* Reference count used by waiters concurrently with signalers that have
acquired the condvar-internal lock.
__g_signals: The number of signals that can still be consumed.
* Used as a futex word by waiters. Used concurrently by waiters and
signalers.
* LSB is true iff this group has been completely signaled (i.e., it is
closed).
__g_size: Waiters remaining in this group (i.e., which have not been
signaled yet.
* Accessed by signalers and waiters that cancel waiting (both do so only
when having acquired the condvar-internal lock.
* The size of G2 is always zero because it cannot be determined until
the group becomes G1.
* Although this is of unsigned type, we rely on using unsigned overflow
rules to make this hold effectively negative values too (in
particular, when waiters in G2 cancel waiting).
glibc中的条件变量的底层实现是相对复杂的,其将信号分成了两个组G1和G2,pthread_cond_wait会将waiter加入到G2组,而pthread_cond_wait将会从G1中进行唤醒,如果G1全部唤醒,将会检查G2,如果G2存在waiter,将切换G1和G2,如此循环往复。 由于需要考虑并发性的问题,程序中加入了很多的检查逻辑,因此程序理解起来是相对复杂的。除此之外,从其源码中,我们也可以更好的理解为什么条件变量会存在虚假唤醒。
页面更新:2024-05-28
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号