C多线程并发处理方式_C语言线程并发处理策略

pthread_create需显式设置栈大小以防溢出,共享变量必须加锁避免竞态,线程退出后须pthread_join或pthread_detach清理资源,信号与多线程交互应避免pthread_kill而改用条件变量或事件通信。

pthread_create 启动线程时必须传入有效栈空间

很多初学者直接传 NULLpthread_create 的第四个参数(线程属性),以为系统会自动分配,结果在线程函数里一用局部变量就崩溃。这不是随机错误,而是因为默认栈大小通常只有 2MB(Linux 下),且无法动态扩展;若线程递归过深或分配大数组(比如 char buf[1024*1024]),就会触发栈溢出,segfault 或静默破坏相邻内存。

实操建议:

  • 避免在多线程中定义超大栈变量;改用 malloc 分配堆内存,并确保线程退出前 free
  • 如需更大栈,用 pthread_attr_setstacksize 显式设置,例如:
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, 8 * 1024 * 1024); // 8MB
    pthread_create(&tid, &attr, thread_func, arg);
    pthread_attr_destroy(&attr);
  • 注意:栈大小不能小于 PTHREAD_STACK_MIN(通常是 16KB),也不能超过系统限制(ulimit -s

共享变量不加锁导致的竞态不是“偶尔出错”,而是必然不可预测

int counter = 0; 被两个线程同时执行 counter++,最终结果大概率是 1 而不是 2——这不是概率问题,是 counter++ 在汇编层面至少包含三步:读取、加 1、写回。两个线程可能交错执行这三步,造成一次更新被覆盖。

常见误判:

  • “我只读不写,不用锁” → 错。即使只读,若另一线程正在写,你可能读到中间状态(非原子写入,尤其结构体或 64 位变量在 32 位平台)
  • “我用 volatile 就安全了” → 错。volatile 只禁用编译器优化,不阻止

    CPU 重排序,也不提供原子性或内存屏障
  • “我 sleep 一下就能错开” → 错。这是典型的“靠运气编程”,在高负载或不同 CPU 架构下立即失效

正确做法:对共享变量的读写,统一用 pthread_mutex_t 保护;若只是计数,可考虑 __atomic_add_fetch(GCC 内置原子操作),但要注意平台兼容性。

线程退出后资源未清理:pthread_join 不是可选的

调用 pthread_create 后不调用 pthread_join,线程变成“分离状态”以外的“可连接线程”(joinable)。这类线程结束后,其退出状态、栈内存等资源不会自动释放,直到有其他线程调用 pthread_join。长期运行的程序若漏掉 pthread_join,会持续泄漏资源,最终 pthread_create 返回 EAGAIN(资源耗尽)。

两种处理路径:

  • 主控线程明确等待子线程结束:创建后立刻或适当时机调用 pthread_join(tid, &retval)
  • 子线程自行分离:在子线程函数开头调用 pthread_detach(pthread_self()),之后无需 join;但要注意,分离后不能再被 join,也不能获取其返回值
  • 切勿在主线程 exit() 前遗漏 join —— exit 会终止整个进程,未 join 的 joinable 线程资源仍不释放

信号与多线程交互极易出错,尽量避开 pthread_kill

pthread_kill(tid, SIGUSR1) 向某线程发信号,看似精准,实际风险极高:信号可能被任意线程接收(取决于信号掩码和调度),且若目标线程正阻塞在 readpthread_cond_wait 上,行为不可控;更严重的是,signal 处理函数是全局的,多个线程同时收到同信号,会共用同一个 handler,引发数据竞争。

替代方案更可靠:

  • pthread_cond_signal + pthread_mutex_t 实现线程间通知(推荐)
  • 用管道(pipe)或 eventfd(Linux)做跨线程事件通信,主循环用 poll 等待
  • 真需要信号,应使用 sigwait 在指定线程中同步等待,且提前用 pthread_sigmask 屏蔽其他线程接收该信号

信号与线程模型本身不正交,C 标准库对多线程信号的支持非常有限,能不用就别碰。