如何在多线程中使用printf()函数

27

我正在实现一个多线程程序,使用不同的核心,许多线程同时执行。每个线程都会调用printf(),结果不易读取。

如何使printf()原子化,以便一个线程中的printf()调用不会与另一个线程中的printf()调用冲突?


不确定你想表达什么意思。但是我怎么能在不使用库的情况下完成它,我的意思是使用信号量等方法。 - user3242743
2
创建一个printf的包装函数并使用互斥锁如何? - ccKep
定义“冲突”。printf在你所说的意义上是“原子性”的,但它无法阻止多个线程写入相同的输出目标。 - Duck
1
@Duck:不确定你说的是否正确。我认为printf使用的缓冲区可能会受到其他线程的影响。 - user3242743
展示一个代码示例来说明这个问题,就像@Duck所说的那样,我从未见过两个“printf”写入交错的情况。 - Jim Mischel
就我所知,我曾经看到一个日志记录辅助函数执行了以下操作:printf( "xyz [%s]: ", level ); printf( fmt, args ); - 这会导致多线程问题,需要显式锁定才能解决。 - zeroimpl
3个回答

28

POSIX规范

POSIX规范包括以下函数:

  • getc_unlocked() (获取字符)
  • getchar_unlocked() (获取标准输入流中的字符)
  • putc_unlocked() (输出字符)
  • putchar_unlocked() (向标准输出流中写入一个字符)
提供函数getc()getchar()putc()putchar()的版本,分别命名为getc_unlocked()getchar_unlocked()putc_unlocked()putchar_unlocked()。它们在功能上与原始版本等效,但不要求以完全线程安全的方式实现。只有在受到flockfile()(或ftrylockfile())和funlockfile()保护的范围内使用时,它们才是线程安全的。如果且仅当调用线程拥有(FILE *)对象并且在成功调用flockfile()ftrylockfile()函数后的情况下,这些函数才能在多线程程序中安全地使用。

这些函数的规范提到:

  • flockfile() - 获取与文件关联的锁定
  • ftrylockfile() - 尝试获取与文件关联的锁定,如果无法获取则立即返回
  • funlockfile() - 释放与文件关联的锁定

flockfile()等函数的规范包括一个普遍要求:

所有引用(FILE *)对象的函数(除了那些以_unlocked结尾的函数),都应该像内部使用flockfile()funlockfile()一样来获取这些(FILE *)对象的所有权。

这个规范取代了之前版本答案中建议的代码。POSIX标准也指定:

[*lockfile()]函数的行为就好像每个(FILE *)对象都有一个锁计数。当(FILE *)对象被创建时,该计数会隐式地初始化为零。当该计数为零时,(FILE *)对象未被锁定。当计数为正时,单个线程拥有(FILE *)对象。当调用flockfile()函数时,如果计数为零或者计数为正且调用者拥有(FILE *)对象,则计数将被增加。否则,调用线程将被暂停,等待计数返回到零。每次调用funlockfile()都会减少该计数。这可以使匹配flockfile()(或成功调用ftrylockfile())和funlockfile()的调用嵌套。

此外还有字符I/O函数的规范:

这里记录了格式化输出函数的文档:

printf()规范中的一个关键条款是:

fprintf()printf()生成的字符被打印,就像调用了fputc()一样。

请注意使用“仿佛”的用法。但是,每个printf()函数都需要应用锁定,以便在多线程应用程序中控制对流的访问。同一时间只有一个线程可以使用给定的文件流。如果操作是用户级调用fputc(),则其他线程可以插入输出。如果操作是用户级调用,如printf(),则整个调用和所有对文件流的访问都受到有效保护,以便只有一个线程在调用printf()返回之前使用它。

在POSIX系统接口的“一般信息”部分关于线程的内容,第2.9.1节"线程安全性"中写道:
所有在POSIX.1-2008标准中定义的函数都应该是线程安全的,除了以下这些函数 1 不需要是线程安全的。
......列出了一些不需要线程安全的函数......
函数 getc_unlocked()、getchar_unlocked()、putc_unlocked()和putchar_unlocked()不需要是线程安全的,除非调用线程拥有通过函数调用访问的(FILE*)对象,在成功调用flockfile()或ftrylockfile()函数之后就是这种情况。实现应根据需要提供内部同步以满足此要求。
豁免功能列表不包括fputc、putc或putchar(或printf()等)。字符级输出在使用未锁定文件之前是线程安全的。更高级的函数,如printf()在开始时概念上调用flockfile(),在结束时调用funlockfile(),这意味着每次调用POSIX定义的流输出函数也是线程安全的。如果您希望为单个线程对文件流进行分组操作,可以通过在相关流上显式使用flockfile()和funlockfile()的调用来实现(而不会干扰系统对*lockfile()函数的使用)。这意味着无需为自己创建互斥锁或等效机制;实现提供了函数,允许您在多线程应用程序中控制对printf()等的访问。

...由于不再相关,因此删除了先前版本答案中的代码...


