多线程
线程定义
线程是进程中的⼀个执⾏单元,
负责当前进程中程序的执⾏,
⼀个进程中⾄少有⼀个线程
⼀个进程中是可以有多个线程
多个线程共享同一个进程的所有资源,每个线程参与操作系统的统一调度
可以简单理解成 进程 = 内存资源 + 主线程 + 子线程 +…
线程与进程
联系比较紧密的任务,在并发时,优先选择多线程,任务联系不紧密,比较独立的任务,建议选择多进程;
- 进程:操作系统分配资源的基本单位,是资源分配的最小单位,是程序的执行和调度单位,是程序的运行实例。
- 线程:是CPU调度和分派的基本单位,是CPU执行的最小单位,是程序执行流的最小单元,是程序执行的最小单位。
线程与进程区别:
- 内存空间
- 一个进程中多个线程共享同一个内存空间
- 多个进程拥有独立的内存空间
- 进程/线程间通讯
- 线程间通讯方式简单
- 进程间通讯方式复杂
线程资源
- 共享进程的资源
- 同一块地址空间
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户id和组id
- 独有资源
- 线程栈
- 每个线程都有私有的上下文信息
- 线程id
- 寄存器的值
- errno值
- 信号屏蔽字以及调度优先级
线程相关命令
在 Linux 系统有很多命令可以查看进程,包括 pidstat 、top 、ps ,可以查看进程,也可以查看一个
进程下的线程
pidstat 命令
ubuntu 下需要安装 sysstat 工具之后,可以支持 pidstat
sudo apt install sysstat
选项
-t : 显示指定进程所关联的线程
-p : 指定 进程 pid
示例
查看进程 12345 所关联的线程
sudo pidstat -t -p 12345
查看所有进程所关联的线程
sudo pidstat -t
查看进程 12345 所关联的线程,每隔 1 秒输出一次
sudo pidstat -t -p 12345 1
查看所有进程所关联的线程,每隔 1 秒输出一次
sudo pidstat -t 1
top 命令
top 命令查看某一个进程下的线程,需要用到 -H 选项在结合 -p 指定 pid
选项
-H : 显示线程信息
-p : 指定 进程 pid
示例
查看进程 12345 所关联的线程
sudo top -H -p 12345
查看所有进程所关联的线程
sudo top -H
ps 命令
ps 命令结合 -T 选项就可以查看某个进程下所有线程
选项
-T : 显示线程信息
-p : 指定 进程 pid
示例
查看进程 12345 所关联的线程
sudo ps -T -p 12345
查看所有进程所关联的线程
sudo ps -T
常见的并发方案
1. 多进程模式
多进程模式下,每个进程负责不同的任务,互不干扰,各自运行在不同的内存空间,互不影响。
- 优点:
- 进程的地址空间独立,一旦某个进程出现异常,不会影响其他进程
- 缺点:
- 每个进程都需要分配独立的内存空间,创建进程的代价高,占用更多的内存
- 进程间协同,进程间通讯比较复杂
- 适用场景:
- 多个任务联系不是非常紧密,可以采用多进程模式
- 任务之间没有依赖关系,可以采用多进程模式
2. 多线程模式
多线程模式下,一个进程内可以有多个线程,共享同一份内存空间,线程之间可以直接通信。
- 优点:
- 线程间通信简单
- 同一个进程的多个线程可以共享资源,可以提高资源利用率
- 缺点:
- 线程没有独立的进程地址空间,主线程退出后,其他线程也会退出
- 线程切换和调度需要消耗资源,线程数量过多,会消耗系统资源
- 线程间同步复杂,需要考虑线程安全问题
- 适用场景:
- 任务之间有依赖关系,可以采用多线程模式
- 任务之间通信比较频繁,可以采用多线程模式
创建线程
1. pthread_create()
pthread_create() 用来创建线程,创建成功后,线程就开始运行,
pthread_create() 调用成功后,会返回 0,否则返回错误码。
函数头文件:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数说明:
- thread: 指向 pthread_t 类型的指针,用来存储线程的 ID。
- attr: 线程属性,可以为 NULL,表示使用默认属性。
- start_routine: 线程的入口函数.
- arg: 传递给线程入口函数的参数。
返回值:
- 0: 创建成功。
- EAGAIN: 资源不足,创建线程失败。
- EINVAL: 参数无效。
- ENOMEM: 内存不足,创建线程失败。
注意:
- 一旦子线程创建成功,则会被独立调度执行,并且与其他线程 并发执行
- 在编译时需要链接 -lpthread 库。
示例:创建一个线程
// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
printf("%s\n",(char *)arg);
}
int main() {
pthread_t tid; //? typedef unsigned long int pthread_t;
// 创建线程
//@param tid 线程ID
//@param attr 线程属性
//@param start_routine 线程函数
//@param arg 线程函数参数
int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
if (ret!= 0){
printf("pthread_create error!\n");
return 1;
}
sleep(1); // 等待线程执行完毕
return 0;
}
2. pthread_exit() 退出线程
pthread_exit() 用来退出线程,线程执行完毕后,会自动调用 pthread_exit() 退出。
函数头文件:
#include <pthread.h>
void pthread_exit(void *retval);
参数说明:
- retval: 线程退出时返回的值。
- 线程函数执行完毕后,会自动调用 pthread_exit() 退出。
3. pthread_join() 等待线程结束
pthread_join() 用来等待线程结束,
调用 pthread_join() 后,当前线程会被阻塞,直到线程结束。
函数头文件:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数说明:
- thread: 线程 ID。
- retval: 指向线程返回值的指针,用来存储线程退出时返回的值。(二级指针)
返回值:
- 0: 等待成功。
- EINVAL: 参数无效。
- ESRCH: 线程 ID 不存在。
- EDEADLK: 线程处于死锁状态。
示例:
// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
sleep(1); // 休眠1秒
printf("%s\n",(char *)arg);
pthread_exit(NULL); // 线程退出
}
int main() {
pthread_t tid; //? typedef unsigned long int pthread_t;
// 创建线程
//* @param tid 线程ID
//* @param attr 线程属性
//* @param start_routine 线程函数
//* @param arg 线程函数参数
int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
if (ret!= 0){
printf("pthread_create error!\n");
return 1;
}
printf("等待线程结束...\n");
// 等待线程结束
//* @param thread 线程ID
//* @param status 线程退出状态
pthread_join(tid, NULL);
return 0;
}
等待线程结束...
Hello, World!
线程分离
线程分为可结合的与可分离的
- 可结合
- 可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。
- 线程创建的默认状态为 可结合的,可以由其他线程调用 pthread_join 函数等待子线程退出并释放相关资源
- 可分离
- 不能被其他线程回收或者杀死的,该线程的资源在它终止时由系统来释放。