一,为什么需要多线程
1.1 什么是线程
进程是资源分配的最小单位,线程是CPU调度的最小单位。
一个进程内部可以创建多个线程,所有线程共享该进程的地址空间,全局变量,文件描述符;每个线程都由自己独立栈,寄存器。
1.2 需要多线程的原因
①可以充分利用多核的CPU算力,多个线程分配到不同的CPU核并行执行,提升整体的运算吞吐量。
🔺单核CPU下多线程是并发不是并行,靠时间片轮转切换。
②避免程序阻塞,提升响应能力,例如:网络服务器中,一个线程等待客户端的数据,其他线程继续处理其他连接。
③线程切换开销远小于进程,线程之间的切换,只需要切换寄存器,栈,切换成本低。
④方便共享数据,线程可以直接读取全局变量。
| 场景类型 | 特点 | 多线程作用 | 典型例子 |
|---|---|---|---|
| CPU密集型 | 程序主要消耗CPU计算资源 | 将计算任务分解到多个CPU核心,并行执行,缩短总耗时 | 视频编码,科学计算,图像渲染 |
| IO密集型 | 程序主要等待输入和输出操作 | 一个线程等待IO时,其他线程继续工作,避免CPU空闲 | Web服务器,数据库,下载工具 |
二,线程和进程的关系及区别
2.1 二者的区别
| 对比项 | 进程 | 线程 |
|---|---|---|
| 基本空间 | 资源分配的基本单位 | CPU调度的基本单位 |
| 地址空间 | 独立拥有 | 共享进程的地址空间 |
| 创建开销 | 大 | 小 |
| 切换开销 | 大 | 小 |
| 隔离性 | 强 | 弱 |
2.2 线程独有的资源(私有)
线程ID,一组寄存器(用于上下文切换),栈(存放局部变量,函数调用),error(错误码),信号屏蔽字,调度优先级
2.3线程共享的资源(共有)
内存资源和系统资源。
三,线程控制
3.1 POSIX线程库
需要引入头文件<pthread.h>
3.2 函数
pthread_create:创建线程的入口函数,所有多线程程序都是从这里开始。返回的是pthread_t类型的线程IDpthread_join / pthread_detach:资源回收的两种方式,join是手动回收,detach是自动回收。pthread_exit / pthread_cancel:线程结束的两种方式,exit是终止自己,cancel是终止同一进程中的其他线程。
3.3 线程等待
为什么需要线程等待?
①已经退出的线程,其空间没有被释放,仍然在进程的空间地址内。
②创建新的线程不会复用刚才退出线程的地址空间。pthread_join函数
作用:等待子进程结束,并回收它的资源。
3.4 分离线程
3.4.1 为什么需要分离线程
默认形况下,新创建的线程是 joinable 的;线程退出后,必须用pthread_join回收资源,否则会造成资源浪费;如果不关心线程返回值,每次都要join是一种负担。
3.4.2 分离线程的作用
告诉系统:线程退出时自动释放资源,无需再等待pthread_join。
四,共享资源和竞态条件
4.1 多个线程修改同一个变量会发生什么
多个线程同时修改同一个全局变量,没有任何保护,会产生竞态条件。这个时候就可以通过添加锁来保证同一时刻只有一个线程进入临界区操作资源。
竞态条件:程序的最终结果,依赖于多个线程的CPU调度执行时序。
示例:多个线程同时计数累加,实际运行结果小于预期值,因为一个线程读取变量之后,还没写回,就被内核切走,另一个线程修改了同一个变量,数据被覆盖丢失。
五,常见问题汇总
1.并发和并行有什么区别?
并发强调多个任务在一段时间内交替进行,不一定是同时进行;
并行强调的是多个任务在同一时刻同时执行,通常需要多个CPU核心。
2.创建线程之后,线程的执行顺序是固定的吗?
不是固定的,线程创建成功之后执行的顺序通常由操作系统调度器决定顺序。
3.为什么不能用sleep代替pthread_join?
sleep只能让当前线程暂停一段时间,不能保证目标线程已经执行完成。
例如:线程任务可能因为系统负载,CPU调度或任务复杂度不同,执行时间超过预期的睡眠时间,这样的情况下,sleep睡眠时间一过,主线程继续向下执行了,就不会等待指定线程执行完成,只有pthread_join是真正的等待指定线程结束的操作。
4.pthread_join 和 pthread_detach 有什么区别?
| 对比项 | pthread_join | pthread_detach |
|---|---|---|
| 是否等待线程结束 | 是,调用后线程阻塞等待目标线程退出 | 否,立即返回,不阻塞 |
| 是否能获取返回值 | 可以 | 不可以,线程退出后直接丢弃 |
| 资源释放时机 | 调用之后才回收线程资源,如果不join ,线程结束后资源不会释放,变成僵尸进程 | 线程一旦执行结束,系统自动回收线程资源,无须外部调用回收函数 |
| 适用场景 | 需要拿到子线程执行结果,主线程要等待子线程做完任务往下执行 | 不关心线程返回结果,不需要等待线程结束,做完任务线程自动销毁 |
5.为什么需要pthread_join呢?
没有调用这个函数的话,如果主线程直接返回,或者进程中任意一个线程调用exit,整个进程都会结束,进程中的其他线程也会被终止,因此调用 pthread_join 后,主线程会等待子线程完成之后才会继续执行。
6.循环创建线程时,为什么不能直接传递&i?
如果传 &i,当线程真正开始运行时,i 的值可能已经被修改,导致多个线程读取到相同的或者错误的值。
解决办法:为每个线程准备独立的参数,可以增加一个数组,如下:
int args[3]; for (int i = 0; i < 3; ++i) { args[i] = i; pthread_create(&threads[i], NULL, worker, &args[i]); }