理解并行线程执行

4

编写简单的C代码,尝试控制来自两个不同线程的输出:

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

sem_t sem;

void* thread_func(void* aArgs)
{
  printf("Entering thread %p with %d\n", (void*)pthread_self(), (int)aArgs);
  int i = 0;
  for(;i < 10; i++)
  {
    sem_wait(&sem);
    if ((i % 2) == (int)aArgs)
      printf("val is %d in thread %p \n", i, (void*)pthread_self());
    sem_post(&sem);
  }
}

int main()
{
  pthread_t thread_1, thread_2;

  sem_init(&sem, 0, 1);

  pthread_create(&thread_1, NULL, (void*)thread_func, (void*)0);
  pthread_create(&thread_2, NULL, (void*)thread_func, (void*)1);

  pthread_join(thread_1, NULL);
  pthread_join(thread_2, NULL);

  sem_destroy(&sem);

  return 0;
}

我想要实现的是交替排列的奇数和偶数。但是,我却从一个线程中得到了所有数字,然后从另一个线程中得到了所有其他数字,就像这样(即使我增加循环计数器的数量):
Entering thread 0xb75f2b40 with 0
val is 0 in thread 0xb75f2b40 
val is 2 in thread 0xb75f2b40 
val is 4 in thread 0xb75f2b40 
val is 6 in thread 0xb75f2b40 
val is 8 in thread 0xb75f2b40 
Entering thread 0xb6df1b40 with 1
val is 1 in thread 0xb6df1b40 
val is 3 in thread 0xb6df1b40 
val is 5 in thread 0xb6df1b40 
val is 7 in thread 0xb6df1b40 
val is 9 in thread 0xb6df1b40

问题是为什么两个独立的线程表现得像它们是两个顺序任务?为什么第二个线程没有控制执行,直到第一个线程完成所有工作?
我尝试在for循环的末尾添加pthread_yield(),但情况并没有显著改变:有时我得到期望的输出,有时就像上面描述的那样。
更新。如何实现确定性的逐一线程执行?是否有任何同步原语可以实现这一点?

1
@Milind:换行符已经会导致打印刷新了。睡眠如何有帮助呢?请注意,OP已经使用信号量来锁定。 - Oliver Charlesworth
就我所知,当运行您的代码时,我没有观察到您所描述的行为。您确定这是您编译和运行的确切代码吗? - Oliver Charlesworth
@OliverCharlesworth 可能你的线程正在并行运行。在我的(单线程)虚拟机上,我得到了与 OP 相同的输出。 - Daniel Kleinstein
1
如果你想让你的线程相互中断,只需增加线程函数的执行时间,像这样:http://pastebin.com/0RMGLzcN。你会看到像这样的输出:http://pastebin.com/r77Mx8DM。问题是,内核调度程序有一个称为“量子”或“时间片”的参数,它是进程切换(抢占)之间的时间周期。你需要使函数的执行时间大于调度程序的“时间片”。 - Sam Protsenko
1
更多链接: [1] http://en.wikipedia.org/wiki/Preemption_%28computing%29#Time_slice [2] https://dev59.com/1WQo5IYBdhLWcg3wBLUo - Sam Protsenko
显示剩余9条评论
4个回答

4
如果您想获得所需的输出,应该使用两个信号量而不是一个。每个线程都应在自己的信号量上等待,并在每个循环迭代完成后发布另一个线程的信号量。主线程可以创建一个值为1的信号量和另一个值为0的信号量来正确启动。这将强制两个线程以交替顺序运行。
当前编写的程序中,进行一次“sem_post”后跟一次“sem_wait”,可能会导致同一线程立即获取信号量(在单CPU系统上)。我很惊讶“pthread_yield”没有帮助,但使用两个信号量将保证无论如何都能正确排序。

我不太确定这如何解决问题 - 与 OP 的问题一样,信号量等于 1 的线程不会等待,而是立即恢复执行。它只等待自己的信号量。 - Daniel Kleinstein
@DanielKleinstein Thread1 等待 Thread2 的信号量,完成工作后在 Thread1 的信号量上发布。Thread2 则相反。您只需要初始化信号量,以便其中一个线程开始第一次循环迭代。 - nos
@nos 哦,明白了。+1,优雅(虽然不太适用于超过2个线程)。 - Daniel Kleinstein
1
我在另一个答案中提供了解决方案的代码。 - Sam Protsenko

2
只是想为JS1answer演示代码,适用于任意数量的线程:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define NUM 3

static sem_t sem[NUM];

static void *thread_func(void *args)
{
    int i;

    for (i = 0; i < 10; ++i) {
        int cur = (long)args;        /* current thread number */
        int next = (cur + 1) % NUM;  /* next thread number*/

        if ((i % NUM) != cur)
            continue;

        sem_wait(&sem[cur]); /* lock this thread's semaphore */
        printf("val is %d, thread num = %ld\n", i, (long)args);
        sem_post(&sem[next]); /* unlock next thread's semaphore */
    }

    return NULL;
}

