Linux多线程-概念及控制
@TOC
零、前言
本章主要讲解学习Linux中的线程
一、Linux线程概念
1、什么是线程
- 概念:
在一个程序里的一个执行路线就叫做线程(thread),更准确的定义是:线程是“一个进程内部的控制序列”
一切进程至少都有一个执行线程,也就是主线程,进程由一个或者多个线程组成,即进程中可以有多个执行流
线程是进程的一个执行分支,实在进程内部运行的一个执行流,本质是在进程地址空间内运行,共享进程的进程地址空间,执行进程的一部分代码
- 以整个运行视角理解:
程序运行,将代码和数据加载到CPU上,同时系统创建对应的进程进行承担分配系统资源,如创建task_struct结构体,构建对应的进程地址空间,页表建立虚拟地址与物理地址的映射等等,即进程是承担分配系统资源的基本单元
在进程中可能存在多个执行流(一定有个主执行流),也就是线程,而这些执行流都是由task_struct描述的,共享同一个进行地址空间,透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流,执行程序的部分代码,这些执行流可以进行并发执行,由于是在进行内部运行,不用切换整个进程的上下文数据,只需切换线程的上下文数据,即线程是系统调度的基本单元
- 示图:
注:在Linux系统下的CPU眼中,看到的PCB(task_struct)都要比传统的进程更加轻量化
- 如何理解之前所说的’进程’:
进程是一个大的整体,包括task_struct(PCB),进程地址空间、文件、信号等,是承担分配系统资源的基本实体,而之前所受的进程都只有一个task_struct,也就是该进程内部只有一个执行流
- 注意:
在Linux中,CPU只关心一个一个的独立执行流,无论进程内部只有一个执行流还是有多个执行流,CPU都是以task_struct为单位进行调度的
Linux下并不存在真正的多线程,而是用进程模拟的。如果要支持真的线程(TCB)会提高操作系统的复杂程度。而线程的和进程的控制块基本是类似实现的,因此Linux直接复用了进程控制块,所以Linux中的所有执行流都叫做轻量级进程
在Linux中都没有真正意义的线程,所以也就没有真正意义上的线程相关的系统调用,但是Linux提供了轻量级进程相关的库和接口,例如vfork函数和原生线程库pthread
2、vfork函数/pthread线程库
- vfork函数原型:
pid_t vfork(void);
- 注意:
功能:创建子进程,但是父子共享进程地址空间
返回值:成功给父进程返回子进程的PID;给子进程返回0
示例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int val=100;
pid_t id=vfork();
if(id==0)
{
//child
int cnt=0;
while(1)
{
printf("i am child pid:%d ppid:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
cnt++;
sleep(1);
if(cnt==2)
val=200;
if(cnt==5)
exit(0);
}
}
else if(id>0)
{
//father
int cnt=0;
while(1)
{
printf("i am father pid:%d ppid:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
cnt++;
sleep(1);
if(cnt==3)
val=300;
}
}
return 0;
}
- 效果:
注:vfork() 保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行;如果子进程没有调用 exec, exit, 程序则会导致死锁,程序是有问题的程序,没有意义
- 原生线程库pthread:
在Linux中,站在内核角度没有真正意义上线程相关的接口,但是站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,因此系统为用户层提供了原生线程库pthread
原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口
3、线程优缺点及其他分析
- 线程的优点:
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作(如边下视频边看视频)
- 注意:
计算密集型:执行流的大部分任务,主要以计算为主。比如加密解密、大数据查找等
IO密集型:执行流的大部分任务,主要以IO为主。比如刷磁盘、访问数据库、访问网络等
- 线程的缺点:
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了;不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺保护的
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
- 线程异常:
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
- 线程用途:
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
二、Linux进程VS线程
1、进程和线程
- 概念:
进程是资源分配的基本单位
线程是调度的基本单位
- 线程共享进程数据,但也有线程自己独有的数据:
线程ID
一组寄存器中线程自己的上下文数据
栈
errno
信号屏蔽字(handler方法是共享的)
调度优先级
- 线程中共享的数据:
代码段和数据段
文件描述符表
每种信号的处理方式
当前工作目录
用户id和组id
注:进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到
- 进程和线程的关系图:
三、Linux线程控制
1、POSIX线程库
- pthread线程库是应用层的原生线程库:
应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方提供的
原生指的是大部分Linux系统都会默认带上该线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文件<pthreaad.h>
链接这些线程函数库时,要使用编译器命令的“-lpthread”选项
- 错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误
pthreads函数出错时不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小
2、线程创建
- pthread_create函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
- 解释:
功能:创建一个新的线程
参数:thread:输出型参数,返回获取线程ID;attr:设置线程的属性,attr为NULL表示使用默认属性;start_routine:是个函数地址,线程启动后要执行的函数,该函数返回值为void *,参数为void *;arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
- 注意:
主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行参入的函数,而主线程则继续往下执行
对于执行函数来说,参数和返回值的类型都是void *,void *是一个通用的类型,可以传入或者返回数据和其他类型的指针,从而传入和带出多样的类型和数据
- 示例:
mypthread.c:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
int val=0;
void* Routine(void* avgs)
{
while(1)
{
printf("I am %s... val:%d\n",(char*)avgs,val);
sleep(1);
}
}
int main()
{
pthread_t tid1,tid2,tid3;
int ret1=pthread_create(&tid1,NULL,Routine,(void*)"pthread 1");
if(ret1!=0)
{
fprintf(stderr,"pthread_creat:%s\n",strerror(ret1));
exit(1);
}
int ret2=pthread_create(&tid2,NULL,Routine,(void*)"pthread 2");
if(ret2!=0)
{
fprintf(stderr,"pthread_creat:%s\n",strerror(ret2));
exit(1);
}
int ret3=pthread_create(&tid3,NULL,Routine,(void*)"pthread 3");
if(ret3!=0)
{
fprintf(stderr,"pthread_creat:%s\n",strerror(ret3));
exit(1);
}
while(1)
{
printf("I am main pthread...val:%d\n",val++);
sleep(1);
}
return 0;
}
Makefile:
mypthread:mypthread.c
gcc -o $@ $^ -pthread
.PHONY:clean
clean:
rm -f mypthread
- 效果:
- 查看线程信息:ps -aL
- 注意:
默认情况下,ps命令不带
-L
,看到的就是一个个的进程;带-L
就可以查看到每个进程内的多个轻量级进程在Linux中,应用层的线程与内核的LWP是一一对应的,实际上操作系统调度的时候采用的是LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的
3、线程ID及线程地址空间布局
- 概念:
- pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事
- 前面讲的线程ID(LWP)属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
- pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的
- 在Linux系统层面有LWP与线程对应,但是Linux是用轻量级进程模拟的线程,而对于用户来说,并不会关心底层实现,从用户角度来说,他们也需要知道线程的信息,状态以及操作线程,由此在共享区中还相应的构建了TCB(线程控制块),便于用户操作线程,在用户区进行维护
- pthread_ self函数原型:
pthread_t pthread_self(void);
功能:获得线程自身的ID
注:对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
- 示图:
注:主线程并不使用动态库里的线程栈,而是使用进程里的栈
4、线程终止
- 终止线程的三种方法:
从线程函数return
线程可以调用pthread_ exit终止自己
线程可以调用pthread_ cancel终止同一进程中的另一个线程或者自己
注:在主线程使用return,以及在线程中使用exit都会终止整个进程
- pthread_exit函数原型:
void pthread_exit(void *value_ptr);
- 解释:
功能:线程终止
参数:value_ptr线程退出传出的数据(不要指向一个局部变量)
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它自身
注:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
- pthread_cancel函数原型:
int pthread_cancel(pthread_t thread);
- 解释:
功能:取消一个执行中的线程
参数:thread表示要操作的线程的ID
返回值:成功返回0;失败返回错误码
注:pthread_cancel函数具有一定的延时性,并不会立即被处理,不建议当线程立即被创建后立即进行cancel取消(线程创建,并不会立即被调度);也不建议在线程退出前执行线程cancel取消(线程可能在取消之前就已经退出了);建议在线程执行中进行cancel取消线程
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{
printf("%s returning ... \n",(char*)arg);
int *p = (int*)malloc(sizeof(int));
*p = 1;
return (void*)p;
}
void *thread2(void *arg)
{
printf("%s exiting ...\n",(char*)arg);
int *p = (int*)malloc(sizeof(int));
*p = 2;
pthread_exit((void*)p);
}
void *thread3(void *arg)
{
while ( 1 ){ //
printf("%s is running ...\n",(char*)arg);
sleep(1);
}
return NULL;
}
int main( void )
{
pthread_t tid;
void *ret;
// thread 1 return
pthread_create(&tid, NULL, thread1, (void*)"thread 1");
pthread_join(tid, &ret);
printf("thread 1 return, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret);
// thread 2 exit
pthread_create(&tid, NULL, thread2, (void*)"thread 2");
pthread_join(tid, &ret);
printf("thread 2 exit, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret);
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, (void*)"thread 3");
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
printf("thread 3 cancel, thread id %X, return code: PTHREAD_CANCELED->%d\n", tid,ret);
else
printf("thread return, thread id %X, return code:%d\n", tid,ret);
return 0;
}
- 效果:
5、线程等待
- 为什么需要线程等待:
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,创建新的线程不会复用刚才退出线程的地址空间,如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的。如果不等待会产生内存泄漏
线程是用来执行分配的任务的,如果主线程想知道任务完成的怎么样,那么就有必要对线程进行等待,获取线程退出的信息
- pthread_join函数原型:
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线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
- 示图:
- 示例:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
int val=0;
struct Ret
{
int exitno;
int exittime;
//...
};
void* Routine(void* avgs)
{
int cnt=1;
while(1)
{
printf("I am %s... val:%d\n",(char*)avgs,val);
sleep(1);
cnt++;
if(cnt==3)
{
struct Ret* p=(struct Ret*)malloc(sizeof(struct Ret));
p->exitno=0;
p->exittime=6666;
pthread_exit((void*)p);
//pthread_cancel(pthread_self());
}
}
}
int main()
{
pthread_t tid1,tid2,tid3;
pthread_create(&tid1,NULL,Routine,(void*)"pthread 1");
pthread_create(&tid2,NULL,Routine,(void*)"pthread 2");
pthread_create(&tid3,NULL,Routine,(void*)"pthread 3");
int cnt=0;
while(1)
{
printf("I am main pthread...val:%d\n",val++);
sleep(1);
cnt++;
if(cnt==3)
break;
}
printf("wait for pthread...\n");
void* ret;
pthread_join(tid1,&ret);
printf("pthread id:%x exitno:%d exittime:%d\n",tid1,((struct Ret*)ret)->exitno,((struct Ret*)ret)->exittime);
pthread_join(tid2,&ret);
printf("pthread id:%x exitno:%d exittime:%d\n",tid2,((struct Ret*)ret)->exitno,((struct Ret*)ret)->exittime);
pthread_join(tid3,&ret);
printf("pthread id:%x exitno:%d exittime:%d\n",tid3,((struct Ret*)ret)->exitno,((struct Ret*)ret)->exittime);
return 0;
}
- 效果:
6、线程分离
- 概念:
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
- pthread_detach函数原型:
int pthread_detach(pthread_t thread);
- 注意:
- 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离: pthread_detach(pthread_self());
- joinable和分离是冲突的,一个线程不能既是joinable又是分离的
- 线程的分离也是具有一定延时性,分离之后如果再进行等待那么得到返回的结果是未定义的
- 线程分离后只是回收的时候自动进行回收,如果主线程先退出,那么整个进程也会退出;如果分离的线程执行崩溃,同样的整个进行也会崩溃
- 示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* Routine (void* arg)
{
pthread_detach(pthread_self());
printf("%s detach success!\n");
int cnt=0;
while(cnt<5)
{
cnt++;
printf("%s running...\n",(char*)arg);
sleep(1);
}
printf("%s return...\n");
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,Routine,(void*)"thread");
sleep(2);//等待线程分离
void* ret;
if(pthread_join(tid,&ret)==0)
printf("thread join success! ret:%d\n",(int*)ret);
else
printf("thread join fail... ret:%d\n",(int*)ret);
return 0;
}
- 效果:
- 点赞
- 收藏
- 关注作者
评论(0)