C语言多线程中自定义ft_printf引发竞态问题的原因及解决
问题代码
线程处理函数
void *dinning_handler(void *arg) { t_philo *philo; struct timeval start; philo = (t_philo *)arg; gettimeofday(&start, NULL); philo->last_meal_time = start; while (philo->max_eats == -1 || philo->eats < philo->max_eats) { print_blue("is thinking", philo->id, get_ts_in_ms()); pthread_mutex_lock(philo->left_fork); pthread_mutex_lock(philo->right_fork); print_blue("has taken a fork", philo->id, get_ts_in_ms()); print_green("is eating", philo->id, get_ts_in_ms()); usleep(philo->time_to_eat * 1000); philo->eats++; gettimeofday(&philo->last_meal_time, NULL); pthread_mutex_unlock(philo->left_fork); pthread_mutex_unlock(philo->right_fork); print_blue("is sleeping", philo->id, get_ts_in_ms()); usleep(philo->time_to_sleep * 1000); } return (NULL); }
自定义打印函数示例
void print_red(char **msg, int id, long time) { ft_printf("\033[1;31m"); ft_printf("%u %d %s\n", time, id, msg); ft_printf("\033[0m"); }
问题描述
上述多线程代码运行时会出现竞态条件,导致终端输出内容错乱,但将自定义的ft_printf替换为标准printf后输出恢复正常。需要解答:
- 出现该现象的原因是什么?
- 标准
printf内部是否会使用互斥锁? - 如何修复自定义代码的问题?
解答
1. 自定义ft_printf输出错乱的原因
自定义打印函数中,一次完整的输出被拆分成3次ft_printf调用:先输出颜色控制码、再输出核心内容、最后恢复默认颜色。在多线程环境下,这三次调用不是原子操作——线程A执行完第一次ft_printf输出颜色后,可能被线程B抢占CPU,线程B的输出会插入到线程A的颜色控制和内容之间,导致颜色错乱、内容混杂,最终出现输出异常。且你的ft_printf实现本身没有线程同步机制,无法保证多线程调用时的输出顺序和完整性。
2. 标准printf的线程安全性
在**POSIX兼容的系统(如Linux、macOS)**中,当stdout指向终端设备时,标准printf是线程安全的:内部会通过互斥锁或其他同步机制,保证一次printf调用的所有输出操作是原子的,不会被其他线程的printf打断。因此多个线程同时调用printf时,输出内容不会混杂。
注意:如果stdout指向文件、管道等非终端设备,printf的线程安全性可能会变化(取决于系统实现),但终端场景下的线程安全是标准保证的特性。
3. 代码修复方案
方案一:给打印操作添加全局互斥锁
这是最直接可靠的方案:
- 定义全局互斥锁变量:
pthread_mutex_t print_mutex; - 在创建线程前初始化锁:
pthread_mutex_init(&print_mutex, NULL); - 修改所有自定义打印函数,在执行输出前加锁,所有输出完成后解锁:
void print_red(char *msg, int id, long time) { pthread_mutex_lock(&print_mutex); ft_printf("\033[1;31m"); ft_printf("%u %d %s\n", time, id, msg); ft_printf("\033[0m"); pthread_mutex_unlock(&print_mutex); }
- 所有线程退出后销毁锁:
pthread_mutex_destroy(&print_mutex);
这样能保证每个打印函数的整个输出流程是原子的,不会被其他线程打断。
方案二:重构ft_printf实现
将一次完整的输出内容(包括颜色控制码、核心内容、恢复码)合并成一个字符串,再通过一次ft_printf调用输出。但这种方式需要处理字符串拼接的内存管理,且如果ft_printf内部本身有线程不安全的操作,仍可能出问题,因此不如加锁方案直接。
内容的提问来源于stack exchange,提问作者Vinicius Bass

