pthread_join 导致奇怪的执行顺序

pthread_join causes a weird execution sequence

这是我的测试 C 程序如下:

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

__thread int var = 0;

void* worker(void* arg);

int main()
{
    pthread_t pid1, pid2;

    pthread_create(&pid1, NULL, worker, (void*)0);
    pthread_create(&pid2, NULL, worker, (void*)1);

    printf("-----------1----------\n");
    pthread_join(pid1, NULL);
    sleep(1);
    printf("-----------2----------\n");
    pthread_join(pid2, NULL);

    return 0;
}

void* worker(void* arg)
{
    int idx = (int)arg;
    int i;

    for (i = 0; i < 10; ++i) {
        printf("thread: %d  ++var = %d\n",
            idx,
            ++var);
    }
}

然后我编译如下:

$ gcc -g -Wall -pthread 1.c -lpthread -o test
1.c: In function ‘worker’:
1.c:27:15: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
   27 |     int idx = (int)arg;
      |               ^
1.c:35:1: warning: control reaches end of non-void function [-Wreturn-type]
   35 | }
      | ^

运行结果如下:1秒后最后一行出来。但我不明白为什么 "thread: 1" 出现在 "thread: 0" 之前?

$ ./test 
-----------1----------
thread: 1  ++var = 1
thread: 1  ++var = 2
thread: 1  ++var = 3
thread: 1  ++var = 4
thread: 1  ++var = 5
thread: 1  ++var = 6
thread: 1  ++var = 7
thread: 1  ++var = 8
thread: 1  ++var = 9
thread: 1  ++var = 10
thread: 0  ++var = 1
thread: 0  ++var = 2
thread: 0  ++var = 3
thread: 0  ++var = 4
thread: 0  ++var = 5
thread: 0  ++var = 6
thread: 0  ++var = 7
thread: 0  ++var = 8
thread: 0  ++var = 9
thread: 0  ++var = 10
-----------2----------

why "thread: 1" comes before "thread: 0"?

只是线程1先得到了CPU时间,而且速度很快,可以全部打印出来

最有可能发生的事情是,main() 有 CPU 时间,然后 main() 运行 pthread_join,在这种情况下它产生处理器时间并且调度程序启动。然后调度程序决定给线程 1 CPU 时间——可能是最后一个,任意选择。线程足够快,可以在调度程序能够重新安排 CPU 时间之前将其全部打印出来。

线程彼此无序 - 你不能期望任何顺序,除了 printf 输出不应该混合,即 printf 本身是线程安全的。一个线程在另一个线程之前打印与任何其他结果一样没有顺序。

warning: cast from pointer to integer of different size 

执行 (uintptr_t)arg; 以消除警告。

warning: control reaches end of non-void function

这是一个非常严重的警告,会导致未定义的行为。特别是,最近我探索了这个确切的问题如何导致 与您的代码非常相似。在函数末尾添加return NULL;

想象一下多核系统中的情况,其中每个线程执行都分配给不同的处理器,它们将在某种程度上相互独立地执行代码,如果我们想要一个更快的程序,如果一个必须等待另一个线程结束才能开始,这样会使两个线程的使用变得无用。

pthread_joins 保证的是,在两个线程都结束各自的工作之前,程序不会继续。

如果你想控制执行流程,那么你不应该有一个以上的线程,或者你必须自己同步执行,在这种情况下你可以使用互斥锁,它看起来像:

void* worker(void* arg);

typedef struct { // shared data
    int idx;
    int var;
    pthread_mutex_t* mutex_ptr; 
} Data;
int main()
{
    pthread_mutex_t mutex; 
    pthread_mutex_init(&mutex, NULL);

    Data data = {.idx = 0, .var = 0, .mutex_ptr = &mutex};

    pthread_t pid1, pid2;

    pthread_create(&pid1, NULL, worker, &data);
    pthread_create(&pid2, NULL, worker, &data);
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);

    pthread_mutex_destroy(&mutex);

}
void* worker(void* arg)
{
    Data* data = (Data*)arg;
    int i;

    pthread_mutex_lock(data->mutex_ptr);
    printf("\n----------%d-----------\n", data->idx);
    for (i = 0; i < 10; ++i) {
        printf("thread: %d  ++var = %d\n",
            data->idx,
            ++data->var);
    }
    data->idx++;
    data->var = 0;
    pthread_mutex_unlock(data->mutex_ptr);
    return NULL; // return type of worker is void* so it must return a pointer
}

在此 live sample 中,您可以看到使用与不使用互斥锁时的区别。我必须添加一个更大的循环才能真正看到它。

带互斥量的预期输出:

----------0-----------
thread: 0  ++var = 1
thread: 0  ++var = 2
thread: 0  ++var = 3
thread: 0  ++var = 4
thread: 0  ++var = 5
thread: 0  ++var = 6
thread: 0  ++var = 7
thread: 0  ++var = 8
thread: 0  ++var = 9
thread: 0  ++var = 10

----------1-----------
thread: 1  ++var = 1
thread: 1  ++var = 2
thread: 1  ++var = 3
thread: 1  ++var = 4
thread: 1  ++var = 5
thread: 1  ++var = 6
thread: 1  ++var = 7
thread: 1  ++var = 8
thread: 1  ++var = 9
thread: 1  ++var = 10

没有互斥锁任何命令都是有效的,例如

----------0-----------
thread: 0  ++var = 1
thread: 0  ++var = 2
thread: 0  ++var = 3
thread: 0  ++var = 4
thread: 1  ++var = 1
thread: 0  ++var = 5

----------1-----------
thread: 0  ++var = 6
thread: 1  ++var = 2
thread: 0  ++var = 7
thread: 1  ++var = 3
thread: 0  ++var = 8
thread: 1  ++var = 4
...
...
...

请注意,由于我们引入的同步,同步示例在速度方面几乎没有利用线程,程序的行为方式类似于我们使用单线程,但因为我们同步我们可以控制对数据的访问,请注意互斥量是共享的,还要注意这是一个示例,但请记住关键部分,锁内的内容,应该只是必要的数据,尽可能避免循环。