printf and Concurrency

Inspired by Condition Variables.

Why So Many Concurrency Errors

我们有下面这段程序:

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

pthread_mutex_t mutex;
void mutex_init(){
	int res = pthread_mutex_init(&mutex, NULL);
	if (res == 0) {
		printf("Mutex lock initialized successfully!\n");
	} else {
		printf("Failed to initialize mutex lock. Error code: %d\n", res);
	}
	return;
}
pthread_cond_t cond;
void cv_init(){
    int res;
    res = pthread_cond_init(&cond, NULL);
    if (res == 0) {
        printf("Condition variable initialized successfully!\n");
    } else {
        printf("Failed to initialize condition variable. Error code: %d\n", res);
    }
    return;
}
int condition = 0; // Explicitly initialize the condition variable to 0
void *thread_func(void *arg) {
	printf("Thread %ld is requiring lock.\n", (long)pthread_self());
    pthread_mutex_lock(&mutex);
    while (!condition) {
        pthread_cond_wait(&cond, &mutex);
        // The thread will block here, and the mutex lock will be released until the condition variable is signaled.
    }
    printf("Thread %ld continues\n", (long)pthread_self());
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
	mutex_init();
	cv_init();
    pthread_t threads[8];
    for (int i = 0; i < 8; i++) {
        pthread_create(&threads[i], NULL, thread_func, NULL);
    }
    
	usleep(100000); // Sleep for a second to ensure the thread is waiting on the condition variable
    printf("Main thread is about to signal the condition variable.\n");
    pthread_mutex_lock(&mutex);
    condition = 1;
    printf("Condition fulfilled by main thread.\n");
    for(int i = 0; i < 8; i++){
	    pthread_cond_signal(&cond);
    }
    pthread_mutex_unlock(&mutex);

    for (int i = 0; i < 8; i++) {
        pthread_join(threads[i], NULL);
    }
    
    // Destroy the condition variable and mutex to release resources
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

编译后用 Helgrind 运行一下检查并发错误。发现了 249 个并发错误,为什么?

==87816== ERROR SUMMARY: 286 errors from 4 contexts (suppressed: 198 from 44)

观察一下 Helgrind 给出的日志,我发现许多 IO 相关的日志输出。那可能是和 printf 有关了。于是我马不停蹄地问 AI 发生了什么?随后 AI 给出了答复:将 printf("Thread %ld is requiring lock.\n", (long)pthread_self()); 注释掉。

void *thread_func(void *arg) {
	// printf("Thread %ld is requiring lock.\n", (long)pthread_self());
    pthread_mutex_lock(&mutex);
    while (!condition) {
        pthread_cond_wait(&cond, &mutex);
        // The thread will block here, and the mutex lock will be released until the condition variable is signaled.
    }
    printf("Thread %ld continues\n", (long)pthread_self());
    pthread_mutex_unlock(&mutex);
    return NULL;
}

注释掉之后运行,的确没有问题了,但是为什么?

Thread-Safe printf

我们用 printf 来写入一些数据并打印到屏幕上,这些数据都会被存到一个缓冲区(stdout)里。既然我们有缓冲区,那么不难理解,我们需要锁来保证有多个线程写缓冲区时不会引起数据竞争(内容交错)。然而,虽然在 C 标准中 printf() 是线程安全的。但内容交错仍然存在。

printf() 函数内部,有一把锁来保护 IO 缓冲区:flockfile / funlockfile。也就是说每次调用 printf 都会触发以下操作:

int printf(const char *restrict format, ...){
    flockfile(stdout); // Lock
    // Write buffer
	// Flushing the buffer
	funlockfile(stdout); // Unlock
}

既然是线程安全的,为什么还会报错?一个原因就是因为 printf() 涉及到 IO 操作,速度可能慢些,如此,可能会影响线程执行的调度顺序。但即使线程被调度器调度,其他的线程也不会写缓冲区。那为什么会出现内容交错呢?