我有一个多线程C基准测试,可以描述如下:
Thread 1 Thread 2 Thread 3 Control thread
while(1) while(1) while(1) while(1)
| | |
| | | |
| | | every one second:
| | | wait for other threads to be blocked
| | | do something with S values
| | | |
| | | |
write S1 write S2 write S3 |
| | | |
| | | |
barrier barrier barrier barrier
我的问题与上图中的wait for other threads to be blocked
语句有关。现在我来到以下解决方案来实现它:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <inttypes.h>
#define NB_THREADS 11
pthread_barrier_t b;
uint8_t blocked_flags[NB_THREADS] = {0};
pthread_mutex_t blocked_flags_mutexes[NB_THREADS];
uint64_t states[NB_THREADS] = {0};
uint64_t time_diff_get(struct timespec *start, struct timespec *end) {
uint64_t end_ns = end->tv_sec * 1E9 + end->tv_nsec;
uint64_t start_ns = start->tv_sec * 1E9 + start->tv_nsec;
uint64_t res = end_ns - start_ns;
return res;
}
static void *worker_thread(void *arg) {
uint8_t id = *((uint8_t *)arg);
int a = 0;
while(1) {
for (int i = 0; i < 1000; i++) {
a++;
}
states[id]++;
pthread_mutex_lock(&blocked_flags_mutexes[id]);
blocked_flags[id] = 1;
pthread_mutex_unlock(&blocked_flags_mutexes[id]);
pthread_barrier_wait(&b);
pthread_mutex_lock(&blocked_flags_mutexes[id]);
blocked_flags[id] = 0;
pthread_mutex_unlock(&blocked_flags_mutexes[id]);
}
printf ("a = %d\n", a);
return NULL;
}
static void *control_thread() {
struct timespec last_time;
clock_gettime(CLOCK_REALTIME, &last_time);
while(1) {
struct timespec time;
clock_gettime(CLOCK_REALTIME, &time);
if (time_diff_get(&last_time, &time) >= 1E9) {
// Wait for all threads to be blocked
for (int i = 0; i < NB_THREADS; i++) {
while (1) {
pthread_mutex_lock(&blocked_flags_mutexes[i]);
if (blocked_flags[i] == 1) {
pthread_mutex_unlock(&blocked_flags_mutexes[i]);
break;
}
pthread_mutex_unlock(&blocked_flags_mutexes[i]);
}
}
for (int i = 0; i < NB_THREADS; i++) {
pthread_mutex_lock(&blocked_flags_mutexes[i]);
if (blocked_flags[i] == 0) {
printf("How could I avoid to be there ??\n");
exit(-1);
}
pthread_mutex_unlock(&blocked_flags_mutexes[i]);
}
// Do some intersting stuff here with states array
// .....
// .....
// Save last time
clock_gettime(CLOCK_REALTIME, &last_time);
}
pthread_barrier_wait(&b);
}
return NULL;
}
int main() {
// Init barrier
pthread_barrier_init(&b, NULL, NB_THREADS + 1);
// Create worker threads
pthread_t threads[NB_THREADS];
uint8_t ids[NB_THREADS];
for (int i = 0; i < NB_THREADS; i++) {
ids[i] = i;
pthread_mutex_init(&blocked_flags_mutexes[i], NULL);
}
for (int i = 0; i < NB_THREADS; i++) {
pthread_attr_t attr;
pthread_attr_init(&attr);
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(i + 1, &cpu_set);
pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpu_set);
pthread_create(&threads[i], &attr, worker_thread, &ids[i]);
}
// Create control thread
pthread_t ctrl_thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpu_set);
pthread_create(&ctrl_thread, &attr, control_thread, NULL);
// Join on worker threads
for (int i = 0; i < NB_THREADS; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
但是在12核英特尔平台上运行这个用gcc -O0
编译的基准测试清楚地告诉我,我有一个&#34;比赛&#34;问题在某处,因为该过程总是在几秒钟后退出消息。我该如何解决这个问题?
注意:在我考虑使用自定义屏障的其他问题后,我需要继续使用pthread_barrier,而不是在互斥锁和cond变量之上实现屏障。
答案 0 :(得分:1)
您的代码有明显的竞争条件。当您的线程被屏障等待解锁时,它们会将标志重置为零。在他们这样做之前,他们的旗帜在一段时间内仍然是1。控制线程可以观察到这个陈旧值为1,并认为相应的线程已经准备好阻塞,而实际上该线程只是要清除标志,刚刚走出障碍等待:
// worker thread
pthread_barrier_wait(&b);
// No longer blocked, but blocked_flags[id] is still 1.
// At this point, the control thread grabs the mutex, and observes the 1 value
// The mistake is thinking that 1 means "I'm about to block"; it actually
// means, "I'm either about to block on the barrier, or have just finished".
pthread_mutex_lock(&blocked_flags_mutexes[id]);
blocked_flags[id] = 0;
pthread_mutex_unlock(&blocked_flags_mutexes[id]);
这种竞争条件足以愚弄每个人都被阻止的控制线程,从而通过它的第一个循环。然后它落入第二个循环,它发现并非所有标志都为零。
你的问题的本质是你有一些重复的,循环的并行处理由一群线程完成,由一个屏障控制。但是,您在循环中仅使用一个屏障等待,这意味着循环只有一个阶段。但是,从语义上讲,您的周期分为两个阶段:线程被阻塞和解除阻塞。您为区分这些阶段而构建的机制不是线程安全的;显而易见的解决方案是再次使用屏障将周期分成更多阶段。
POSIX障碍具有“串行线程”功能:其中一个等待线程被告知它是特殊的。这允许您实现特殊阶段,其中只有串行线程执行一些重要操作,而其他线程可以执行其他操作,例如调用屏障等待跳到下一阶段。这应该消除了实现hacks的需要,比如标志,一个线程试图猜测其他线程何时变为静止。
注意:您无法在POSIX屏障等待中选择哪个线程是串行线程,因此您不能仅为该操作设置专用控制线程。您只需使用N个线程,而不是N + 1个线程。他们都做同样的事情,当他们到达障碍时;他们中的任何一个都可以被告知它是串行线程。基于此,串行线程执行一些替代代码与其他代码相比。
所以,图表时间:
while(1) while(1) while(1)
| | |
| | |
| | |
| | | <---- WRITE PHASE
| | |
| | |
| | |
write S1 write S2 write S3
| | |
| | |
barrier barrier barrier
| | |
| | | <--- CHECK PHASE
| | |
| | serial thread!
| | |
| | next second?-- YES -> do something with S values!
| | | NO |
| | | |
| | +------------+
| | |
barrier barrier barrier
| | |
| | |
back to top, next WRITE PHASE.
这里,在CHECK PHASE
中,串行线程(可以是N个线程中的任何一个)执行检查:自上次转换到下一秒以来,时间是否已转换到下一秒?如果是这样,它会对S值做一些事情。
屏障确保其他线程不会触及CHECK_PHASE
中的值,因此串行线程不需要互斥锁来处理S值!您已为此同步付费已经在每个循环中进行了额外的屏障调用。
你可以有一个额外的线程提供一个时基:它的工作是睡眠,直到下一秒到达,然后递增一个计数器。串行线程只需检查此计数器是否已递增(相对于其旧值,存储在另一个变量中)。然后执行操作并更新旧计数器以匹配新计数器。这样,您就不必调用操作系统来获取主处理循环中的当前时间。
答案 1 :(得分:0)
不是为每个工作线程保留一个标志,而是可以互斥保护单个计数器,并且每个工作线程可以在屏障释放后立即阻止和减少该计数器。这将使您免于等待第一个线程被阻止,然后是第二个线程,然后是第三个线程等等。
我不知道你的控制线程退出的位置(除了处于意外情况之外)并且主线程似乎没有等待它。
也许您还想在工作线程之前创建控制线程。
也许您还希望通过让他们在释放并开始实际工作之前等待屏障来同步工作线程和控制线程。
答案 2 :(得分:0)
我认为发生的事情可能就是这样:
while(1)
的第一次执行中,time_diff_get(&last_time, &time)
返回一个值&lt; 1E9,所以线程直接进入障碍control_thread()
第二次执行它的循环并立即检查blocked_flags[i]
很抱歉,我目前无法提供解决方案,但如果我正确理解问题是解决方案的良好开端。