我有3个需要同步的进程。进程一做某事然后唤醒进程二并休眠,这会做一些事情然后唤醒进程三并休眠,这会做一些事情并唤醒进程并休眠。整个循环定时运行大约25hz(在我的“真实”应用程序中触发进程2之前,由外部同步进入进程1)。我使用sem_post来触发(唤醒)每个进程,并使用sem_timedwait()来等待触发器。
这一切都成功运行了几个小时。但是在某个随机时间(通常在两到四个小时之后),其中一个进程在sem_timedwait()中开始超时,即使我确定使用sem_post()触发了信号量。为了证明这一点,我甚至在超时后立即使用sem_getvalue(),并且值为1,因此应该触发timedwait。
请参阅以下代码:
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <semaphore.h>
sem_t trigger_sem1, trigger_sem2, trigger_sem3;
// The main thread process. Called three times with a different num arg - 1, 2 or 3.
void *thread(void *arg)
{
int num = (int) arg;
sem_t *wait, *trigger;
int val, retval;
struct timespec ts;
struct timeval tv;
switch (num)
{
case 1:
wait = &trigger_sem1;
trigger = &trigger_sem2;
break;
case 2:
wait = &trigger_sem2;
trigger = &trigger_sem3;
break;
case 3:
wait = &trigger_sem3;
trigger = &trigger_sem1;
break;
}
while (1)
{
// The first thread delays by 40ms to time the whole loop.
// This is an external sync in the real app.
if (num == 1)
usleep(40000);
// print sem value before we wait. If this is 1, sem_timedwait() will
// return immediately, otherwise it will block until sem_post() is called on this sem.
sem_getvalue(wait, &val);
printf("sem%d wait sync sem%d. val before %d\n", num, num, val);
// get current time and add half a second for timeout.
gettimeofday(&tv, NULL);
ts.tv_sec = tv.tv_sec;
ts.tv_nsec = (tv.tv_usec + 500000); // add half a second
if (ts.tv_nsec > 1000000)
{
ts.tv_sec++;
ts.tv_nsec -= 1000000;
}
ts.tv_nsec *= 1000; /* convert to nanosecs */
retval = sem_timedwait(wait, &ts);
if (retval == -1)
{
// timed out. Print value of sem now. This should be 0, otherwise sem_timedwait
// would have woken before timeout (unless the sem_post happened between the
// timeout and this call to sem_getvalue).
sem_getvalue(wait, &val);
printf("!!!!!! sem%d sem_timedwait failed: %s, val now %d\n",
num, strerror(errno), val);
}
else
printf("sem%d wakeup.\n", num);
// get value of semaphore to trigger. If it's 1, don't post as it has already been
// triggered and sem_timedwait on this sem *should* not block.
sem_getvalue(trigger, &val);
if (val <= 0)
{
printf("sem%d send sync sem%d. val before %d\n", num, (num == 3 ? 1 : num+1), val);
sem_post(trigger);
}
else
printf("!! sem%d not sending sync, val %d\n", num, val);
}
}
int main(int argc, char *argv[])
{
pthread_t t1, t2, t3;
// create semaphores. val of sem1 is 1 to trigger straight away and start the whole ball rolling.
if (sem_init(&trigger_sem1, 0, 1) == -1)
perror("Error creating trigger_listman semaphore");
if (sem_init(&trigger_sem2, 0, 0) == -1)
perror("Error creating trigger_comms semaphore");
if (sem_init(&trigger_sem3, 0, 0) == -1)
perror("Error creating trigger_vws semaphore");
pthread_create(&t1, NULL, thread, (void *) 1);
pthread_create(&t2, NULL, thread, (void *) 2);
pthread_create(&t3, NULL, thread, (void *) 3);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
}
当程序正确运行时(在开始时和随机但很长时间后)打印以下输出。 sem1的值在thread1等待40ms之前始终为1,此时sem3已触发它,因此它会立即唤醒。其他两个线程一直等到从前一个线程收到信号量。
[...]
sem1 wait sync sem1. val before 1
sem1 wakeup.
sem1 send sync sem2. val before 0
sem2 wakeup.
sem2 send sync sem3. val before 0
sem2 wait sync sem2. val before 0
sem3 wakeup.
sem3 send sync sem1. val before 0
sem3 wait sync sem3. val before 0
sem1 wait sync sem1. val before 1
sem1 wakeup.
sem1 send sync sem2. val before 0
[...]
然而,几个小时后,其中一个线程开始超时。我可以从输出中看到信号量被触发,当我在超时后打印该值时,它是1.所以sem_timedwait应该在超时之前很好地唤醒。在超时之后我永远不会期望信号量的值为1,除非在超时之后但在我调用sem_getvalue之前触发发生非常罕见的场合(几乎肯定从不但可能)。
此外,一旦它开始失败,该信号量上的每个sem_timedwait()也会以相同的方式失败。请参阅以下输出,我已编号:
01 sem3 wait sync sem3. val before 0
02 sem1 wakeup.
03 sem1 send sync sem2. val before 0
04 sem2 wakeup.
05 sem2 send sync sem3. val before 0
06 sem2 wait sync sem2. val before 0
07 sem1 wait sync sem1. val before 0
08 !!!!!! sem3 sem_timedwait failed: Connection timed out, val now 1
09 sem3 send sync sem1. val before 0
10 sem3 wait sync sem3. val before 1
11 sem3 wakeup.
12 !! sem3 not sending sync, val 1
13 sem3 wait sync sem3. val before 0
14 sem1 wakeup.
[...]
在第1行,线程3(我在printf中混淆地称为sem3)等待触发sem3。在第5行,thread2为sem3调用sem_post。但是,第8行显示sem3超时,但信号量的值为1. thread3然后触发sem1并再次等待(10)。但是,因为该值已经为1,所以它会立即唤醒。它不再发送sem1,因为这一切都发生在对thread1给出控制之前,然后它再次等待(val现在为0)并且sem1唤醒。这现在重复,sem3总是超时并显示值为1.
所以,我的问题是为什么sem3超时,即使信号量已被触发且值明显为1?我永远不会期望在输出中看到第08行。如果它超时(因为,假设线程2已经崩溃或花费的时间太长),则该值应为0.为什么它在进入此状态之前首先工作3或4小时?
我尝试过使用三个独立程序进行类似测试,通过共享内存进行通信,而不是同一程序中的三个线程。这更类似于我的真实世界应用程序。结果和输出是一样的。问题确实出现在信号量(特别是sem_timedwait调用)中,而不是与pthreads有关。
我也尝试过更短和更长的延迟,以及完全消除延迟,结果与上述类似。没有任何延迟,它有时会在几分钟而不是几小时后开始产生错误。这当然意味着问题可以更快地重现。
这是在内核2.6.28上使用Ubuntu 9.4。相同的程序在Redhat和Fedora上运行正常,但我现在正试图移植到Ubuntu。我也试过使用Ubuntu 9.10,没有任何区别。
感谢您的任何建议, 贾尔斯
答案 0 :(得分:2)
(很抱歉给出第二个答案,但这个太乱了,只能通过编辑来清理)
我认为答案已经在问题的原始帖子中了。
所以,我的问题是为什么sem3 超时,即使信号量有 被触发,价值是 显然1?我永远不会期待看到 输出中的第08行。如果它超时 (因为,假设线程2已经崩溃或 价值应该是值得的 是0.为什么它适用于3 或者在进入前4小时 这个状态?
所以场景是:
sem_timedwait
sem_getvalue
sem_post
sem3
sem_getvalue
并看到1 sem_post
在sem1
这种竞争条件很难触发,基本上你必须点击一个小线程,其中一个线程在等待信号量时遇到问题,然后用{{1}读取信号量}。这种情况的发生在很大程度上取决于环境(系统类型,内核数量,负载,IO中断),因此这解释了为什么它只发生在数小时后,如果不发生的话。
让控制流取决于sem_getvalue
通常是一个坏主意。对sem_getvalue
的唯一原子非阻止访问权限是sem_t
和sem_post
。
因此,问题中的示例代码具有竞争条件。这并不意味着吉尔兹的原始问题代码确实具有相同的竞争条件。也许这个例子过于简单化,对他来说仍然表现出同样的现象。
我的猜测是,在他原来的问题中,有一个不受保护的 sem_trywait
。这是一个sem_wait
,只检查其返回值,而不是sem_wait
,如果它失败了。 errno
EINTR
确实会在sem_wait
上自然发生,如果进程有一些IO。如果遇到do - while
,您只需errno
检查并重置EINTR
。
答案 1 :(得分:1)
我不知道出了什么问题,代码看起来也很好。以下是您可以做的一些事情,以某种方式获取更多信息。
正如Jens所指出,有两场比赛:
第一个是在调用sem_timedwait之后评估信号量的值。 这不会改变控制流方面对信号量的影响。线程超时与否,它仍然会经历“我应该触发下一个线程”块。
第二个是“我应该唤醒下一个线程”部分。我们可以举办以下活动:
sem_getvalue(trigger)
并获得1 sem_timedwait
返回,信号量变为0 现在,我看不出这是如何触发观察到的行为的。毕竟,因为线程n + 1无论如何被唤醒,它将反过来唤醒线程n + 2,这将唤醒线程n等......
虽然有可能出现故障,但我看不出这会如何导致线程系统超时。
答案 2 :(得分:1)
这非常有趣。虽然我没有找到错误的来源,(仍在寻找)我已经在运行Linux 2.6.34的Ubuntu 9.04上验证了这一点。
答案 3 :(得分:1)
不要责怪ubuntu或任何其他发行版:-)这里更重要的是你正在使用的gcc版本,32或64位等,你的系统有多少核心。所以请提供更多信息。但仔细查看代码,我发现有几个地方可能会带来意想不到的行为:
从头开始,投射int
在void*
来回,你是
寻找麻烦。使用uintptr_t
如果你必须这样做,但在这里
没有理由通过真实的
指向值的指针。 &(int){ 1 }
和一些更精明的演员会为C99做点什么。
ts.tv_nsec = (tv.tv_usec + 500000)
是另一个麻烦点。右侧可能与左侧的宽度不同。做
ts.tv_nsec = tv.tv_usec;
ts.tv_nsec + = 500000;
sem系列函数不是中断安全的。这样的中断可能例如由IO触发,因为您正在执行printf等。检查-1
左右的返回值是不够的,但在这种情况下,您应该检查errno
并确定是否要重试。然后,如果你想要精确的话,你必须重新计算剩余的时间和类似的东西。然后,sem_timedwait
的手册页会列出可能出现的不同错误代码及其原因。
您还可以从值中得出结论
你通过sem_getvalue
获得。在一个
多线程/多进程/多处理器
你的线程可能有的环境
在返回之间没有计划
来自sem_timedwait
和
sem_getvalue
。基本上你不能
从变量中推导出任何东西
只是偶然的价值
你观察到的不要从中得出结论。
答案 4 :(得分:1)
问题似乎来自传递无效的超时参数。
至少在我的机器上,第一次失败不是ETIMEDOUT而是:
!!!!!! sem2 sem_timedwait failed:参数无效,val现在为0
现在,如果我写:
if (ts.tv_nsec >= 1000000)
(注意添加=)然后它工作正常。这是另一个问题,为什么信号量的状态(大概)会消失,以便在后续尝试中超时,或者直接在sem_wait上直接阻塞。看起来像libc或内核中的错误。
答案 5 :(得分:0)
我在Ubuntu 10.04 x86_64 Core i7机器上拍了一个程序。
当使用usleep(40000)运行时,程序运行了半小时或者无聊的事情。
当使用usleep(40)运行时,程序在我的机器冻结之前再运行了半小时,也许更多。 X死了。 Control + alt + F1-7死亡。我不能ssh。(可悲的是,这个愚蠢的Apple键盘没有sysrq键。我喜欢打字,但我肯定不需要f13,f14或f15。我会做可怕的事情来获得一个合适的sysrq密钥。)
绝对最好:我的日志中的 NOTHING 告诉我发生了什么。
$ uname -a
Linux haig 2.6.32-22-generic #36-Ubuntu SMP Thu Jun 3 19:31:57 UTC 2010 x86_64 GNU/Linux
与此同时,我还在浏览器中玩Java游戏(由stackoverflow用户发布,寻找反馈,有趣的转移:) - 所以有可能jvm负责发痒的东西来冻结我的机器固体