我们知道,进程间是各自独立的,每个进程有他自己的私有地址空间,这便导致进程之间交换数据很困难。为了让多执行流完成各自的任务同时又能快速共享数据,这便是本文所要介绍的线程。
线程:是操作系统进行运算调度的基本单位,是一个进程内部的控制序列。进程与线程是1:N的关系,所以一个进程至少有一个线程(主执行流)。线程是在进程内部运行,其本质是在进程的地址空间内运行。
有些情况,需要在一个进程中执行多个执行流程,这时线程就派上用场了。比如使用下载软件,一边与用户交互,等待和处理用户的键盘鼠标时间,一边同时下载文件,等待和处理从多个网络主机发来的数据。
在之前的文章中所讨论的进程只有一个控制流,而在进程信号篇,我们知道main函数和信号处理函数sighandler是同一个进程地址空间的多个控制流程,多线程也是如此,但是比信号处理函数更加灵活:信号处理函数的控制流程只能在信号递达时产生,在信号处理完后就结束,而多线程的控制流程可以长期并存,并且操作系统可以在各线程之间调度和切换。
特别注意:同一进程下的多个线程共享同一地址空间:
Linux中没有专门为线程设计TCB,而是用进程的PCB(task_struct)来模拟线程。对于CPU而言,看到的仍然是一个个PCB,但是已经比传统的进程轻量化了。
因此代码段,数据段都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。
于是在任意一个时间段,同一个进程内的代码和数据,可以被CPU同时处理和推进(得益于多执行流的存在)。
除此之外,各线程还共享以下进程资源和环境:
但还有一些资源是每个线程独自占有的:
进程和线程的关系如下图所示:
优点:
缺点:
优点相对突出,Linux下由进程模仿出来的线程,导致两者的差别不是很大。
线程常用于:1)
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程。
进程终止,该进程内的所有线程也就随即退出。
因为Linux下,线程是由进程模拟的,所以Linux没有直接提供操作线程的接口,而是给我们提供在同一个地址空间内创建PCB的方法,分配资源给指定的PCB接口。好在工程师们在用户层对Linux轻量级进程接口进行了封装,给我们打包成库,让用户可以直接使用库接口(原生线程库)——POSIX线程库。
POSIX线程(POSIX Threads,缩写为Pthread)是POSIX的线程标准,定义了一套控制线程的API。
该标准下的线程系统调用名字大多以"pthread_"开头的。
使用线程库需要引用头文件pthread.h
,编译时需加上选项-lpthread
(小写L指定库名)。
ldd 查看依赖库
ps -aL :其中L查看轻量级进程(LWP)——OS调度的基本单位
PID与LWD相同的称为主线程,而一个进程下的所有线程称为线程组,一个线程组的组ID也是当前进程的PID。
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
//Compile and link with -pthread.
返回值:成功返回0,失败返回错误号且thread不定义。
注意:虽然每个线程拥有自己的errno,但这是为了兼容其他函数接口而提供的,pthread本身并不使用它。而读取返回值要比读取线程内的errno变量更加清晰。
函数参数
#include
pthread_t pthread_self(void);
pthread_create 成功返回后,新创建的线程id被填写到thread指向的内存中,其作用对应进程中的 getpid() 。
进程id是全局唯一的,而线程id在当前进程中保证唯一。不同的系统中pthread_t类型有不同的实现,可能为整数值,结构体或是地址,
在Linux下对应无符号长整型(unsigned long,%lu)。
调用pthread_self 可以在当前线程中获得线程id,与调用该进程的函数pthread_create中的thread值保持一致。
#include
#include
#include
void* thread1(void* argv)
{
while(1)
{
printf("%s(tid:%lu) is running...\n",(char*)argv,pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread1,(void*)"thread1");
while(1)
{
printf("i am main thread,i create thread:%lu\n",tid);
sleep(1);
}
return 0;
}
我们创建一个线程,查看主线程与新建线程的tid与pid。
#include
#include
#include
#include
#include
void* thread1(void* argv)
{
while(1)
{
printf("%s(pid: %d,tid: %lu) is running\n",(char*)argv,getpid(),pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int err;
err=pthread_create(&tid,NULL,thread1,(void*)"thread1");
if(err!=0)
{
fprintf(stderr,"can't create thread:%s\n",strerror(err));
exit(1);
}
while(1)
{
printf("i am main thread(pid: %d,tid: %lu),i create thread1(tid: %lu)\n",getpid(),pthread_self(),tid);
sleep(1);
}
return 0;
}
由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。
为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前使用while循环不断打印,这只是一种权宜之计,后面我们会看到更好的办法。
结果:
可以看到他们的pid是一致的,说明从属一个进程。
创建多个线程(省略出错处理):
#include
#include
#include
#include
void* thread_run(void* argv)
{
int num=*(int*)argv;
while(1)
{
printf("thread[%d](pid: %d,tid: %lu) is running\n",num,getpid(),pthread_self());
sleep(10);
}
return NULL;
}
int main()
{
pthread_t tid[5];
int i=0;
for(i=0;i<5;++i)
{
pthread_create(tid+i,NULL,thread_run,(void*)(&i));
sleep(1);
}
while(1)
{
printf("i am main thread(pid: %d,tid: %lu)\n",getpid(),pthread_self());
sleep(1);
}
return 0;
}
在新线程中发送信号,查看进程退出情况(省略出错处理):
#include
#include
#include
#include
void* thread_run(void* argv)
{
while(1)
{
printf("thread[%ld](pid: %d,tid: %lu) is running\n",(long)argv,getpid(),pthread_self());
sleep(3);
raise(2);//3秒后线程发信号SIGINT
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)0);
while(1)
{
printf("i am main thread(pid: %d,tid: %lu)\n",getpid(),pthread_self());
sleep(1);
}
return 0;
}
一个线程遇到退出信号,会导致整个进程退出。
通过 ps -aL
可以查看轻量级进程号:
但是我们发现LWP号和我们函数中打印的tid并不匹配。
线程 ID 具有什么含义呢?
首先得知Linux不提供线程,所以线程id不是来源于内核。
通过 ldd
指令可以得知线程库实际上是调用动态库:
进程运行时如果需要动态库,那么动态库从磁盘加载到内存,如果进程创建线程,那么可以动态库会把所需的线程通过页表映射到进程地址空间的共享区:
每个线程都有自己的私有栈来存放自己运行过程中的上下文数据,主线程采用的栈是进程地址空间的原生栈,其他的子线程采用的栈则是动态库映射到共享区开辟的线程结构体 —— struct pthread,其中包含了线程各种属性与数据,这点类似于进程控制块(PCB),但是进程控制块由操作系统维护处于内核态,而线程控制块处于用户层,由动态库维护。我们每新建一个线程,就在共享区中开辟一个线程结构体空间,并映射到内存的线程库里。
内存中的动态共享库会负责管理操作系统中的所有线程,在管理之前必须先记录每个线程控制块的地址,,而线程ID是一个虚拟地址(通过页表映射入线程库),用于找到共享区中的线程结构体(图中的紫框)。
大部分线程库函数都需要提供线程ID,本质都是对库内的线程控制块进行各种操作,最后将要执行的代码和线程数据交给对应的内核级LWP去执行就行了。
如果需要只终止某个线程而不终止整个进程,有三种方法:
在学习线程退出之前,我们应当先学习线程等待来了解如何获取线程函数的退出状态。
为什么需要线程等待?
类似于父进程等待并回收子进程的wait函数,作为主线程也应该知道线程退出的状态,得到线程函数的返回值,并回收资源。
#include
int pthread_join(pthread_t thread, void **retval);
功能:阻塞等待线程退出,获取线程退出状态。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的。
返回值:成功返回0,失败返回错误号。
参数
⚠这里有4点需要注意:
1. 对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。(关于线程分离会在后面说到)
2.被pthread_join释放的内存空间仅仅是系统空间,动态分配的空间(malloc)后续必须由等待的线程手动清除。
3.面对代码异常导致的线程退出,pthread_join无法处理,因为有异常导致系统发出信号是进程应该处理的问题,这也是为什么pthread_join不用获取信号。
4.大多项目中需要子线程计算后的值就需要加join方法。
线程函数的return可以让线程退出,可以返回共享数据段,全局变量,动态开辟的堆空间数据(注意free),结构体指针以及函数指针等数据(不能是函数中的局部或临时变量)。
终止调用者自身
#include
void pthread_exit(void *retval);
参数
返回值,无返回值,跟进程一样,线程结束的时候无法返回到他的调用者。
注意:我们不能使用exit将指定线程退出,因为这会导致整个进程退出。
多线程环境中,应尽量少用或者不使用exit函数,除非你非常清楚某个线程出了问题其他线程也别想跑了,取而代之使用pthread_exit函数,将单个线程退出。其他线程工作未结束,主控线程退出时不能return或exit。
要求:创建多线程,分别使用return,pthread_exit来退出线程,使用pthread_join来获取,其中在return中返回整数,pthread_exit返回结构体指针:
#include
#include
#include
#include
#include
pthread_t g_tid;
struct DOOR
{
int width;
int length;
};
void* thread1_run(void* argv)
{
printf("%s (tid: %lu) is running\n",(char*)argv,pthread_self());
return (void*)111;
}
void* thread2_run(void* argv)
{
printf("%s (tid: %lu) is running\n",(char*)argv,pthread_self());
struct DOOR* mydoor=(struct DOOR*)malloc(sizeof(struct DOOR));
mydoor->width=1000;
mydoor->length=2000;
pthread_exit((void*)mydoor);
}
int main()
{
void* status=NULL;
pthread_t tid;
pthread_create(&tid,NULL,thread1_run,(void*)"thread1");
pthread_join(tid,&status);
printf("thread 1 exit code = %d\n\n",(int)status);
pthread_create(&tid,NULL,thread2_run,(void*)"thread2");
pthread_join(tid,&status);
struct DOOR* mydoor=(struct DOOR*)status;
printf("thread 2 exit ,my door's width = %dmm,length=%dmm\n\n",mydoor->width,mydoor->length);
free(mydoor);
return 0;
}
向目标线程发送cancel信号,请求取消目标线程。
#include
int pthread_cancel(pthread_t thread);
使用 pthread_cancel 是比较复杂的,首先介绍一些前置概念。
在程序运行时间段内,期间程序被挂起(阻塞),是一个可以被取消的时间点。也就是说当线程出现阻塞的时候,这个阻塞的地方就是可被 pthread_cancel 取消的地方。
那我们可以得到结论:如果子线程的程序没有阻塞,那么主线程将无法pthread_cancel取消它:
void* thread_run(void* argv)
{
int i=0;
while(1)
{
i++;
}
}
int main()
{
void* status=NULL;
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread");
printf("thread(tid:%lu) begin\n",tid);
sleep(1);//防止运行速度过快,得先让子线程进入死循环
pthread_cancel(tid);
pthread_join(tid,&status);
printf("thread 3 exit code = %d\n",(int)status);
return 0;
}
运行后发现,子线程进入死循环,没有阻塞点无法被终止,主线程为了等待子线程的返回也阻塞住:
使程序出现阻塞点多为系统调用函数,如:printf,sleep,read,write等。
pthread线程库也为我们提供了阻塞点函数(pthread_testcancel()),以及如何让线程选择是否退出的方案,之后介绍。
现在我们知道,pthread_cancel调用并不等待线程终止,只提出撤销请求(发送cancel信号)。线程在撤销请求(pthread_cancel)发出后仍会继续运行,直到到达某个取消点(CancellationPoint)。
改变线程遇到cancel信号的状态
int pthread_setcancelstate(int state,int *oldstate)
state :
oldstate :输出型参数,备份线程原有的状态,如不关心设为NULL。
返回值 : 成功返回0,失败返回错误码。
实验
void* thread_run(void* argv)
{
//修改该线程状态为不可被取消
int oldstate;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,&oldstate);
printf("%s (tid: %lu) is running\n",(char*)argv,pthread_self());
int i=5;
while(i)
{
sleep(1);
i--;
}
return (void*)111;
}
int main()
{
void* status=NULL;
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread");
printf("thread(tid:%lu) begin\n",tid);
int ret=pthread_cancel(tid);
printf("%d\n",ret);
pthread_join(tid,&status);
printf("thread 3 exit code = %d\n",(int)status);
return 0;
}
pthread_cancel函数是成功调用的,但是线程不理会。
改变线程的终止类型,前提是cancelstate为enable
int pthread_setcanceltype(int type,int *oldtype)
type :
oldtype :输出型参数,备份线程原有的退出类型,如不关心设为NULL。
返回值 : 成功返回0,失败返回错误码。
实验
void* thread_run(void* argv)
{
//不理会取消信号
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
printf("%s (tid: %lu) is running\n",(char*)argv,pthread_self());
int i=10;
while(i)
{
sleep(1);
i--;
printf("%d\n",i);
if(i==5)
{
//5秒后理会信号
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
//立即取消
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);
}
}
return (void*)111;
}
int main()
{
//g_tid=pthread_self();
void* status=NULL;
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread");
printf("thread(tid:%lu) begin\n",tid);
sleep(1);//防止运行速度过快,得先让子线程进入死循环
pthread_cancel(tid);
pthread_join(tid,&status);
printf("thread 3 exit code = %d\n",(int)status);
return 0;
}
在不包含取消点,但又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求。
void pthread_testcancel(void);
线程cancelstate处于启用状态(ENABLE),且canceltype为延迟状态(DEFERRED)时,pthread_testcancel()函数有效。如果在cancel功能处于禁用状态下调用pthread_testcancel(),则该函数不起作用。
注意:我们可以用主线程取消子线程,当然也可以用子线程取消主线程,如下代码:
#include
#include
#include
#include
pthread_t g_tid;//全局变量记录主线程id
void* thread_run(void* argv)
{
while(1)
{
printf("i am %s (pid: %d,tid: %lu) is running\n",(char*)argv,getpid(),pthread_self());
sleep(3);
pthread_cancel(g_tid);//取消主线程
}
}
int main()
{
g_tid=pthread_self();//获取主线程id
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
sleep(50);
return 0;
}
我们再来查看进程状态:
其中的一条子线程仍在运行,我们将主线程取消了,但是发现此进程并没有退出,而是进入了僵尸态。所以这个子进程的资源将不能回收(没有主线程帮他join),导致系统资源浪费!
利用pthread_cancel可以让主线程退出,但是不能让进程退出。我们不建议使用子线程来 pthread_cancel 掉主线程。
一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取他的状态为止。
如果我们对子线程的退出状态不感兴趣,也不想使用pthread_join一直阻塞在那里等待子线程退出,那我们可以使用线程分离,分离之后的线程不需要被join,运行完毕之后,操作系统会立即回收它所占用的所有资源,而不保留终止状态。
#include
int pthread_detach(pthread_t thread);
参数
返回值 :成功返回0,失败返回错误号。
线程分离后,线程被置为detach状态,与主线程断开关系,这样的线程调用 pthread_join 将会返回 EINVAL(宏,值为22)错误。
分离线程常用于网络与多线程服务器。
#include
#include
#include
#include
#include
#include
pthread_t g_tid;
void* thread1_run(void* argv)
{
pthread_detach(pthread_self());
printf("%s (tid: %lu) is running\n",(char*)argv,pthread_self());
sleep(2);
return (void*)111;
}
int main()
{
//g_tid=pthread_self();
void* status=NULL;
pthread_t tid;
pthread_create(&tid,NULL,thread1_run,(void*)"thread1");
printf("create thread (tid:%lu)\n",tid);
sleep(3);//留时间给子线程先分离
int err=pthread_join(tid,&status);
if(err==0)
printf("err = %d ,status = %d\n",err,(int)status);
else
{
fprintf(stderr,"thread join err:%s\n",strerror(err));
printf("err = %d ,status = %d\n",err,(int)status);
}
return 0;
}
青山不改 绿水长流