线程创建期间的程序执行流程

Posted

技术标签:

【中文标题】线程创建期间的程序执行流程【英文标题】:Flow of program execution during Thread creation 【发布时间】:2014-08-04 11:46:08 【问题描述】:

我是线程新手。

我已经编写了一个示例程序来创建一个线程。

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
#include<string.h>
#include<pthread.h>


void * func(void * temp)

    printf("inside function\n");
    return NULL;


int main()

    pthread_t pt1;
    printf("creating thread\n");
    pthread_create(&pt1,NULL,&func,NULL);
    printf("inside main created thread\n");

return 0;

编译后发现答案是:

creating thread
inside main created thread
inside function
inside function

我知道答案可能会有所不同,因为 return 0; 可能会在执行 func 中的 printf 之前被调用。 但是怎么解决的,inside function被打印了两次呢?

关于使用gcc -o temp thread1.c -lpthread 编译 第一次运行:

creating thread
inside main created thread

第二次运行:

creating thread
inside main created thread
inside function
inside function

关于使用gcc -pthread -o temp thread1.c 编译 第一次运行:

creating thread
inside main created thread
inside function
inside function

我在

上观察到了这种行为
gcc version: 4.4.3 (Ubuntu 4.4.3-4ubuntu5)
Kernel release:2.6.32-24-generic
glib version:2.11.1

【问题讨论】:

您应该始终加入(或分离)您创建的线程。 (但是您看到的行为有点奇怪。您是如何编译的?) 当我用$ gcc test.c -lpthread 编译它时,我根本没有得到“内部函数”输出。如果我使用pthread_join,“内部函数”会按预期打印一次。 奇怪!它必须与在工作线程中写入 io 缓冲区有关,但随后由主线程刷新,然后才能在工作线程中清除。换句话说,您正在使用的printf 的实现不是线程安全的(这可能不是新闻,我不知道)。但实际的解决方案就是像 Mat 所说的那样使用 join 或 create。 我已经看到了这种效果(在 Linux x86_64 机器上)。正如您所说,在 printf() 输出当前行之后,子进程似乎突然(和意外)停止,但在它更新其缓冲区状态之前。但是,我不认为这意味着线程安全问题......我认为这与实现突然线程终止的位置有关——这可能(有可能)在它完成真正的 I/O 之后立即发生(对底层 fd),处于低于 FILE 缓冲区处理的级别。 对于任何感兴趣的人,here is a tale 某人通过标准库的旅程试图确定对标准输出的写入实际发生的位置。这是一个关于复杂代码神秘宏定义的悲惨故事。 【参考方案1】:

我在 gcc 版本 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)、glib 版本 2.15 上观察到此问题,并带有 O2 标志。如果没有任何优化标志,则不会观察到此问题。

为什么输出很奇怪 C 语言规范没有提及任何特定的编译器、操作系统或 CPU。它引用了一个抽象机器,它是实际系统的概括。这个抽象机器(至少达到 C99 规范)是单线程的。所以标准库(包括printf)默认不需要是线程安全的。如果您跨线程使用标准库函数(使用某些库,例如 posix libpthread),则您有责任在访问非进入标准库函数之前添加同步(互斥量、信号量、condvar 等)。如果您不这样做,可能会不时出现令人惊讶的结果,您应自行承担使用风险。

