线程基础:从进程到线程的进化
说实话,我第一次接触线程这个概念时,心里是有点抵触的。那时候我习惯了fork()创建进程,觉得进程模型挺清晰的——各干各的,互不干扰。直到我在一个网络服务器项目里,发现每来一个客户端就fork一个进程,内存开销大得吓人,进程间通信也搞得我焦头烂额。嗯,从那时起,我才真正理解了线程存在的意义。
线程是什么?
线程,说白了就是「轻量级进程」。你想想看,进程创建时要复制一大堆东西——地址空间、文件描述符表、信号处理函数……而线程呢?它只创建必要的执行上下文,比如栈、寄存器状态、程序计数器。其他东西,比如代码段、数据段、堆内存,统统和创建它的进程共享。
我习惯这样理解:进程是资源分配的最小单位,线程是CPU调度的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源,但各自独立执行。
核心区别一句话:进程有独立的地址空间,线程共享地址空间。
线程的优势——为什么我们需要它?
我在做嵌入式Linux项目时,遇到过这样一个场景:需要同时采集传感器数据、处理用户输入、刷新显示界面。如果用多进程,光IPC(进程间通信)就能写出一堆bug。用线程就简单多了——共享内存,直接读写全局变量。
线程的优势主要体现在这几个方面:
- 创建开销小——创建线程比创建进程快10-100倍。我实测过,在x86_64 Linux上,pthread_create大约需要几微秒,而fork需要几十微秒。
- 通信效率高——共享内存,不需要管道、消息队列这些IPC机制。但要注意同步问题,这个后面会讲。
- 资源占用少——线程只占栈空间和线程控制块,不像进程要复制整个地址空间。
- 切换速度快——同一进程内的线程切换,不需要切换页表,TLB不用刷新。
我的经验:如果你需要频繁创建和销毁执行单元,或者需要大量数据共享,线程是更好的选择。但如果需要强隔离(比如一个子任务崩溃不影响其他任务),进程更合适。
三个核心API:pthread_create / pthread_exit / pthread_join
Linux下线程编程用的是POSIX线程库,也就是pthread。使用时要链接-lpthread。我刚开始学的时候,经常忘记加这个链接选项,编译报错半天找不到原因。
pthread_create——创建线程
函数原型长这样:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
参数说明:
thread:输出参数,返回线程IDattr:线程属性,传NULL使用默认属性start_routine:线程入口函数,返回void*,参数void*arg:传给线程函数的参数
返回值:成功返回0,失败返回错误码。
来看个最简单的例子:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg) {
int *num = (int *)arg;
printf("子线程收到参数: %d\n", *num);
return NULL;
}
int main() {
pthread_t tid;
int value = 42;
if (pthread_create(&tid, NULL, thread_func, &value) != 0) {
perror("pthread_create");
return 1;
}
printf("主线程等待子线程...\n");
pthread_join(tid, NULL);
printf("子线程结束,主线程退出\n");
return 0;
}
注意:传给线程的参数arg必须是线程安全的。我曾经犯过一个错误——在主线程栈上定义一个局部变量,然后传给子线程。结果主线程先退出了,子线程访问的是已经失效的栈内存。嗯,这种bug很难查。
pthread_exit——线程主动退出
线程退出有三种方式:
- 线程函数执行完return
- 调用pthread_exit()
- 被其他线程调用pthread_cancel()取消
pthread_exit的原型:
void pthread_exit(void *retval);
retval是线程的返回值,可以被pthread_join获取。注意:retval指向的内存必须是有效的,不能是线程栈上的局部变量。
我习惯这样用:
void *worker(void *arg) {
// 做一些工作...
int *result = malloc(sizeof(int));
*result = 100;
pthread_exit((void *)result);
}
小技巧:在主线程中调用pthread_exit(),主线程会退出,但进程不会结束,其他子线程会继续运行。这和return不一样,return会导致整个进程退出。
pthread_join——等待线程结束
这个函数的作用和进程的wait()类似——阻塞等待指定线程退出,并获取其返回值。
int pthread_join(pthread_t thread, void **retval);
参数:
thread:要等待的线程IDretval:二级指针,用于接收线程的返回值。如果不需要,传NULL
完整示例:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
void *compute(void *arg) {
int n = *(int *)arg;
int *sum = malloc(sizeof(int));
*sum = 0;
for (int i = 1; i <= n; i++) {
*sum += i;
}
pthread_exit((void *)sum);
}
int main() {
pthread_t tid;
int n = 100;
pthread_create(&tid, NULL, compute, &n);
void *retval;
pthread_join(tid, &retval);
printf("1到%d的和 = %d\n", n, *(int *)retval);
free(retval); // 别忘了释放内存
return 0;
}
线程ID与进程ID
这里有个容易混淆的点。在Linux中,每个线程在内核里其实就是一个轻量级进程(LWP)。所以:
- 进程ID(PID):由内核分配,标识一个进程。同一进程内的所有线程共享同一个PID。
- 线程ID(TID):由内核分配,标识一个线程。每个线程有自己唯一的TID。
- pthread_t:由pthread库维护的线程标识符,用于pthread API调用。
获取方式:
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>
// 获取pthread线程ID
pthread_t self = pthread_self();
// 获取内核线程ID(TID)
pid_t tid = syscall(SYS_gettid);
// 获取进程ID(PID)
pid_t pid = getpid();
我写了个小工具来观察这些ID:
#include <stdio.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>
void *show_ids(void *arg) {
printf("子线程: PID=%d, TID=%ld, pthread_t=%lu\n",
getpid(), syscall(SYS_gettid), pthread_self());
return NULL;
}
int main() {
pthread_t tid;
printf("主线程: PID=%d, TID=%ld, pthread_t=%lu\n",
getpid(), syscall(SYS_gettid), pthread_self());
pthread_create(&tid, NULL, show_ids, NULL);
pthread_join(tid, NULL);
return 0;
}
运行结果类似:
主线程: PID=12345, TID=12345, pthread_t=140736000000000
子线程: PID=12345, TID=12346, pthread_t=140735000000000
看到了吗?PID相同,TID不同。主线程的TID等于PID,因为它是进程中的第一个线程。
关键点:pthread_t是用户态的线程标识,TID是内核态的线程标识。pthread_t只在同一进程内有意义,不能跨进程使用。
知识体系总览
下面这张图总结了线程基础的核心概念和API关系:
避坑指南
最后分享几个我踩过的坑:
- 线程栈大小——默认线程栈是8MB,如果递归调用太深或者局部变量太大,会栈溢出。可以用pthread_attr_setstacksize()调整。
- 线程安全——多个线程同时读写同一变量,一定要加锁。我见过太多因为没加锁导致的数据竞争bug。
- 僵尸线程——创建了线程但不join也不detach,线程退出后资源无法回收,造成内存泄漏。要么pthread_join,要么pthread_detach。
- 信号处理——多线程程序中的信号处理很复杂,建议只在主线程中处理信号,其他线程用sigprocmask阻塞。
曾经我犯过一个低级错误:在子线程里调用了exit(),结果整个进程直接退出了,其他线程还没干完活。记住,exit()是进程级的,pthread_exit()才是线程级的。
好了,线程基础就讲到这里。这些概念和API是后续学习线程同步、线程池、并发编程的基石。建议你亲手敲一遍代码,看看PID和TID的区别,感受一下线程的创建开销。实践出真知嘛。