目录
1. 线程概念的铺设
2. Linux线程概念
2.1 什么是线程
2.2 线程的优点
2.3 线程的缺点
2.4 线程异常
2.5 线程用途
3. Linux进程VS线程
4. Linux线程控制
4.1 POSIX线程库
4.2 创建线程
4.3 进程ID和线程ID
4.4 线程终止
4.5 线程等待
4.6 分离线程
Linux🌷
1. 线程概念的铺设
在我们之前的学习中都是一个进程对应一个执行流,也就是说进程是OS分配资源的基本单位,也是CPU调度的基本单位;
今天,我们便要学习一个进程对应一个执行流,或者是一个进程对应多个执行流的情况,这里的每个执行流便可称为一个线程;
也就是说一个进程内是可能存在多个线程的,进程与线程数之比=1:n;内核中有可能存在着大量的线程,OS便要对这些线程进行管理。“先描述再组织”是OS管理对象的一个准则,线程是通过线程控制块(TCB)描述的,这是常规的一些操作系统的做法,比如Windows;
在Linux中是没有专门为线程设计TCB的,而是用进程PCB来模拟线程。
下面用一张图来大概说明下线程与进程:
对于线程来说只创建task_struct,将当前进程的资源(代码+数据)划分为若干份让每个task_struct用,每个task_struct就是一个需要被调度的执行流;对于CPU来说,此时看到的task_struct是小于原先的task_struct的,这里的task_struct也成为轻量级进程;多个线程是共享同一进程的地址空间的;
这样做不用维护复杂的进程和线程的关系,不用单独为线程设计任何算法,直接使用进程的一套相关的方法,OS只需要聚焦在线程间的资源分配上就好了;
进程的今昔对比:
- 之前的进程,内部只有一个执行流;
- 今天的进程,内部可以具有多个执行流;
Linux线程与接口关系的认识:
Linux中的线程是用进程模拟的,Linux中没有提供直接操作线程的接口,只是提供了在同一地址空间内创建task_struct的方法,分配资源给指定的task_struct的接口,这种方法对用户特别不友好,系统级别的工程师在用户层,对Linux轻量级进程接口进行了封装,给我们打包成了库,让用户直接使用库函数,这个库称为 原生线程库 是用户层的;
2. Linux线程概念
2.1 什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”;
- 一切进程至少都有一个执行线程;
- 线程在进程内部运行,本质是在进程地址空间内运行;
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化;
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流;
2.2 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多;
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多;
- 线程占用的资源要比进程少很多;
- 能充分利用多处理器的可并行数量;
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务;
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现;
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作;
在这里要注意的一点是:
对于计算密集型应用,并不是说线程越多是越好的,一般线程的个数与CPU的核数相等即可,如果线程太多会导致被过度调度切换(有成本的),假如只有一个CPU划分了10个线程,这样的话还不如让一个进程直接在CPU上运行;
对于IO密集型应用,IO是允许多一些线程,如果一个进程需要等待磁盘资源、IO资源,那么便可以将该进程分为多个线程,让等待这些资源的时间重叠,从而缩短整个等待时间。但也不是说越多越好的,因为磁盘只有一个,划分为再多的线程也只有排队等待同一个磁盘资源,无济于事;
2.3 线程的缺点
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计
算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指
的是增加了额外的同步和调度开销,而可用的资源不变。
2. 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3. 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响。
编写与调试一个多线程程序比单线程程序困难得多
2.4 线程异常
- 单个线程如果出现除零,野指针等问题导致线程崩溃,进程也会随着崩溃;
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出;
2.5 线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率;
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现);
3. Linux进程VS线程
1. 进程和线程
- 进程是资源分配的基本单位;
- 线程是CPU调度的基本单位;
- 线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
在这最重要的是:栈和上下文;
栈保存临时数据,上下文用于CPU的调度切换;
2. 进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表;
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数);
- 当前工作目录;
- 用户id和组id;
4. Linux线程控制
4.1 POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的;
- 要使用这些函数库,要通过引入头文<pthread.h>;
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项;
4.2 创建线程
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)
(void*), void *arg);
参数
thread:输出型参数,返回线程ID;
attr:设置线程的属性,attr为NULL表示使用默认属性;
start_routine:是个函数地址,线程启动后要执行的函数;
arg:传给线程启动函数的参数;
返回值:成功返回0;失败返回错误码
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误;
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回;
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小;
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
void *thread_run(void *arg)
{
int i;
for(;;)
{
printf("I am %s,pid:%d\n",(char*)arg,getpid());
sleep(1);
}
}
int main()
{
//用于接收新创建的线程的id
pthread_t tid;
//用于接收创建线程函数的返回值
int ret;
//创建线程
if((ret=pthread_create(&tid,NULL,thread_run,(void*)"thread1"))!=0)
{
printf("pthread_create:%s\n",strerror(ret));
return 1;
}
//主执行流
int i;
for(;;)
{
printf("I am main thread!pid:%d\n",getpid());
sleep(1);
}
return 0;
}
4.3 进程ID和线程ID
- 在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
- 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
- Linux内核引入了线程组的概念
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
- 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID;
查看线程ID:
ps -aL
强调一点:
线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。
在程序中,我们也可以使用函数的方式获得该线程自身的ID:
#include <pthread.h>
pthread_t pthread_self(void);
该函数获得的线程ID和刚才查出的线程ID是不同的,我们查到的线程ID是 pthread 线程库的线程ID,使用命令的方式查看的是Linux内核中的LWP,pthread库的线程ID是一个虚拟地址,如下图中的pthread_t id所示;
4.4 线程终止
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit(终止整个进程);
- 线程可以调用pthread_ exit终止自己;
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程;
功能:线程终止
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
功能:取消一个执行中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
4.5 线程等待
线程等待的原因:
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join函数:
功能:等待线程结束
原型:
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID;
value_ptr:输出型参数,用来获取新线程推出时候的函数的返回值;
返回值:成功返回0;失败返回错误码
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值;
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED((void*)-1);
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数;
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数;
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
void *thread1( void *arg )
{
printf("thread 1 returning ... \n");
int *p = (int*)malloc(sizeof(int));
*p = 1;
return (void*)p;
}
void *thread2( void *arg )
{
printf("thread 2 exiting ...\n");
int *p = (int*)malloc(sizeof(int));
*p = 2;
pthread_exit((void*)p);
}
void *thread3( void *arg )
{
while(1)
{
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
void *ret;
//thread1 return
//创建线程1
pthread_create(&tid, NULL, thread1, NULL);
//等待线程1
pthread_join(tid,&ret);
printf("thread return, thread id:%X, return code:%d\n",tid,*(int*)ret);
free(ret);
//thread2 exit
//创建线程2
pthread_create(&tid, NULL, thread2, NULL);
//等待线程2
pthread_join(tid,&ret);
printf("thread return, thread id:%X, return code:%d\n",tid,*(int*)ret);
free(ret);
//thread3 cancel by other
//创建线程3
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid,&ret);
if(ret==PTHREAD_CANCELED)
printf("thread return, thread id:%X, return code:PTHREAD_CANELED\n");
else
printf("thread return, thread id:%X, return code:NULL\n",tid);
return 0;
}
线程对于整个程序来说,根本上也是用函数的形式呈现的;
函数退出时有三种情况:
- 1. 代码跑完结果正确;
- 2. 代码跑完结果不正确;
- 3. 代码异常;
对于上述3种情况,我们只 join 前两种,因为线程代码异常的话,OS会发信号给进程,整个进程就瘫痪了,无需考虑此情况;
4.6 分离线程
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏;
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_run(void *arg)
{
pthread_detach(pthread_self());
printf("%s\n",(char*)arg);
return NULL;
}
int main()
{
pthread_t tid;
if(pthread_create(&tid, NULL, thread_run, (void*)"thread1")!=0)
{
printf("create thread error!\n");
return 1;
}
int ret=0;
//很重要,让新创建的线程先分离,在等待
sleep(1);
if(pthread_join(tid,NULL)==0)
{
printf("pthread wait success!\n");
ret=0;
}
else
{
printf("pthread wait failed!\n");
ret=1;
}
return ret;
}
如果本篇博客对您有所收获的话,还请点赞👍、收藏🤏加关注🎈