在我可以重现此问题的环境中进行一些分析 分析为两个版本的标志生成的程序集,我找不到任何显着差异(值得注意的是,printfs 转换为puts

查看puts的来源

int
_IO_puts (str)
     const char *str;

  int result = EOF;
  _IO_size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);

  if ((_IO_vtable_offset (_IO_stdout) != 0
       || _IO_fwide (_IO_stdout, -1) == -1)
      && _IO_sputn (_IO_stdout, str, len) == len
      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
    result = MIN (INT_MAX, len + 1);

  _IO_release_lock (_IO_stdout);
  return result;


#ifdef weak_alias
weak_alias (_IO_puts, puts)
#endif

看来问题出在_IO_putc_unlocked('\n', _IO_stdout)。这可能会刷新流,并可能在更新流状态之前被杀死。

学习多线程编码 当主线程返回时,它终止整个进程。这包括所有其他线程。所以通知所有子线程退出(或使用pthread_kill)并使用pthread_exit 使主线程退出或使用pthread_join

【讨论】:

感谢您添加的详细信息。关于您的声明,我有一个后续问题,_IO_putc_unlocked() 可能会在更新流状态之前刷新流并被杀死。这是否意味着字符被推送到屏幕/控制台但在线程被杀死时以某种方式保留在缓冲区中,然后在主线程退出时再次被刷新? @ScottLawson 我不会称自己为 glibc 专家。由于奇怪的行为,我试图深入研究。是的,似乎缓冲区首先被复制到控制台然后被清除。但这一次,死线程无法清除它。 谢谢!这是我一直在寻找的那种细节,这些特定的函数名称为我进一步挖掘提供了一个很好的起点。【参考方案2】:

通常你不能假设 printf 和相关的内存结构是线程安全的。这取决于 stdio 库的实现方式。 特别是,当线程和进程终止时可能会发生故障,因为运行时库通常会在退出之前刷新输出缓冲区。我已经看到过这样的行为,解决方案通常是互斥锁或信号量来保护输出操作(更准确地说,是保护对 FILE 对象的访问)。

【讨论】:

【参考方案3】:

对于初学者,据我所知,使用“-pthread”编译等同于使用“-D_REENTRANT -lpthread”编译,所以这是唯一的区别。 请注意,printf 等是不可重入的,因为它们在全局缓冲区上运行。

话虽如此,但很遗憾,我无法重新创建问题的 有趣 部分(线程目标函数中的 printf 似乎已被调用了两次)。每种编译方法(-lpthread 和 -pthread)都给了我相同的结果:我从 main 内部获取打印,但没有从线程目标内部打印(正如您在第一次运行时看到的那样)。我认为这只是一个时间问题,线程目标在主退出之前没有“绕过”打印。事实上,在从 main 返回之前只睡 1/100 秒就可以让我打印线程目标函数。试一试,让我们知道你看到了什么:

#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
#include<string.h>
#include<pthread.h>
#include <unistd.h>

void * func(void * temp)

    printf("inside function\n");
    return NULL;


int main()

    pthread_t pt1;
    printf("creating thread\n");
    pthread_create(&pt1,NULL,&func,NULL);
    printf("inside main created thread\n");

    /* ZzzzzZZZ... */
    usleep(10000);

    return 0;

我玩弄了延迟时间,甚至是 1/1000000 秒: 睡眠(1); 我仍然得到了我所有预期的 printfs。随着睡眠延迟的减少,打印更有可能发生乱序,这是我希望看到的。

所以关于多重打印:正如我之前的许多人指出的那样,printf 等操作在全局结构上,并且不是可重入的。如果您在 printf 之后刷新标准输出,我很想看看您的输出,所有这些都受到互斥锁的保护:

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void * func(void * temp)

    pthread_mutex_lock(&mutex);
    printf("inside function\n");
    fflush(stdout);
    pthread_mutex_unlock(&mutex);

    return NULL;


int main()

    pthread_t pt1;

    pthread_mutex_lock(&mutex);
    printf("creating thread\n");
    fflush(stdout);
    pthread_mutex_unlock(&mutex);

    pthread_create(&pt1,NULL,&func,NULL);

    pthread_mutex_lock(&mutex);
    printf("inside main created thread\n");
    fflush(stdout);
    pthread_mutex_unlock(&mutex);

    usleep(10000);

    return 0;

编辑

对不起,当我在上面建议使用 fflush() 时,我并没有 100% 清楚。我认为问题在于您在将字符推送到屏幕和刷新缓冲区之间被打断了。当缓冲区实际上真正刷新时,您实际上已经推送了两次字符串。

【讨论】:

您的帖子内容丰富,但没有回答原始问题。 对不起,我只是暗示了我的意思。它已被编辑【参考方案4】:

我无法发表评论,但有 several duplicate questions 提供更多信息的答案。 glibc bug 可能是意外行为的原因,正如其他答案中所述,pthread_exitpthread_join 是建议的解决方法。

【讨论】:

以上是关于线程创建期间的程序执行流程的主要内容,如果未能解决你的问题,请参考以下文章

java线程池原理

线程的创建和运行

Java 线程池使用详解

线程池的执行流程?

线程池的执行流程?

为啥线程在进程间通信期间会破坏命名管道?