多线程
- 1. 线程概念
- 1.1 Linux线程与接口关系的认识
- 1.2 线程的私有 & 共有资源
- 1.3 线程的优缺点
- 1.4 线程的异常
- 2. 线程控制
- 2.1 pthread_create 创建线程
- 2.2 pthread_join 线程等待
- 2.3 线程终止的方案
- 2.4 pthrerad_detach 线程分离
a zing never lies
正文开始@小边小边别发愁
线程,是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细和轻量化。
一个进程内,可能存在多个线程,进程:线程 = 1:n,操作系统中可能存在更多的线程,OS就要管理线程 —— 先描述,再组织。线程也应该有线程控制块 —— TCB(thread control block),如上所说是常规OS的做法,例如windows,但是Linux的实现有所不同。
Linux中没有专门为线程设计PCB,而是用进程PCB来模拟。
这样以来,就不用维护进程和线程复杂的关系,不用单独为线程涉及任何算法,而是直接使用进程的一套相关算法,OS只需要聚焦在线程间的资源分配上。
线程,是在进程内部(线程在进程的地址空间内执行)运行的一个执行分支(执行流)(CPU调度时只看PCB,每一个PCB曾经被指派过指向方法和数据,CPU可以直接调度),属于进程的一部分(今天的进程vs过去的进程),粒度要比进程更加细和轻量化。
之前的进程,是内部只包含一个执行流的进程;今天的进程,内部可以具有多个执行流。
创建进程需要创建PCB、地址空间、页表,加载数据和代码,构建映射关系,文件等等;而创建线程,只需要创建PCB。创建进程的成本(空间+时间)非常高,要使用的资源非常多(0→1)!
内核视角:进程是承担分配系统资源的基本实体!!线程是CPU调度的基本单位,承担进程资源的一部分基本实体,即进程划分资源给线程。
1. 线程概念
1.1 Linux线程与接口关系的认识
Linux PCB <= 传统意义上的进程PCB
- OS层面上,创建“线程”时更轻量化
- CPU调度
Linux的进程,一般称为轻量级进程。
Linux因为是用进程模拟的,所以Linux不会给我们提供直接操作线程的接口,而是给我们提供在同一个地址空间创建PCB的方法,分配资源给指定的PCB的接口。这对用户(系统级别的工程师)特别不友好!!所以他们在用户层对Linux轻量级进程接口进行封装,给我们打包成库,让我们直接使用库接口 —— 原生线程库pthread(用户层)
1.2 线程的私有 & 共有资源
所有的轻量级进程(可能是“线程”),都是在进程的内部运行(地址空间:表示进程所能看到的大部分资源)
进程,具有独立性,可以有部分共享资源(管道、ipc资源);线程,大部分资源是共享的,也可以有部分资源是“私有”(pcb、栈、上下文)
线程的私有资源
- 线程ID
- 一组寄存器(上下文)
- 栈
- errno
- 信号屏蔽字
- 调度优先级
线程的共享资源
- 文件描述符表
- 每种信号的处理方式 (SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
1.3 线程的优缺点
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量(多进程)
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务(多进程)
- 计算密集型应用 (加密、大数据运算等,主要使用的是CPU资源),为了能在多处理器系统上运行,将计算分解到多个线程中实现。线程是越多越好嘛?不一定的,如果线程太多会导致线程被过度调度切换,这也是有成本的。
- I/O密集型应用 (网络下载、云盘、SSH、在线直播、看电影,主要使用的是内存和外设的IO资源),为了提高性能,将各线程等待I/O操作的时间重叠。线程可以同时等待不同的I/O操作。线程是不是越多越好呢?不一定啊喂,不过IO允许多一些线程,因为大部分的时间是等待IO就绪的。
很多应用都是CPU_IO密集型的,例如网络游戏
线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变(多进程) - 健壮性降低 (compared with进程 - 独立性)
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。后文验证。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多 - 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
1.4 线程的异常
- 单个线程崩溃(如果出现除零,野指针问题),进程也会随着崩溃,因为终止进程是发送信号的,信号是发给进程的,进程的资源就被释放了。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
2. 线程控制
2.1 pthread_create 创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread. 事实上这就是第三方库
返回值:On success, pthread_create() returns 0; on error, it returns an error number
参数:
-
thread
:线程ID,无符号(长)整数,不同操作系统有差别。 -
attr
:线程属性。设为NULL
,交给操作系统来设置。 -
start_routine
:线程的回调函数 -
arg
:其实就是传递给回调函数的参数。
此时依旧只有一个进程,但是进程内部一定具有两个执行流。发送kill
信号,都被干掉了,说明信号是发送给进程的。
ps -aL //-L查看轻量级进程
我们发现,这两个线程的PID相同,但不同的是LWP(light weight process 轻量级进程)。事实上,Linux OS调度时看的是LWP。
这与我们之前单独一个进程的情况并不矛盾,PID = LWP → 主线程。
//obtain ID of the calling thread
#include <pthread.h>
pthread_t pthread_self(void);
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_run(void* args)
{
const char* id = (const char*)args;
while(1)
{
printf("%d: 我是新线程%s, 我的线程ID是%lu\n", getpid(), id, pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, (void*)"thread 1");
while(1)
{
printf("%d: 我是main线程, 我的线程ID是%lu, 我创建的线程ID是%lu\n", getpid(), pthread_self(), tid);
sleep(1);
}
return 0;
}
我们发现新创建的thread 1线程ID是一个很大的数,与我们之前看到的LWP并不相同 ——
( 这是内核LWP,和这些长整数是不一样的,操作系统和线程库中的线程ID没规定必须一样~~)
下面验证一下这个长整数是什么?
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
void* thread_run(void* args)
{
while(1)
{
printf("new thread: 0x%x\n", pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, "new thread");
while(1)
{
printf("main thread: 0x%x\n", pthread_self());
sleep(1);
}
return 0;
}
我们查看到的线程ID是pthread库的线程ID,不是Linux内核中的LWP,pthread库是一个内存地址!那一定是虚拟地址。
可以快速拿到线程的属性:是一个被映射进虚拟地址空间,pthread库中的地址 → 线程ID
那么 ——
我们当然还可以创建一批线程 ——
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_run(void* args)
{
const char* id = (const char*)args;
while(1)
{
// printf("%d: 我是新线程%s, 我的线程ID是%lu\n", getpid(), id, pthread_self());
sleep(3);
}
}
int main()
{
pthread_t tid[5];
int i = 0;
for(i = 0; i<5 ;i++)
{
pthread_create(tid+1, NULL, thread_run, (void*)"new thread");
}
while(1)
{
printf("我是主线程, 我的thread_ID是%lu\n", pthread_self());
printf("=====================begin=======================\n");
for(i = 0; i<5 ;i++)
{
printf("我创建的线程[%d]是:%lu\n", i, tid[i]);
}
printf("======================end========================\n");
sleep(1);
}
return 0;
}
我们再来验证一下,一个线程崩溃,整个进程终止,写一段不太优雅的代码 ——
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_run(void* args)
{
int num = *(int*)args;
while(1)
{
printf("我是新线程[%d], 我的线程ID是%lu\n", num, pthread_self());
if(num == 3)
{
//故意设置了野指针问题 - 崩溃
printf("thread number: %d quit...\n", num);
int* p = NULL;
*p = 100;
}
sleep(5);
}
}
int main()
{
pthread_t tid[5];
int i = 0;
for(i = 0; i<5 ;i++)
{
pthread_create(tid+1, NULL, thread_run, (void*)&i);
sleep(1);
}
return 0;
}
由此可见,多线程健壮性不强。
2.2 pthread_join 线程等待
一般而言,线程也是需要被等待的,如果不等待,可能会导致类似“僵尸进程”的问题。
进程需要等待,同样的,创建线程的目的也是让他帮我办事儿,办的怎么样也需要知道;另外,线程复用了PCB结构,退出码退出信号也需要填入PCB。
💛 join with a terminated thread
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
返回值:On success, pthread_join() returns 0; on error, it returns an error number.
参数:
thread
:等待哪个线程。就是我们刚才pthread_self()获取的长整数。retval
:输出型参数,用于获取新线程退出时的返回值 ↓ 当然了~ 不要认为这儿的返回值只是整数,也可以是其他变量/对象的地址,但不能是临时的。
我们知道进程退出无非三种情况:①代码跑完结果对 ②代码跑完结果不对 ③异常。进程那里有退出码,在多进程这儿就是通过**void*
返回值得知执行结果**。那么代码异常如何处理?事实上,根本不需要处理,因为这是进程的事儿。
那么当然就可以通过返回值得知线程执行的怎么样了 ——
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_run(void* args)
{
int num = *(int*)args;
while(1)
{
printf("我是新线程[%d], 我的线程ID是%lu\n", num, pthread_self());
sleep(5);
break;
}
//新线程跑完
return (void*)111;
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM ;i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
//void* - 占8个字节 - 指针变量,本身就可以充当某种容器保存数据
void* status = NULL;
pthread_join(tid[0], &status);
printf("ret: %d\n", (int)status);
return 0;
}
2.3 线程终止的方案
- 函数中return
- main函数return的时候代表主线程/进程退出
- 其他线程函数return只代表当前线程退出
- 新线程通过pthread_exit()终止自己
#include <pthread.h>
void pthread_exit(void *retval);
terminate calling thread 相当于return
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
void* thread_run(void* args)
{
int num = *(int*)args;
while(1)
{
printf("我是新线程[%d], 我的线程ID是%lu\n", num, pthread_self());
sleep(2);
break;
}
//新线程跑完
pthread_exit((void*)123);
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM ;i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
void* status = NULL;
pthread_join(tid[0], &status);
printf("ret: %d\n", (int)status);
return 0;
}
这和我们之前的exit()有什么区别呢?exit是终止进程,如果你只想终止一个线程的话,不要在其它线程中调用。
- 取消目标进程 send a cancellation request to a thread
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
我们发现,进程被取消时,返回值是-1
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_run(void* args)
{
int num = *(int*)args;
while(1)
{
printf("我是新线程[%d], 我的线程ID是%lu\n", num, pthread_self());
sleep(2);
// break;
}
//新线程跑完
// return (void*)111;
// pthread_exit((void*)123);
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM ;i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
printf("wait sub thread...\n");
sleep(5);
printf("cancal sub thread...\n");
pthread_cancel(tid[0]);
void* status = NULL;
pthread_join(tid[0], &status);
printf("ret: %d\n", (int)status);
return 0;
}
那如果用新线程取消主线程呢?
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_t g_id;
void* thread_run(void* args)
{
int num = *(int*)args;
while(1)
{
printf("我是新线程[%d], 我的线程ID是%lu\n", num, pthread_self());
sleep(2);
pthread_cancel(g_id);
}
}
#define NUM 1
int main()
{
g_id = pthread_self();
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM ;i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
printf("wait sub thread...\n");
sleep(50);
printf("cancal sub thread...\n");
pthread_cancel(tid[0]);
void* status = NULL;
pthread_join(tid[0], &status);
printf("ret: %d\n", (int)status);
return 0;
}
我们发现主线程变为 < defunct >,类似于僵尸进程,而新线程还愉快的跑着,这与主函数return不同,但我们不建议也极少有情况需要这样做 ——
2.4 pthrerad_detach 线程分离
等待,如果我不想等呢?—— 线程分离,分离后的线程不需要被join,运行完毕后,会自动释放Z-pcb,类比signal(SIGCHLD, SIG_IGN)
,相当于同一屋檐下的陌生人,你不要关心我啦。
如何分离?
#include <pthread.h>
int pthread_detach(pthread_t thread);
Compile and link with -pthread.
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_t g_id;
void* thread_run(void* args)
{
pthread_detach(pthread_self());
int num = *(int*)args;
while(1)
{
printf("我是新线程[%d], 我的线程ID是%lu\n", num, pthread_self());
sleep(2);
break;
}
//新线程跑完
return (void*)111;
// pthread_exit((void*)123);
}
#define NUM 1
int main()
{
g_id = pthread_self();
pthread_t tid[NUM];
int i = 0;
for(i = 0; i < NUM ;i++)
{
pthread_create(tid+i, NULL, thread_run, (void*)&i);
sleep(1);
}
printf("wait sub thread...\n");
sleep(1);
printf("cancal sub thread...\n");
// pthread_cancel(tid[0]);
//void* - 占8个字节 - 指针变量,本身就可以充当某种容器保存数据
void* status = NULL;
int ret = 0;
for(i = 0; i < NUM;i++)
{
ret = pthread_join(tid[0], &status);
}
printf("ret: %d, status: %d\n", ret, (int)status);
sleep(3);
return 0;
}
说明我们pthread_join是失败的,status也没拿到退出结果 ——
一个线程被设为分离后,绝对不能再join了!分了就是分了,别再等我啦~
一般场景是主线程不退出,新线程业务处理完毕后再退出。