int main(void)
{
    size_t i;

    pthread_t t[NUM];

    for (i = 0; i < NUM; ++i)
        sem_init(&sem[i], 0, 0); /* locked */

    for (i = 0; i < NUM; ++i)
        pthread_create(&t[i], NULL, thread_func, (void *)i);

    sem_post(&sem[0]);

    for (i = 0; i < NUM; ++i)
        pthread_join(t[i], NULL);

    for (i = 0; i < NUM; ++i)
        sem_destroy(&sem[i]);

    return 0;
}

输出:

val is 0, thread num = 0
val is 1, thread num = 1
val is 2, thread num = 2
val is 3, thread num = 0
val is 4, thread num = 1
val is 5, thread num = 2
val is 6, thread num = 0
val is 7, thread num = 1
val is 8, thread num = 2
val is 9, thread num = 0

不错的解决方案。如果有三个线程,我们只需要锁定执行线程的信号量并解锁所有其他线程的信号量吗? - ars
1
对于三个线程,每个线程都应该解锁下一个线程的信号量。因此,线程0应该解锁线程1,线程1解锁线程2,线程2解锁线程0。 - JS1
@sam 感谢你编写代码来演示我的意思。我现在正在使用平板电脑,所以很难打出代码。 - JS1
@ars,我已经重写了代码以支持任意数量的线程,请编辑“NUM”常量。 - Sam Protsenko
@JS1,没问题。顺便说一句,很好的解决方案! - Sam Protsenko

1
你在循环的同一次迭代中不断调用sem_waitsem_post,因此线程在其时间片期间保持对信号量的控制 - 一旦调用sem_postsem_wait将立即在以下迭代中(在同一线程中)再次调用。

以下是使用条件变量解决问题的方法:

pthread_mutex_t mut;
pthread_cond_t print_cond;
int print_thread; //equals 0 or 1

这些是用于在两个线程之间同步输出的全局变量。当我们想要第一个线程打印时,print_thread等于0,当我们想要第二个线程打印时,它等于1。
thread_func内部:
for(;i < 10; i++)
{
    pthread_mutex_lock(&mut);
    if ((i % 2) == (int)aArgs){
        while (print_thread != (int)aArgs){
            pthread_cond_wait(&print_cond, &mut);
        }
        printf("val is %d in thread %p \n", i, (void*)pthread_self());
        print_thread = 1 - (int)aArgs;
        pthread_cond_signal(&print_cond);
        pthread_mutex_unlock(&mut);
    } else {
        pthread_mutex_unlock(&mut);
    }
}

使用这段代码,您应该获得类似以下的输出:
Entering thread 0xb6fbcb70 with 1
Entering thread 0xb77bdb70 with 0
val is 0 in thread 0xb77bdb70 
val is 1 in thread 0xb6fbcb70 
val is 2 in thread 0xb77bdb70 
val is 3 in thread 0xb6fbcb70 
val is 4 in thread 0xb77bdb70 
val is 5 in thread 0xb6fbcb70 
val is 6 in thread 0xb77bdb70 
val is 7 in thread 0xb6fbcb70 
val is 8 in thread 0xb77bdb70 
val is 9 in thread 0xb6fbcb70 

请注意,此解决方案适用于使用超过两个线程进行打印:唯一需要更改的是适当更新“print_thread”。

我不确定这是否正确。一旦一个线程发布,另一个等待的线程应立即接管。 - Oliver Charlesworth
@Daniel Kleinstein:当我将sem_wait()和sem_post()的调用移动到if()语句下面时,没有任何变化。 - ars
@OliverCharlesworth 无法保证在大多数系统上,基于信号量的等待不是一个先进先出队列,而且解除一个对信号量的等待并不一定会强制切换到该进程的上下文——因此,刚刚解除互斥锁的进程很可能会立即再次获取它。然而,在多处理器系统上,其他线程被唤醒并获取互斥锁的几率较高。 - nos

0

pthread_yield() 是一个非标准的调用,应该使用 sched.h 中的 sched_yield()

另外,我建议将信号量初始化为 0,并在创建两个线程后调用 sem_post

因此,线程的代码如下:

for(;i < 10; i++)
{
    sem_wait(&sem);
    if ((i % 2) == (int)aArgs)
        printf("val is %d in thread %p \n", i, (void*)pthread_self());
    sem_post(&sem);
    sched_yield();
}

而在主函数中:

sem_init(&sem, 0, 0);

pthread_create(&thread_1, NULL, (void*)thread_func, (void*)0);
pthread_create(&thread_2, NULL, (void*)thread_func, (void*)1);

sem_post(&sem);

得到的是什么:

Entering thread 0x7f74c7697700 with 0
val is 0 in thread 0x7f74c7697700 
Entering thread 0x7f74c6e96700 with 1
val is 2 in thread 0x7f74c7697700 
val is 4 in thread 0x7f74c7697700 
val is 1 in thread 0x7f74c6e96700 
val is 3 in thread 0x7f74c6e96700 
val is 5 in thread 0x7f74c6e96700 
val is 7 in thread 0x7f74c6e96700 
val is 6 in thread 0x7f74c7697700 
val is 9 in thread 0x7f74c6e96700 
val is 8 in thread 0x7f74c7697700 

对我来说不起作用。看起来调度程序每次重新调度后都会返回到第一个线程,因此它高度依赖于调度程序策略和 CPU 频率。因此,这个答案是不确定的。 - Sam Protsenko
OP问如何让每个线程逐行打印。 - Daniel Kleinstein

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接