线程基础:线程的概念、线程与进程的区别、线程的创建与终止、线程的属性
各位同学,今天我们来聊聊多线程编程的起点——线程基础。这部分内容我每次讲课时都会特别强调,因为它是后续所有并发技巧的根基。我自己刚入行时,就因为对线程和进程的区别理解不透彻,踩过一个挺深的坑,后面我会跟大家细说。
一、线程到底是什么?
简单来说,线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
嗯,这里要注意:线程自己不拥有系统资源,它与同属一个进程的其他线程共享进程所拥有的全部资源。说白了,线程就是「轻量级的执行路径」。
核心要点:线程是CPU调度的基本单位,进程是资源分配的基本单位。
二、线程与进程的区别——我当年踩过的坑
我记得刚工作那会儿,接手一个网络服务程序。当时项目里用了多进程模型,每个连接来了就fork一个子进程去处理。结果服务器跑了不到半小时,内存就爆了。后来老工程师看了一眼代码,说:「你试试用线程代替进程。」
替换之后,内存占用直接降了70%。为什么?因为进程之间是独立的地址空间,而线程共享同一地址空间。这就是两者最本质的区别。
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立地址空间 | 共享进程地址空间 |
| 资源开销 | 创建/切换开销大 | 创建/切换开销小 |
| 通信方式 | IPC(管道、消息队列等) | 直接读写全局变量 |
| 健壮性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程退出 |
| 适用场景 | 强隔离、高可靠性需求 | 高性能、频繁通信的场景 |
我的个人习惯:如果任务之间需要频繁交换数据,我首选线程;如果要求高隔离性(比如不同用户的数据处理),我选进程。
三、线程的创建与终止
在C语言中,我们通常使用POSIX线程库(pthread)来操作线程。你想想看,一个线程从生到死,其实就三个步骤:创建、运行、终止。
3.1 创建线程
创建线程使用 pthread_create 函数。它的原型长这样:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
参数说明:
thread:线程ID的指针attr:线程属性(传NULL使用默认属性)start_routine:线程要执行的函数arg:传给线程函数的参数
来看一个最简单的例子:
#include <stdio.h>
#include <pthread.h>
void* print_message(void* arg) {
char* msg = (char*)arg;
printf("线程说:%s\n", msg);
return NULL;
}
int main() {
pthread_t tid;
char* message = "Hello from thread!";
if (pthread_create(&tid, NULL, print_message, message) != 0) {
fprintf(stderr, "线程创建失败\n");
return 1;
}
pthread_join(tid, NULL);
return 0;
}
我曾经踩过的坑:忘记检查 pthread_create 的返回值。在资源紧张的系统上,线程创建可能失败,不检查返回值会导致后续操作全部异常。记住:任何系统调用都要检查返回值!
3.2 线程的终止
线程终止有三种方式:
- 自然返回:线程函数执行完毕,return返回
- 主动退出:调用
pthread_exit() - 被动取消:其他线程调用
pthread_cancel()
我个人最推荐第一种方式——让线程函数自然返回。为什么?因为这样资源清理最干净,不容易出现内存泄漏。
void* worker(void* arg) {
// 做一些工作...
printf("工作完成\n");
pthread_exit(NULL); // 等价于 return NULL;
}
四、线程的属性——定制你的线程
线程属性用 pthread_attr_t 结构体表示。说实话,很多初学者从来不用这个,全程传NULL。但在实际项目中,定制线程属性非常有用。
常用的属性包括:
| 属性 | 说明 | 常用设置 |
|---|---|---|
| detachstate | 线程的分离状态 | PTHREAD_CREATE_JOINABLE(可等待) PTHREAD_CREATE_DETACHED(分离) |
| stacksize | 线程栈大小 | 默认通常为8MB,可调小以节省内存 |
| schedpolicy | 调度策略 | SCHED_FIFO、SCHED_RR、SCHED_OTHER |
来看一个设置分离线程的例子:
pthread_attr_t attr;
pthread_t tid;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, worker, NULL) != 0) {
// 处理错误
}
pthread_attr_destroy(&attr); // 用完记得销毁
我的建议:如果你确定不需要等待线程结束(比如后台日志线程),就用分离线程。这样可以避免 pthread_join 阻塞,也省去了手动回收的麻烦。
五、知识体系总览
下面这张图是我自己整理的线程基础知识体系,你可以把它当作学习路线图:
六、避坑指南
最后,分享几个我这些年积累的经验:
- 线程栈别设太小:默认8MB通常够用,但如果你在函数里声明了大数组,记得调大栈空间。我曾经因为栈溢出查了整整两天bug。
- 主线程退出前要等待:如果主线程先退出了,整个进程就结束了,子线程会被强制终止。用
pthread_join或者pthread_detach来管理线程生命周期。 - 线程函数签名不能错:返回值必须是
void*,参数必须是void*。这是C语言线程库的硬性规定,别想着偷懒改签名。
一句话总结:线程是轻量级的执行单元,创建快、切换快、通信方便,但需要你更小心地管理共享资源和生命周期。