多个线程在循环中运行时如何影响索引

我试图编写一个运行5个线程并相应打印其索引的程序。

以下是代码:

#include  #include  #include  int nthreads=5; void *busy(void* c) { int my_busy = *(int *) c; printf("Hello World with thread index %d\n", my_busy); return NULL; } int main() { pthread_t t1[nthreads]; void* result; for(int i=0; i<nthreads; i++) { pthread_create(&t1[i], NULL, busy, &i); } for(int i=0; i<nthreads; i++) { pthread_join(t1[i], &result); } return 0; } 

获得的产出:

 Hello World with thread index 1 Hello World with thread index 4 Hello World with thread index 2 Hello World with thread index 0 Hello World with thread index 0 

虽然所有5个线程都运行为什么没有正确输出相应的索引? 为什么我倾向于松散一些索引并让其他人两次? 例如,在这种情况下,我输了3,输出0两次。 虽然在一个循环中使用pthread_joinpthread_create可以解决问题但它不会安排所有线程并行运行。 在这种情况下应该怎么做才能打印所有索引?

虽然所有5个线程都运行为什么没有正确输出相应的索引?

您将指向变量的指针传递给每个线程,并在线程函数访问它的同时修改该变量。 为什么期望线程函数看到任何特定值? 它们同时运行 。 在一些架构上,如果一个线程正在读取值而另一个线程正在修改它,则线程可能会看到完全不可能的乱码值。

例如,在这种情况下,我输了3,输出0两次。

尽管由例如GCC生成的机器代码在创建每个线程函数之后增加线程函数访问的变量,但是线程函数观察到的值在某些体系结构上可能是“旧的”,因为没有使用障碍或同步。

这是否发生在您的特定计算机上(没有明确的障碍或同步),取决于您的计算机实现的内存排序模型 。


例如,在x86-64(又名AMD64; 64位Intel / AMD架构)上,按顺序观察所有读取和写入, 但可以在加载后订购存储 。 这意味着如果最初说i = 0; ,线程A确实i = 1; ,即使线程A修改了变量,线程B仍然可以看到i == 0

请注意,在使用大多数C编译器时,使用提供的x86 / AMD64内在函数添加障碍(例如_mm_fence() )不足以确保每个线程都看到唯一值,因为每个线程的开始都可以延迟尊重调用pthread_create()时的真实世界时刻。 他们确保的最多只有一个线程看到零值。 两个线程可能看到值1,三个值2,依此类推; 所有线程甚至可以看到值5。

在这种情况下应该怎么做才能打印所有索引?

最简单的选择是提供要作为值打印的索引,而不是作为指向变量的指针。 在busy()中,使用

 my_busy = (int)(intptr_t)c; 

在main()中,

 pthread_create(&t1[i], NULL, busy, (void *)(intptr_t)i); 

intptr_t类型是一个能够保存指针的有符号整数类型,在定义(通常包括 )。

(由于问题标记为linux ,我可能应该指出,在Linux中,在所有体系结构中,您可以使用long而不是intptr_t ,而使用unsigned long而不是uintptr_tlongunsigned long中没有陷阱表示,并且每个可能的long / unsigned long值可以转换为唯一的void * ,反之亦然;往返保证正常工作。内核系统调用接口要求,因此将来也不太可能改变。)


如果需要将指针传递给i ,但希望每个线程都能看到一个唯一值,则需要使用某种同步。

最简单的同步方法是使用信号量。 您可以将其设置为全局,但使用结构来描述工作参数,并传递结构的指针(即使相同的一个用于所有工作线程)也更强大:

 #include  #include  #include  #include  #include  #define NTHREADS 5 struct work { int i; sem_t s; }; void *worker(void *data) { struct work *const w = data; int i; /* Obtain a copy of the value. */ i = w->i; /* Let others know we have copied the value. */ sem_post(&w->s); /* Do the work. */ printf("i == %d\n", i); fflush(stdout); return NULL; } int main() { pthread_t thread[NTHREADS]; struct work w; int rc, i; /* Initialize the semaphore. */ sem_init(&w.s, 0, 0); /* Create the threads. */ for (i = 0; i < NTHREADS; i++) { /* Create the thread. */ wi = i; rc = pthread_create(&thread[i], NULL, worker, &w); if (rc) { fprintf(stderr, "Failed to create thread %d: %s.\n", i, strerror(rc)); exit(EXIT_FAILURE); } /* Wait for the thread function to grab its copy. */ sem_wait(&w.s); } /* Reap the threads. */ for (i = 0; i < NTHREADS; i++) { pthread_join(thread[i], NULL); } /* Done. */ return EXIT_SUCCESS; } 

因为主线程(修改每个工作线程看到的值的线程)参与同步,所以每个工作器函数在创建下一个线程之前读取值,输出将始终按i递增顺序。


一个更好的方法是创建一个工作池 ,其中主线程定义由线程共同完成的工作,并且线程函数只需以任何顺序获得要完成的下一部分工作:

 #define _POSIX_C_SOURCE 200809L #include  #include  #include  #include  #include  #include  #define NTHREADS 5 #define LOOPS 3 struct work { pthread_mutex_t lock; int i; }; void *worker(void *data) { struct work *const w = data; int n, i; for (n = 0; n < LOOPS; n++) { /* Grab next piece of work. */ pthread_mutex_lock(&w->lock); i = w->i; w->i++; pthread_mutex_unlock(&w->lock); /* Display the work */ printf("i == %d, n == %d\n", i, n); fflush(stdout); } return NULL; } int main(void) { pthread_attr_t attrs; pthread_t thread[NTHREADS]; struct work w; int i, rc; /* Create the work set. */ pthread_mutex_init(&w.lock, NULL); wi = 0; /* Thread workers don't need a lot of stack. */ pthread_attr_init(&attrs); pthread_attr_setstacksize(&attrs, 2 * PTHREAD_STACK_MIN); /* Create the threads. */ for (i = 0; i < NTHREADS; i++) { rc = pthread_create(thread + i, &attrs, worker, &w); if (rc != 0) { fprintf(stderr, "Error creating thread %d of %d: %s.\n", i + 1, NTHREADS, strerror(rc)); exit(EXIT_FAILURE); } } /* The thread attribute set is no longer needed. */ pthread_attr_destroy(&attrs); /* Reap the threads. */ for (i = 0; i < NTHREADS; i++) { pthread_join(thread[i], NULL); } /* All done. */ return EXIT_SUCCESS; } 

如果您编译并运行最后一个示例,您会注意到输出可能是奇数顺序,但每个i都是唯一的,并且每个n = 0n = LOOPS-1恰好出现NTHREADS次。

因为“忙”function启动时索引值已经改变。 最好将单独的参数副本传递给线程proc。