多线程
线程概念
大条件:
Linux
中:
因为Linux
环境下线程以进程的PCB
模拟实现线程,所以Linux
中PCB
是线程。因此Linux
线程也叫轻量级进程。
进程就是线程组。以前所描述的进程其实是单线程进程。
因为Linux
线程是PCB
,因此线程是CPU
调度的基本单位。
因为进程是线程组,程序运行起来,资源是分配给整个线程组的,因此进程是资源分配的基本单位。
多进程可以并行多任务 / 多线程也可以并行多任务,何者更优?
多进程与多线程优缺点对比
- 优点:
一个进程中的线程共用同一个虚拟地址空间,因此:
- 线程间通信更加方便。
- 线程的创建 / 销毁成本更低。
- 线程间切换调度成本更低。
- 线程的执行粒度更细致。
- 缺点:
线程缺乏访问控制:系统调用(如exit()
),异常针对的是整个进程,健壮性低。
多进程 / 多线程进行多任务处理的优势体现与细节:
CPU
密集型程序IO
密集型程序
vfork()
创建一个子进程共用同一块虚拟地址空间,避免出现调用栈混乱,因此子进程运行完毕或程序替换后父进程才开始运行。
多线程PCB
使用同一块虚拟地址空间,如何实现同步运行而不会出现调用栈混乱?
为每个线程在虚拟地址空间中单独分配一块空间,每个线程都会有一些独立的信息(栈、寄存器、errno
、信号屏蔽字、调度优先级)
线程之间共享的数据:代码段、数据段、文件描述符表、每种信号的处理方式、用户id、组id、当前工作目录路径。
线程控制
操作系统并没有为用户提供直接创建线程的系统调用接口,因此提供者封装了一套线程库,实现线程控制。
因此我们用户创建的线程是一个用户态线程,在内核态中对应了一个轻量级进程,实现程序的调度运行。
线程创建
pthread_create()接口
需要包含的头文件<pthread.h>
,因为是库函数,因此链接时需要加上-pthread
或-lpthread
选项来链接线程库。
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);
thread
:输出型参数,用于获取新创建的线程tid
。attr
:设置线程属性,通常置NULL
。start_routine
:线程的入口函数,是一个函数指针。arg
:作为实参,通过线程入口函数传递给线程。- 返回值:
成功返回0
。
失败返回errno
,它的大小>0
。
区别:
tid
:线程地址空间的首地址。task_struct
中的pid
:LWP
。task_struct
中的tgid
:线程组id
(主线程的pid
)(也是进程id
)。
ps -L
:查看轻量级进程(线程)信息。其中LWP
查看的就是task_struct
中的pid
。
pthread_self()接口
返回调用线程的id
。
线程创建实例
void *thr_start(void *arg){
//pthread_t pthread_self(void);
// 返回调用线程id
pthread_t tid = pthread_self();
while(1) {
printf("i am thread~~~%s-%p\n", (char*)arg, tid);
sleep(1);
}
return NULL;
}
int main(int argc, char *argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"giturtle");
if (ret != 0) {
printf("thread create error\n");
return -1;
}
printf("main tid:%p\n", pthread_self());
printf("child tid:%p\n", tid);
while(1) {
printf("hello world~~~\n");
sleep(1);
}
return 0;
}
运行结果
main tid:0x7ff449b99740
child tid:0x7ff4493a7700
hello world
i am thread~~giturtle-0x7ff4493a7700
hello world
i am thread~~giturtle-0x7ff4493a7700
hello world
i am thread~~giturtle-0x7ff4493a7700
hello world
i am thread~~giturtle-0x7ff4493a7700
hello world
i am thread~~giturtle-0x7ff4493a7700
hello world
...
线程终止
-
在线程入口函数中
return
。
main
函数中不能return
,否则退出的是进程。 -
使用接口退出调用线程:
pthread_exit()接口
pthread_exit(void *retval);
pthread_exit(hello!);
退出调用线程,主线程退出,进程并不会退出,等所有线程退出进程才会退出。
线程退出也会成为僵尸线程(普通线程退出无效果),线程也要保存返回值,线程地址空间无法被回收再利用,造成资源泄露。
- 使用接口取消一个指定的线程:
pthread_cancel()接口
pthread_cancel(tid);
取消一个指定线程,方式为被动退出。
【注】如果一个线程是被取消的,则返回值是一个宏PTHREAD_CANCELED
,它的值是-1
。
调用pthread_cancel()
终止另一线程,而对于接收cancel
信号后结束执行的目标线程,等同于线程自己执行pthread_exit(PTHREAD_CANCELED);
,也就是说,当一个线程被强制终止执行时,它会返回 PTHREAD_CANCELED
这个宏(定义在<pthread.h>头文件中,值为 -1
)。
注意,pthread_cancel() 函数的功能仅仅是向目标线程发送 Cancel 信号,至于目标线程是否处理该信号以及何时结束执行,由目标线程决定。
线程终止实例
void *thr_start(void *arg){
while(1) {
printf("child thread\n");
sleep(1);
//或者使用 pthread_exit(NULL);退出线程
}
return NULL;
}
int main(int argc, char *argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
pthread_cancel(pthread_self());
while(1) {
printf("main thread\n");
sleep(1);
}
return 0;
}
运行结果
main thread
child thread
线程等待
- 线程等待的前提:线程能够被等待,即处于 joinable 状态。(本质是一个标志位)
因为一个线程运行起来默认拥有一个属性joinable
,处于这个属性的线程退出后不会自动释放资源,必须被其他线程等待。因为线程退出后,为了保存返回值,不会自动回收线程的资源而成为僵尸线程(现象不能直观体现)。
所以选择使用pthread_join()
接口实现线程等待,获取指定线程的返回值,并允许系统回收线程资源。
pthread_join()接口
int pthread_join(pthread_t thread, void **retval);
thread
:要等待的指定线程id
,并不能等待任意线程退出。retval
:输出型参数,用于获取退出线程的退出返回值。- 返回值:
成功返回0
,失败返回非零值errno
。
线程分离
将线程的一个属性从joinable
设置为detach
属性,设置线程的detach
属性退出后处于detach
状态,这类线程资源直接自动被回收。这类线程不能被等待。
线程的分离对于一个线程来说,无论何处任意线程调用都可以,被等待的线程无法被等待。若是非要pthread_join
等待,则会直接报错误返回。
pthread_detach()接口
线程的分离,对于一个线程来说,任意线程在任意位置调用都可以。
分离一个线程其实设置线程的detach
属性。
int pthread_detach(pthread_t thread);
thread
:要被分离的线程id
。- 返回值:返回错误码。通常如果用户对线程的返回值不关心,则在创建线程之后直接分离线程或者在线程入口函数中第一时间分离自己。
【 小结 】:
处于
joinable
状态的线程退出后不会自动释放资源,需要被等待。
处于detach
状态的线程退出后自动释放回收资源,不需要被等待。
进程分离实例
void *thr_start(void *arg){
//pthread_detach(pthread_self()); //分离一个指定的线程---自己也可以
sleep(3);
return "giturtle";
}
int main(int argc, char *argv[]){
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
pthread_detach(tid);
char *ptr = NULL;
ret = pthread_join(tid, (void**)&ptr); //强行等待一个不能被等待的线程
if (ret == EINVAL) {
printf("this thread can not be wait!!\n");
}
printf("retval:%d--%p\n", ret, ptr);
return 0;
}
运行结果
this thread cannot be wait!
return value:22,(nil)
在本篇博客的结尾给出多线程阶段(下篇)博客:
【https://blog.csdn.net/qq_42351880/article/details/89435599 】
其中包括死锁、同步与互斥、信号量,有兴趣的看官大佬可以了解一下~