一、线程概述线程是一种轻量级进程(LWP:lightweightprocess),在Linux环境下,线程的本质仍然是进程。计算机上运行的程序是一组指令和指令参数的组合,指令按照既定的逻辑控制计算机的运行。操作系统以进程为单位分配系统资源。可以理解为进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。先从概念上理解线程和进程的区别:1、一个进程有自己独立的地址空间,多个线程共享同一个地址空间。线程节省系统资源,效率不仅可以保持,而且在一个地址空间中的多个线程是独占的:每个线程都有自己的堆栈区,寄存器(在内核中管理)由多个线程共享地址空间:代码段、堆区、全局数据区、打开的文件(文件描述符表)由线程共享2、线程是程序的最小执行单位,进程是运行中的最小资源分配单位系统。每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片。可以在地址空间上划分多个线程,在有效资源的基础上可以抢到更多的CPU时间片。3、CPU调度与切换:线程上下文切换比进程多上下文切换快:进程/线程划分CPU时间片复用时,切换前会保存上一个任务的状态。下次切换回这个任务时,状态会加载并继续运行。从保存到重新加载任务的过程是上下文切换。4、线程更便宜,启动和退出速度更快,对系统资源的影响更小。在处理多任务程序时使用多线程比使用多进程更有优势,但是线程越多越好,如何控制线程数呢?文件IO操作:文件IO没有很高的CPU使用率,所以CPU时间片可以分时复用,线程数=2*CPU核数(效率最高)处理复杂算法(主要是CPU操作,压力大)大),线程数=CPU核心数(效率最高)2.创建线程2.1线程函数每个线程都有一个唯一的线程ID,ID类型为pthread_t,这个ID是一个无符号长整型。如果要获取当前线程的线程ID,可以调用如下函数:pthread_tpthread_self(void);//返回当前线程的线程ID。在一个进程中调用线程创建函数得到一个子线程。不像进程,需要给每一个创建的线程指定一个处理函数,否则线程无法工作。#includeintpthread_create(pthread_t*thread,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg);//Compileandlinkwith-pthread,线程库的名字是pthread,全称:libpthread.solibptread.a参数:thread:传出参数,无符号长整型。如果线程创建成功,则将线程ID写入this指针指向的内存中attr:线程的属性,一般使用default属性,写入NULLstart_routine:函数指针,创建的处理动作子线程,即函数在子线程中执行。arg:作为实参传递给start_routine指针指向的函数返回值:线程创建成功返回0,创建失败返回对应的错误号函数与指定的函数指针类型一致:void*(*start_routine)(void*)://pthread_create.c#include#include#include#include#include//子线程的处理代码void*working(void*arg){printf("我是子线程,线程ID:%ld\n",pthread_self());for(inti=0;i<9;++i){printf("child==i:=%d\n",i);}returnNULL;}intmain(){//1.创建子线程pthread_ttid;pthread_create(&tid,NULL,working,NULL);printf("子线程创建成功,线程ID:%ld\n",tid);//2.子线程不会执行下面的代码,主线程执行printf("I是主线程,线程ID:%ld\n",pthread_self());for(inti=0;i<3;++i){printf("i=%d\n",i);}//休息,休息一会儿...//sleep(1);return0;}编译测试程序,你会看到如下报错信息:$gccpthread_create.c/tmp/cctkubA6.o:Infunction`main':pthread_create.c:(.text+0x7f):undefinedreferenceto`pthread_create'collect2:error:ldreturned1exitstatus,报错原因是编译器无法链接线程库文件(动态库),编译时需要通过参数指定。动态库名称为libpthread.so要使用的参数是-l。按照规则,最终的形式应该写成:-lpthread(参数和参数值之间可以有一个空格)。正确的编译命令是:#pthread_create函数定义在某个库中,编译时需要添加库名pthread$gccpthread_create.c-lpthread$./a.out子线程创建成功,线程ID:139712560109312I是主线程,线程ID:139712568477440i=0i=1i=2为什么打印的日志输出中子线程线程处理函数还没有执行(只看到子线程的部分日志输出)?主线程一直在运行,在执行过程中创建了子线程,说明主线程有CPU时间片。代码在这个时间片内执行完后,主线程退出。子线程创建后,需要抢cpu时间片。如果它不能被抓住,它就不能运行。如果主线程退出,虚拟地址空间就会被释放,子线程也会一起销毁。但是如果有子线程退出,主线程还在运行,虚拟地址空间依然存在。得到的结论:在没有人为干预的情况下,虚拟地址空间的生命周期与主线程相同,与子线程无关。目前解决方案:让子线程执行完,然后主线程退出,可以在主线程中添加挂起函数sleep();3.线程退出在写多线程程序的时候,如果想让线程退出,但是又没有会导致释放虚拟地址空间(对于主线程),我们可以在线程中调用线程退出函数线程库,只要调用该函数,当前线程就会立即退出,不会影响其他线程的正常运行,无论是在子线程还是主线程中都可以使用。#includevoidpthread_exit(void*retval);参数:线程退出时携带的数据,当前子线程的主线程会获取该数据。如果您不需要使用它,请将其指定为NULL。以下是线程退出的示例代码。您可以在任何线程需要时调用此函数:#include#include#include#include#include#include//子线程处理代码void*working(void*arg){sleep(1);printf("我是子线程,线程ID:%ld\n",pthread_self());for(inti=0;i<9;++i){if(i==6){pthread_exit(NULL);//直接退出子线程}printf("child==i:=%d\n",i);}returnNULL;}intmain(){//1。创建子线程pthread_ttid;pthread_create(&tid,NULL,working,NULL);printf("子线程创建成功,线程ID:%ld\n",tid);//2.子线程不会执行下面的代码,主线程会执行printf("我是主线程,线程ID:%ld\n",pthread_self());for(inti=0;i<3;++i){printf("i=%d\n",i);}//主线程调用exit函数退出,地址空间不会释放pthread_exit(NULL);return0;}四4、线程回收4.1线程函数线程和进程一样。当一个子线程退出时,其内核资源主要由主线程回收。线程库中提供的线程回收函数叫做pthread_join()。这个函数是一个阻塞函数。如果有子线程运行时,调用该函数会阻塞,子线程退出函数解除阻塞,回收资源。该函数被调用一次,只能回收一个子线程。如果有多个子线程,需要循环回收。另外,通过线程回收函数,还可以获得子线程退出时传递过来的数据。函数原型如下:#include//这是一个阻塞函数,运行该函数时子线程会阻塞//子线程退出,函数解除阻塞,回收对应的子线程线程资源,类似于回收进程使用的函数wait()intpthread_join(pthread_tthread,void**retval);参数:thread:要回收的子线程的线程IDretval:二级指针,指向一级指针的地址,是一个传出参数,存放pthread_exit()传过来的数据,如果这个参数不是需要时,可以指定为NULL返回值:线程恢复成功返回0,恢复失败返回错误Number。4.2回收子线程数据当子线程退出时,可以使用pthread_exit()的参数来传递数据。在回收子线程时,可以使用phread_join()的第二个参数接收子线程传过来的数据。处理接收到的数据有多种方式,这里列举几种:4.2.1通过函数pthread_exit(void*retval)使用子线程栈;可以知道,当子线程退出时,需要将数据记录在一块内存中,通过参数是存放数据的内存地址,而不是具体的数据。因为参数是void*类型,所以这个通用指针可以指向任何类型的内存地址。先看第一种方式,将子线程退出数据保存在子线程自己的栈区://pthread_join.c#include#include#include#include#include//定义结构体structPersion{intid;charname[36];intage;};//子线程的处理代码void*working(void*arg){printf("我是子线程,线程ID:%ld\n",pthread_self());for(inti=0;i<9;++i){printf("child==i:=%d\n“,我);if(i==6){structPersionp;p.age=12;strcpy(p.name,"tom");p.id=100;//这个函数的参数把这个地址传给主线程的pthread_join()pthread_exit(&p);}}returnNULL;//代码在到达这个位置之前退出}intmain(){//1.创建子线程pthread_ttid;pthread_create(&tid,NULL,working,NULL);printf("子线程创建成功,线程ID:%ld\n",tid);//2.子线程不会执行下面的代码,主线程会执行printf("我是主线程,线程ID:%ld\n",pthread_self());for(inti=0;i<3;++i){printf("i=%d\n",i);}//阻塞等待子线程退出void*ptr=NULL;//ptr为传出参数,让this指针指向a函数内部有效内存//此内存地址为pthread_exit()参数指向的内存pthread_join(tid,&ptr);//打印信息structPersion*pp=(structPersion*)ptr;printf("子线程返回数据:姓名:%s,年龄:%d,身份证:%d\n",pp->name,pp->age,pp->id);printf("子线程资源回收成功...\n");return0;}编译执行测试程序:#编译代码$gccpthread_join.c-lpthread#执行程序$./a.out子线程创建成功,线程ID:140652794640128我是主线程,线程ID:140652803008256i=0i=1i=2我是子线程,线程ID:140652794640128child==i:=0child==i:=1child==i:=2child==i:=3child==i:=4child==i:=5child==i:=6子线程返回数据:name:,age:0,id:0子线程资源回收成功...从打印的日志可以发现,子线程返回的数据信息并没有在主线程中获取到,具体原因如下如下:如果多个线程共享同一个虚拟地址空间,则每个线程在栈区都有自己的内存,也就是说栈区被这些线程平分,当线程退出时,栈区的内存线程也被回收了4.2.2Threads使用位于同一个虚拟地址空间的全局变量,虽然不能共享栈数据,但是可以共享全局数据和堆数据,所以在子线程退出时,传出的数据可以存放在全局变量、静态变量或者堆中记忆。在以下示例中,数据存储在全局变量中:#include#include#include#include#include//定义结构体structPersion{intid;charname[36];intage;};structPersionp;//定义全局变量//子线程的处理代码void*working(void*arg){printf("我是子线程-thread,线程ID:%ld\n",pthread_self());for(inti=0;i<9;++i){printf("child==i:=%d\n",i);if(i==6){//使用全局变量p.age=12;strcpy(p.name,"tom");p.id=100;//这个函数的参数把这个地址传给main线程的pthread_join()pthread_exit(&p);}}returnNULL;}intmain(){//1.创建子线程pthread_ttid;pthread_create(&tid,NULL,working,NULL);printf("子线程创建成功,线程ID:%ld\n",tid);//2.子线程不会执行下面的代码,主线程会执行printf("我是主线程,线程ID:%ld\n",pthread_self());for(inti=0;i<3;++i){printf("i=%d\n",i);}//阻塞等待子线程退出void*ptr=NULL;//ptr是传出参数,让这个指针指向函数内部的一个block有效内存//这个内存地址就是pthread_exit()参数指向的内存pthread_join(tid,&ptr);//打印信息structPersion*pp=(structPersion*)指针;printf("姓名:%s,年龄:%d,身份证:%d\n",pp->姓名,pp->年龄,pp->id);printf("子线程资源回收成功...\n");return0;}4.2.3使用主线程栈虽然每个线程都有自己的栈区,但是位于同一个地址空间的多个线程可以互相访问对方栈空间上的数据很多时候,子线程的资源需要在主线程中回收,所以主线程通常最后退出。为此,在以下程序中,子线程返回的数据保存在主线程的栈内存中:#include#include#include#include#include//定义结构structPersion{intid;charname[36];intage;};//子线程的处理代码void*working(void*arg){structPersion*p=(structPersion*)arg;printf("我是子线程,线程ID:%ld\n",pthread_self());for(inti=0;i<9;++i){printf("child==i:=%d\n",i);if(i==6){//使用栈内存mainthreadp->age=12;strcpy(p->name,"tom");p->id=100;//这个函数的参数把这个地址传给pthread_join()pthread_exit(p);}}returnNULL;}intmain(){//1。创建子线程pthread_ttid;structPersionp;//将主线程的栈内存传递给子线程pthread_create(&tid,NULL,working,&p);printf("子线程创建成功,线程ID:%ld\n",tid);//2.子线程不会执行下面的代码,主线程会执行printf("我是主线程,线程ID:%ld\n",pthread_self());for(inti=0;i<3;++i){printf("i=%d\n",i);}//阻塞等待子线程退出void*ptr=NULL;//ptr是传出参数,在函数内部让这个指针指向一段有效的内存//这个内存地址就是pthread_exit()参数pthread_join(tid,&ptr)指向的内存;//打印信息printf("name:%s,age:%d,id:%d\n",p.name,p.age,p.id);printf("子线程资源回收成功。..\n");return0;}上面程序中,调用pthread_create()创建子线程,将主线程中栈空间变量p的地址传给子线程,写入数据在子线程中传输也就是说,在程序的main()函数中,可以通过指针变量ptr或者结构体变量p来读取子线程传输的数据。5、线程分离在某些情况下,程序中的主线程有自己的业务处理流程。如果主线程负责子线程的资源回收,只要子线程不退出主线程,调用pthread_join()就会一直阻塞,主线程的任务无法执行。线程库函数中为我们提供了线程分离函数pthread_detach()。调用该函数后,可以将指定的子线程从主线程中分离出来。当子线程退出时,其占用的内核资源将被系统中的其他进程接管并回收。线程分离后,在主线程中使用pthread_join()不会回收子线程资源。#include//参数是子线程的线程ID,主线程可以和这个子线程分开intpthread_detach(pthread_tthread);在下面的代码中,在主线程中创建一个子线程并调用线程分离函数,实现了主线程和子线程的分离:#include#include#include#include#include//子线程处理代码void*working(void*arg){printf("我是子线程,线程ID:%ld\n",pthread_self());for(inti=0;i<9;++i){printf("child==i:=%d\n",i);}returnNULL;}intmain(){//1.创建子线程pthread_ttid;pthread_create(&tid,NULL,working,NULL);printf("子线程创建成功,线程ID:%ld\n",tid);//2.子线程不会执行下面的代码,主线程执行printf("我是主线程,线程ID:%ld\n",pthread_self());for(inti=0;i<3;++i){printf("i=%d\n",i);}//设置子线程和主线程分离pthread_detach(tid);//让主线程可以自行退出pthread_exit(NULL);return0;}6.其他线程函数6.1线程取消线程取消是指在某种情况下杀死一个线程中的另一个线程。使用该函数杀死线程需要两步:在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,此时线程B不能死亡。在线程B中处理一个系统调用(从用户区切换到内核区),否则线程B可以一直运行下去。这其实和七步断肠散、半步癫笑一样。中毒不动不笑也没关系。#include//参数为子线程的线程IDintpthread_cancel(pthread_tthread);参数:要杀死的线程的线程ID返回值:函数调用成功返回0,调用失败返回非零错误号。下面的示例代码中,主线程调用了线程取消函数,只要在子线程中进行系统调用,当子线程执行到这个位置时,就会挂掉。#include#include#include#include#include//子线程的处理代码void*working(void*arg){intj=0;for(inti=0;i<9;++i){j++;}//这个函数会调用系统函数,所以这是一个间接的系统调用printf("我是子线程,线程ID:%ld\n",pthread_self());for(inti=0;i<9;++i){printf("childi:%d\n",i);}returnNULL;}intmain(){//1.创建子线程pthread_ttid;pthread_create(&tid,NULL,working,NULL);printf("子线程创建成功,线程ID:%ld\n",tid);//2.子线程不会执行下面的代码,主线程执行printf("我是主线程,线程ID:%ld\n",pthread_self());for(inti=0;i<3;++i){printf("i=%d\n",i);}//杀掉子线程,如果在子线程中进行系统调用,子线程threadwillendpthread_cancel(tid);//让主线程自己退出pthread_exit(NULL);return0;}关于调用系统有两种方式:直接调用linux系统函数和调用标准C库函数。为了实现一些功能,标准C库函数会在Linux平台下调用相关的系统函数。6.2线程ID比较在Linux中,线程ID本质上是一个Unsignedlonginteger,所以可以直接使用比较运算符来比较两个线程的ID,但是线程库是可以跨平台使用的。在某些平台上,pthread_t可能不是一个简单的整数。这种情况下,比较两个线程的ID就必须使用比较函数,函数原型如下:#includeintpthread_equal(pthread_tt1,pthread_tt2);参数:t1和t2为待比较线程的线程ID返回值:两个线程ID相等则返回非零,不相等则返回0