含pthreads的简单C程序出现偶发Bug,求分析环境相关原因
程序说明
以下是一个简单的C程序源码,主循环提供三个选项供用户选择:
- 执行计算任务
- 打印指定文本
- 退出程序
选择选项1时会调用shenanigans函数,该函数是计算密集型任务,通过pthreads实现多核并行。注意shenanigans会调用pthread_join等待所有工作线程完成后才返回,其多线程实现对程序其他部分完全透明。
源码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <limits.h> #include <errno.h> #include <pthread.h> int convert_to_integer(const char *str, int *outValue) { if (str == NULL || outValue == NULL) return -1; // NULL pointer error char *endptr; errno = 0; long temp = strtol(str, &endptr, 10); if (errno == ERANGE && (temp == LONG_MAX || temp == LONG_MIN)) return -2; // Overflow or underflow if (errno != 0 && temp == 0) return -3; // Other conversion error if (endptr == str) return -4; // No digits were found if (*endptr != '\0') return -5; // Additional characters after number if (temp > INT_MAX || temp < INT_MIN) return -6; // Value out of int range *outValue = (int)temp; return 0; // Success } int get_int_from_stdin(int *target) { if (target == NULL) return -1; // NULL pointer error char input[64]; if (fgets(input, sizeof(input), stdin) == NULL) return -2; // Reading error or EOF // null-terminate the string if newline is found size_t len = strlen(input); if (input[len - 1] == '\n') { input[len - 1] = '\0'; } else { // Clear stdin buffer if input exceeded buffer length, to avoid leftover input affecting subsequent reads int ch; while ((ch = getchar()) != '\n' && ch != EOF); if (len == sizeof(input) - 1) return -3; // Input too long } int value; int result = convert_to_integer(input, &value); if (result != 0) return result; // Propagate error code from conversion *target = value; return 0; // Success } void * thread_function(void * args) { // this is work that will be done by threads // notice there is no side effects and no interaction with // stdout and stdin // just waste a bunch of time long x = 0; for (int i = 0; i < 10000; i++) { for (int j = 0; j < 1000000; j++) { if (i %2 == 0) x++; else x--; } } return NULL; } void shenanigans(){ // run 7 different worker threads in parallel printf("running tasks in parallel\n"); fflush(stdout); // flushing the output buffer doesn't do anything //---- start of multithreading code pthread_t threads[7]; // initiate 7 worker threads to do some work for (int j = 0; j < 7; j++) pthread_create(&threads[j], NULL, thread_function, NULL); // wait for all threads to finish for (int j = 0; j < 7; j++) pthread_join(threads[j], NULL); // ---- end of multithreading code printf("task finished...\n"); } void print_junk(){ printf("Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n"); } int main() { while (1) { printf("\nChoose an option from the following menu: \n"); printf("1- do some calculations\n"); printf("2- print some junk to standard output\n"); printf("3- exit...\n"); int choice; int result = get_int_from_stdin(&choice); if (result != 0) { printf("Invalid input, try again\n"); continue; } if (choice == 1) { // this function uses parallelism to get its job done faster // but the parallelism ought to be completely transparent to the main function shenanigans(); } else if (choice == 2) { print_junk(); } else if (choice == 3) { break; } else { printf("Invalid choice, try again\n"); } } return 0; }
问题描述
编译运行这段代码时出现两个偶发异常:
- 选择选项1后,预期"running tasks in parallel"会立即输出,之后主线程才会等待工作线程完成,但实际常出现程序先无响应(工作线程运行中),一段时间后才显示该文本;
- 主线程等待工作线程期间,输入缓冲区似乎被写入了无关字符,导致后续主循环读取用户选择时出现异常。
这些异常非常反常,因为:
- 工作线程完全没有IO操作;
- 线程间没有共享内存或全局状态修改。
排查后发现是环境问题:在Oracle VirtualBox托管的Debian虚拟机中会出现,换成VMware Workstation托管的Debian环境后问题消失。请问这背后的原因是什么?
原因分析
这两个异常本质是VirtualBox与VMware在虚拟IO/控制台处理和CPU调度模拟上的实现差异或缺陷导致的:
1. 输出延迟问题
虽然代码显式调用了fflush(stdout),但stdout的刷新还受终端行缓冲机制、虚拟机控制台转发逻辑的影响。VirtualBox在处理宿主机与虚拟机的IO转发时,可能存在缓冲同步延迟:主线程执行printf和fflush后,虚拟机内核的输出缓冲区数据并没有立即被转发到宿主机终端,反而被后续计算密集型线程抢占了CPU资源,负责控制台输出转发的内核线程长时间得不到调度,直到工作线程完成后才批量输出内容。
而VMware的控制台IO转发机制更高效,能及时调度IO相关线程,确保输出内容立即显示。
2. 输入缓冲区异常问题
这个问题大概率是VirtualBox的虚拟键盘/输入模拟bug:当虚拟机内CPU被计算密集型线程占满时,VirtualBox的输入模拟模块可能出现异常,将宿主机的键盘输入(甚至内部模拟信号)错误写入虚拟机的stdin缓冲区。这种情况属于虚拟机底层bug,和程序本身的线程逻辑无关——毕竟程序的工作线程完全没有触碰stdin资源。
VMware的输入模拟实现更稳定,不存在这类高负载场景下的异常写入问题。
另外补充:VirtualBox在多核虚拟机的CPU调度模拟上,对于高负载线程场景可能存在调度不公平性,导致IO处理内核线程被长时间抢占,进一步放大了上述两个问题。
内容的提问来源于stack exchange,提问作者ForeverStudent

