本文导航:
内容 | 所占百分比 |
---|---|
线程概念 | 40% |
线程与进程区别与联系 | 20% |
线程优缺点 | 10% |
线程控制(创建,终止,等待) | 30% |
谈到线程,我们先从进程说起。
我们写的程序从硬盘加载到内存开始运行时,进程就产生了。也就是操作系统开始为这个程序创建PCB,分配系统资源,比如分配一块虚拟地址空间,一个页表,一块物理内存。当这个进程内部有多个执行流时,现在我们可以简单理解父进程用vfork()
创建了多个子进程时,这些子进程也需要资源。如果能分得资源就会独自分的资源,但如果分不了就共享。共享一块地址空间。共享一个页表和一块物理内存。
为了便于来理解,我画了一张简明图
在这张图中,有3个PCB,在以前的认知里,我们叫做3个进程。
现在我们需要引入线程的概念。
线程概念
线程是一个进程内部的控制序列。控制序列可以理解为一个执行流。进程内部是指虚拟地址空间
一个进程至少有一个线程。这个时候可以粗略理解为进程就是线程。
进程到线程
在明确线程概念后,我们就要站在线程的角度去理解上面那张图了。
图中3个PCB ,我们就要把他们称之为3个线程了。因为这是在进程里的3个执行控制流。他们共享一块虚拟地址空间。一块页表,所以这3个线程所看到的物理类存也是一样的。但是他们可以执行各自的任务,并有自己生命特征。
所以在图中,进程就是创建执行代码,创建PCB,申请资源,分配资源的实体,即整个图。而线程就是那些单个PCB,在线程中称之为TCB。
或许下面这张图有助于来理解。
线程是资源竞争的基本单位。
操作系统有很多资源。进程与进程之间要竞争操作系统资源,当一个进程申请得到一大堆资源。而这些资源又会分配给线程。一个进程内部有多个线程,去竞争进程所获得的资源。所以说线程是资源竞争的基本单位。
线程是程序执行的最小单位
当用户让进程去执行某个任务时,进程又会将任务细化。进程内部有很多线程,让这些线程去执行
其中最重要的数据是栈和寄存器。私有栈是为了保存临时变量,便于函数调用等操作。私有寄存器是为了方便线程切换,保存上下文。
.同地址空间,因此Test Segment,Data Segment都是共享的,如果定义个函数雇各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
在Linux下并没有专门为线程设计这么一个概念,也就是说没有真正意义上的线程,它在linux下是由进程模拟的,可以认为所有的PCB都可以称为轻量级进程(不一定是进程,也可能是线程)。进程是分配系统资源的一个实体,而线程是CPU调度的基本单位。
在单个程序中同时运行多个线程完成不同的工作,称为多线程。相对来说,多进程相对稳定,多线程相对不稳定。可以认为,进程是分配系统资源的一个实体,而线程是CPU调度的基本单位。
进程具有独立的地址空间,而线程并没有,同一进程内部的线程共享进程的地址空间。
由于Linx下没有真正的线程,Linux用进程来描述线程和组织线程。所以在Linux下只有轻量级的线程,操作系统是看不到线程的TCB的。所以下图中的 第四个图:多个进程多个线程图中,操作系统可以看到的是6个PCB,
创建一个新线程的代价要比创建一个新进程小得多,释放成本也更低。因为创建一个进程就意味着要创建PCB,分配虚拟地址空间,页表,物理内存等系统资源,而创建一个线程只需要创建一个PCB(TCB)即可
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。进程切换,需要切换对应的虚拟地址空间,更换页表等。过程繁琐。
线程占用的资源要比进程少很多。
能充分利用多处理器的可并行数量。
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。比如打开一个网易云音乐播放器,你可以一边下载,一边在线听歌。这就是两个线程在工作。
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
进程可以将线程串行执行变成并行执行。最后汇总,提高效率。但是尽量不要创建太多线程,线程切换也是需要成本的。
性能损失
一个很少被外部事件阻塞的计算密集型线程往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。一个线程挂掉,因为线程共享一块资源。其他线程也会挂掉。进而导致进程退出,资源被回收。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调⽤用某些OS函数会对整个进程造成影响。多线程访问临界资源,它的访问控制是由编程者决定。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多,基于同步互斥。你需要不断的加锁,
与线程有关的函数构成了一个完整的系列,绝⼤大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文件, 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
常见操作如下
返回值:成功返回0,失败返回 错误码
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回 pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
测试用例
注意:编译链接时,一定要加上-lpthreead选项,见图中命令部分。因为这是用户级别的库,并不是系统提供的库。
#include
#include
#include
//新线程的例程(执行任务,打印字符串)
void * my_run( void *arg)
{
while(1)
{
printf( " i am %s\n",( char *)arg);
sleep( 1);
}
}
int main( )
{
//申明一个本地变量tid,用来保存对等线程的ID
pthread_t tid;
//调用pthread_create函数创建一个新的线程,
pthread_create(&tid,NULL,my_run,"newthread");
//调用结束,同时运行,并且tid包含了新线程的id
while(1)
{
printf( " i am main thread\n");
sleep(2);
}
return 0;
}
pthread_create创建成功!
主线程和新线程各自打印不同的内容。
正常情况下,一个程序里是不会同时执行两个死循环的。但是,在这个程序里,居然可以同时执行两个死循环。原因就在于这时两个线程在运行。各自执行不同的任务,有自己的栈空间和寄存器,支持上下文切换和函数调用。
如果需要只终止某个线程而不终止整个进程,可以有以下方法:
void * my_run( void *arg)
{
while(1)
{
printf( " i am %s\n",( char *)arg);
sleep( 1);
return NULL;
}
}
新线程终止,主线程继续执行。
- 线程可以调用pthread_ exit终止自己。
pthread exit函数
功能:线程终止
#include
void pthread_exit(void *retval);
参数
value ptr:value ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
#include
#include
#include
#include
void * my_run( void *arg)
{
printf("thread 1 returning ....\n");
int *p=(int *)malloc(sizeof(int));
*p=1;
pthread_exit((void *)p);
}
int main( )
{
pthread_t tid;
void *ret;
//thread 1 exit
pthread_create(&tid,NULL,my_run,NULL);
//等待新线程退出,退出码保存在ret中
pthread_join(tid,&ret);
printf("thread return,id is:%ld,return code:%d\n",tid,*(int *)ret);
free(ret);
while(1)
{
printf("i am main thread\n");
sleep(2);
}
return 0;
}
,pthread exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
调用phread_exit(),他会等待所有其他对等线程(就是同一个进程中的除自身外其他线程)终止,然后再终止主线程和整个进程。
如果某个对等线程调用linux的exit函数,则该函数终止进程以及所有与该进程相关的线程
pthread_cancel函数
功能:取消一个执行中的线程
#include
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
void* my_run_2(void* arg)
{
while(1)
{
printf( "thread 2 is running...\n");
sleep(1);
}
return NULL;
}
int main( )
{
pthread_t tid;
void *ret;
pthread_create(&tid,NULL,my_run_2,NULL);
sleep(1);
//取消执行中的线程2
pthread_cancel(tid);
//等待线程2的返回状况,获取返回值
pthread_join(tid,&ret);
if(ret==PTHREAD_CANCELED)
{
printf("thread return,id is:%ld,return code:PTHREAD_CANCELED\n",tid);
}
else
{
printf("thread return,id is:%ld,return code:NULL\n",tid);
}
return 0;
}
介绍一个获取当前线程id的函数
#include
pthread_t pthread_self(void);
获取当前文件中线程tid命令
ps -eLf | head -1&& ps -eLf | grep a.out
以上都是显示终止,另外还有隐式终止。即:顶层的线程调用返回时,线程会隐式终止。
为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。 创建新的线程不会复⽤用刚才退出线程的地址空间。
功能:等待线程结束原型
int pthread_join(pthread_t thread, void **value ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
如果thread线程通过return返回,value ptr所指向的单元⾥里存放的是thread线程函数的返回值。
如果thread线程被别的线程调⽤用pthread cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD CANCELED。
如果thread线程是自调用pthreadexit终止的,valueptr所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
测试用例可以参考上面的代码。
在默认情况下,新线程默认是joinable的。线程退出后,需要对其进行pthread_join操作来回收资源,否则无法释放资源,造成内存泄露。
如果不关心线程的返回值,join就是一种负担。线程提供一种自动回收的方法,叫做线程分离。当线程分离,退出后,自动释放线程资源。
线程分离接口
#include
int pthread_detach(pthread_t thread);
#include
int pthread_detach(pthread_t self());
返回值:都是成功返回0,失败返回-1.
线程被分离后,就不能在进行pthread_join()操作。否则会出错。因为分离后的线程资源自动就被回收了,再进行等待回收资源。必定导致等待失败。
测试用例如下
#include
#include
#include
#include
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,"thread1 run...")!=0)
{
printf("create thread error\n");
return 1;
}
//主线程分离新线程
//pthread_detach(tid);
int ret=0;
sleep(1);
if( pthread_join( tid,NULL)==0)
{
printf("wait thread success\n");
ret=0;
}
else
{
printf("wait thread failed\n");
ret= 1;
}
return ret;
}