1
如果操作是用户级调用,例如printf(),那么整个调用和对文件流的所有访问都会受到有效保护,以便只有一个线程在使用它,直到调用printf()返回。需要引证。我没有在标准语言中看到这个要求。 - Bruce Fields
你是否查看了回答中链接到flockfile()规范?我相信它在回答中已经非常明确地说明了,正如回答中引用的话所说:“所有涉及FILE *对象的函数...应该表现得像它们内部使用flockfile()funlockfile()一样,以获取这些(FILE *)对象的所有权。” 我承认这是一个相当不易察觉的位置,但这就是POSIX标准中“例如printf()这样的用户级调用将受到有效保护”的要求。 - Jonathan Leffler
它只是说需要获取所有权,而不必在调用期间持续保持所有权。在我看来,假设printf实现会偶尔释放锁并重新获取,则仍将按照需求的字面意义严格执行。只要它在实际输出期间保持锁定状态,就仍然会为底层对象保留某种一致性水平,但它无法防止交错。 - Bruce Fields
我认为标准委员会没有预料到实现者会如此……变态?狡猾?……以至于想到在整个调用过程中不持有文件流上的锁。你可以尝试制作一个按照你所建议的行为方式运行的实现,但我认为你很快就会发现用户将把QoI(实现质量)视为“不满意”,并停止使用它。意图相当明确,即代码应在进入时锁定流(或者至少在第一次使用流本身之前),并在返回之前解锁它(或者至少在最后一次使用流本身之后)。 - Jonathan Leffler
好的!我认为标准委员会的工作就是编写防止滥用的语言,而在这种情况下,这并不难。但我想不出有什么好理由需要在打印过程中暂时释放锁,所以你肯定是正确的,没有人会那样做。 - Bruce Fields

22

为了避免混淆来自不同线程的输出,您需要确保一次只有一个线程使用printf。为了实现这一点,最简单的解决方案是使用一个mutex。在开始时初始化mutex

static pthread_mutex_t printf_mutex;
...
int main()
{
    ...
    pthread_mutex_init(&printf_mutex, NULL);
    ...

然后在printf周围创建一个包装器,以确保只有获得了mutex的线程才能调用printf(否则它将被阻塞直到mutex可用):

int sync_printf(const char *format, ...)
{
    va_list args;
    va_start(args, format);

    pthread_mutex_lock(&printf_mutex);
    vprintf(format, args);
    pthread_mutex_unlock(&printf_mutex);

    va_end(args);
}

5
另一种可能性是使用一个服务线程,其工作是与终端交互。其他线程可以将“打印任务”排队给它。 - David Schwartz
2
不需要定义自己的锁,因为已经有标准函数可以实现;请参见例如 https://www.gnu.org/software/libc/manual/html_node/Streams-and-Threads.html(该网站还声称 printf 本身是原子性的。我不知道这是否正确)。 - Bruce Fields

-3

对于Linux,这是用C语言编写的代码:3个线程,在不同的核心上执行打印“Hello World”,彼此不冲突,感谢锁。

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <syscall.h>
#include <sys/types.h>

void * printA ( void *);
void * printB ( void *);
void * printC ( void *);

pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;


int main(int argc,  char *argv[]) {
   int error;
   pthread_t tid1, tid2,tid3;

    if ( error = pthread_create (&tid1, NULL, printA, NULL ))
        {
        fprintf (stderr, "Failed to create first thread: %s\n",strerror(error));
        return 1;
    }
    if ( error = pthread_create (&tid2, NULL, printB, NULL ))
        {
        fprintf (stderr, "Failed to create second thread: %s\n",strerror(error));
        return 1;
    }
    if ( error = pthread_create (&tid3, NULL, printC, NULL ))
        {
        fprintf (stderr, "Failed to create second thread: %s\n",strerror(error));
        return 1;
    }

    if (error = pthread_join(tid1, NULL))
        {
        fprintf (stderr, "Failed to join first thread: %s\n",strerror(error));
        return 1;
    }
    if (error = pthread_join(tid2, NULL))
        {
        fprintf (stderr, "Failed to join second thread: %s\n",strerror(error));
        return 1;
    }

    if (error = pthread_join(tid3, NULL))
        {
        fprintf (stderr, "Failed to join second thread: %s\n",strerror(error));
        return 1;
    }
    return 0;
}

void * printA ( void *arg )
{
      if ( error = pthread_mutex_lock( &mylock ))
      {
    fprintf (stderr, "Failed to acquire lock in printA: %s\n",strerror(error));
    return NULL;
      }
   printf("Hello world\n");

      if ( error = pthread_mutex_unlock( &mylock ))
      {
    fprintf (stderr, "Failed to release lock in printA: %s\n",strerror(error));
    return NULL;
      }
   }

void * printB ( void *arg )
{
   int error;
      if ( error = pthread_mutex_lock( &mylock ))
      {
    fprintf (stderr, "Failed to acquire lock in printB: %s\n",strerror(error));
    return NULL;
      }


   printf("Hello world\n");

      if ( error = pthread_mutex_unlock( &mylock ))
      {
    fprintf (stderr, "Failed to release lock in printA: %s\n",strerror(error));
    return NULL;
      }
   }


void * printC ( void *arg )
{
   int error;
      if ( error = pthread_mutex_lock( &mylock ))
      {
    fprintf (stderr, "Failed to acquire lock in printB: %s\n",strerror(error));
    return NULL;
      }


   printf("Hello world\n");

      if ( error = pthread_mutex_unlock( &mylock ))
      {
    fprintf (stderr, "Failed to release lock in printA: %s\n",strerror(error));
    return NULL;
      }
   }

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