莱州环球网站建设,苏州搜索引擎排名优化商家,中交建设集团有限公司,wordpress文章 公众号进程在前面已经讲过了#xff0c;所以这次我们来讨论一下多线程。前言#xff1a;线程的背景进程是Linux中资源及事物管理的基本单位#xff0c;是系统进行资源分配和调度的一个独立单位。但是实现进程间通信需要借助操作系统中专门的通信机制#xff0c;但是只这些机制将占…进程在前面已经讲过了所以这次我们来讨论一下多线程。前言线程的背景进程是Linux中资源及事物管理的基本单位是系统进行资源分配和调度的一个独立单位。但是实现进程间通信需要借助操作系统中专门的通信机制但是只这些机制将占用大量的空间资源特别是少量数据传递时显得庞大而欠灵活。因此才出现了线程。线程和进程一样具有创建退出取消等待等基本操作可以独立完成特定事物的处理。并且线程占用的资源更少。 1.线程的基本概念1.1什么是线程在一个程序里的一个执行路线叫做线程。一切进程至少有一个执行路线。1.2线程与进程的对比1.21用户空间资源对比一个进程的创建包含着进程控制块task_struct,进程地址空间mm_struct,以及页表的创建。线程是当前进程内的一个执行流并且在进程地址空间里运行这个进程申请的所有资源都被线程共享。但是如果用fork函数创建一个子进程而线程用ptread_creat()函数创建一个新线程一起对比一下所占资源情况总结每个进程在创建是额外申请了新的内存和空间以及存储代码数据段堆栈空间。并且初始化为父进程的值父子进程在创建后不能互访对方资源。每个创建的新线程在用户阶段仅申请自己的栈空间并且与同进程的其他线程共享其他地址空间这使得同进程下的个线程共享数据很方便。但是带来的问题也是同步的接下来我们会讲到。1.22内核空间资源对比1.前面的进程中我们说了每一个进程在内核中都有自己的进程控制块PCB来识别当前进程所能够访问的系统资源。通过该进程的PCB可以访问到进程的所有资源。目前在Linux下的进程统称为轻量级进程甚至很多的书中都说LINUX并不区分进程与线程如果我们站在内核的角度想在创建线程时Linux内核仍然创建一个新的PCB来表示这个线程而内核对他俩的认识都来自PCB所以内核并不认为他俩有区别。2.在Linux下每一个进程的PCB的mm_struct都用来描述地址空间。而父子进程间的地址空间是分开的而同一个进程下的线程共享这个地址空间所以才在用户的角度说两者是有区别的。但是从调度的角度来看操作系统是基于线程调度的及内核并不认为他俩有区别。3.一个进程如果不创建新的线程那他就是只有一个线程的进程如果创建了额外的线程原来的进程也称为主线程。总结一下4.进程在使用时占用了大量的内存特别是进程间通信这使得进程不够灵活而且耗费资源而线程占用资源少使用灵活且同进程下的线程间数据交互不需要经过OS所以很多应用程序都使用了大量的进程但是线程不能脱离进程而存在。线程引进能做啥事如果说进程中有10个函数但是只有一个线程那这10个函数一定是按照顺序一次进行但是如果有多个线程被创建那这几个线程就可以分别实现这几个函数。单线程执行流被调度多个执行流被调度linxu下没有真正的多线程1.操作系统中存在大量的进程一个进程内又存在一个或多个线程因此线程的数量一定比进程的数量多当线程的数量足够多的时候很明显线程的执行粒度要比进程更细。2.如果一款操作系统要支持真的线程那么就需要对这些线程进行管理。比如说创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等这个和进程的绝大多数功能重复所以委员会决定在内核层面没必要去区分他俩都是用test_struct去表示。3.因此如果要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来描述线程的控制块和描述进程的控制块是类似的因此Linux并没有重新为线程设计数据结构而是直接复用了进程控制块所以我们说Linux中的所有执行流都叫做轻量级进程。既然linxu中没有真正意义上的线程所以就没有真正意义上的线程调用了。但是Linux可以提供创建轻量级进程的接口也就是创建进程共享空间其中最典型的代表就是vfork函数。提到vfork函数就不得不提到他的老大哥fork函数这个vfork()函数其实就是fork的阉割版fork创建的子进程是完全11模仿的父进程但是操作者创建的进程如果是只想实现一些小功能函数就不需要完全复制父进程。fork():父进程的一个副本代码数据vfork():共享父进程的代码与数据vfork()原型pid_t vfork(void);vfork函数的返回值与fork函数的返回值相同给父进程返回子进程的PID。给子进程返回0。例如在下面的代码中父进程使用vfork函数创建子进程子进程将全局变量g_val由100改为了200父进程休眠3秒后再读取到全局变量g_val的值。可以看到父进程读取到g_val的值是子进程修改后的值也就证明了vfork创建的子进程与其父进程是共享地址空间的。1.3从新理解啥是进程在用户视角内核数据结构该进程对应的代码和数据内核视角承担系统资源分配的基本实体刚开始创建一个进程时该进程就会向操作系统要资源内核区栈堆...,他是以进程的身份向系统要资源。当资源要完了又创建新的线程时线程就不在伸手向系统要了而是向你这个进程要了。换句话说创建新进程时向操作系统要资源的不是线程而是以进程为单位要的。既然是进程要的那系统分配资源时不就是以进程为基本单位进行分配的吗怎样看我们写的代码呢原来我们写的代码都是一个task_struct,但是现在有多个task_struct如果说以前的叫做单进程多线程的话那现在的就叫做多进程多线程了也就是说原来的是现在多个task_struct的一个子集。也就是说一个进程就是一个地址空间而一个线程就是一个task_struct。CPU视角其实CPU不关心到底系统是咋区分进程和线程的他只看task_struct只要运行队列中有task_struct就直接执行其中的代码和数据。CPU不关心到底代码和数据是和谁共享的只要能执行就行。1.4进程和线程之间的关系线程的创建2.1线程创建函数一般我们都是用pthread_create()来创建一个线程的man pthread_create //查看其函数声明第一个参数线程ID没错进程有进程ID线程有线程ID第二个参数设置线程属性没毛病知道我们学完线程这个默认也没关系不用改。第三个参数设置线程运行的代码起始地址是个函数指针。因为线程是进程一部分所以用函数指针来接受进程的地址第四个参数运行函数的参数地址。2.2创建线程先创建一个Makefile文件mythread:mythread.ccg -o $ $^ -lpthread
.PHONY:clean
clean:rm -f mythread那个lpthread是引入线程库否则要是新创建一个线程的话就会报错然后在创建一个thread.cc来存放代码#include iostream
#include unistd.h //这个getpid函数头文件
#include pthread.h
#include cstdio
#include string.husing namespace std; // 这是C中的命名空间void *threadRun(void *args)
{const string name (char *)args;while (true){cout name , pid: getpid() \n endl;sleep(1);}
}int main()
{pthread_t tid[5]; // 一次创建5个线程char name[64]; // 这是线程名但是可能有点小问题for (int i 0; i 5; i){snprintf(name, sizeof name, %s-%d, thread, i); // 给线程重新命名pthread_create(tid i, NULL, threadRun, (void *)name);sleep(1);}while (true){cout main thread, pid: getpid() endl;sleep(3); // 避免和上面的混淆}
}ldd mythread确认一下有没有用到这里的库运行一下mythread---./mythread接下来我们创建一个分屏然后用ps axj查看命令来查看ps axj | head -1 ps axj |grep mythread但是这里却只有一个进程不是说系统可以创建轻量级进程吗?在哪呢输入以下命令ps -aL
ps -aL | head -1 ps -aL |grep mythread看见PID后面那个LWP没那个就是轻量级进程对应的PID19,20,26...而第一个线程的PID和LWP是一样的所以他叫做主线程。所以操作系统调度线程时看的是LWP因为PID和线程是一对多的关系。我们输入一下kill -9 PID当我们结束一个进程那他其中的所有线程就都结束了。代码区的共享我再新写一个函数void show(const string name)
{cout name , pid: getpid() \n endl;
}void *threadRun(void *args)
{const string name (char *)args;while (true){show(name); sleep(1);}
}int main()
{pthread_t tid[5]; // 一次创建5个线程char name[64]; // 这是线程名但是可能有点小问题for (int i 0; i 5; i){snprintf(name, sizeof name, %s-%d, thread, i); // 给线程重新命名pthread_create(tid i, NULL, threadRun, (void *)name);sleep(1);}while (true){cout main thread, pid: getpid() endl;sleep(3); // 避免和上面的混淆}
}这个函数能被所有的线程进行访问再重新make一下再编译也能跑。当然全局变量也可以被共享2.3线程的缺点说了这么多我们就来聊一聊写线程时遇到的缺点1.性能损失一个很少被外部事件阻塞的计算机密集型线程往往无法与其他线程共享同一个处理器如果密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的损失指的是增加了额外的同步和调度开销而可以资源不变。2.健壮性降低编写多线程需要更全面更深入的考虑 在一个多线程程序中因时间分配上的细微差别或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程间是缺乏保护的。3.缺乏访问控制进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。4.编程难度提高编写与调试一个多线程程序比单线程困难的多。2.4线程异常这个后面会做示范这里就提一下单个线程如果出现除零野指针问题导致线程崩溃进程也会随之崩溃。线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程1进程终止该进程中的所有线程也就随即退出。这里我们再重新创建一个新线程跑一跑试试makefile文件和上面一样为了方便继续用mythread.cc#include iostream
#include unistd.h //这个getpid函数头文件
#include pthread.h
#include cstdio
#include string.husing namespace std;void *threadRoutine(void *args)
{ while(true){cout 新线程 (char *)args running... endl;sleep(1);}
}
int main()
{pthread_t tid; // 创建线程pthread_create(tid, NULL, threadRoutine, (void *)thread 1);while (true){cout main线程 running ... endl;sleep(1);}return 0;
}这两个新线程都可以跑上面不是说如果一个线程崩溃其他线程也不能跑了我们试一下吧。我们把代码改一下这里的\0代表 硬件异常CPU中的状态标记为被置为0,这里我们来模拟线程出错。重新make一下报错不管然后编译。出现这个错误然后在输入kill -l又出现这个信号这里就不存在我们创建的mythread线程了。所以线程谁先运行与调度器有关。如果一个线程出现异常都可能导致整个进程退出。线程在创建和执行的时候也是需要等待的。如果主线程不等待就会导致3.线程等待3.1线程等待函数pthread_join()man pthread_jointhread被等待线程的ID。retval线程退出时的退出码信息。void *threadRoutine(void *args)
{int i 0;while (true){cout 新线程 (char *)args running... endl;sleep(1);if (i 10){break;}}cout new thread quit... endl;
}
int main()
{pthread_t tid; // 创建线程pthread_create(tid, NULL, threadRoutine, (void *)thread 1);pthread_join(tid, NULL); //进程等待函数cout main thread wait down .... endl;while (true){cout main线程 running ... endl;sleep(1);}return 0;
}如果发生进程等待就是先打印新线程后打印main线程一点毛病没有。总结一下如果thread线程通过return返回retval所指向的单元里存放的是thread线程函数的返回值。如果thread线程被别的线程调用pthread_cancel异常终止掉retval所指向的单元里存放的是常数PTHREAD_CANCELED。如果thread线程是自己调用pthread_exit终止的retval所指向的单元存放的是传给pthread_exit的参数。如果对thread线程的终止状态不感兴趣可以传NULL给retval参数。3.2pthread_join第二个参数我们写的功能函数的返回值是void*, 这个返回类型可以自己设置。就比如我们让threadRoutine函数返回10但是必须要强制类型转换成void*, 说白了就是把10当做一个指针数据也就是说有一个地址这个地址是10。返回值但是返回给谁呢当然是返回给主线程了主线程创建分线程就是办事的所以必须要知道事办的咋样。但是主线程咋接收所以此时就用到pthread_join()的第二个参数了。指针就是一个地址地址说白了就是一个数据我们就把他看做自变常量。void* 10看做一个数据。下面的ret看做空间。取指针的地址所以第二个参数就是二级指针。void *threadRoutine(void *args)
{int i 0;while (true){cout 新线程 (char *)args running... endl;sleep(1);if (i 3){break;}}cout new thread quit... endl;return (void*) 10;
}
int main()
{pthread_t tid; // 创建线程pthread_create(tid, NULL, threadRoutine, (void *)thread 1);void* ret NULL;pthread_join(tid, ret); // 进程等待函数,默认阻塞等待新线程退出cout main thread wait down .... new thread init (long long)ret\n endl;return 0;
}新线程退出以后主线程就活动新线程的返回值了。我们在来个新玩法void *threadRoutine(void *args)
{int i 0;int *date new int[10];while (true){cout 新线程 (char *)args running... endl;sleep(1);date[i]i;if (i 10){break;}}cout new thread quit... endl;// return (void*) 10;return (void*)date;
}
int main()
{pthread_t tid; // 创建线程pthread_create(tid, NULL, threadRoutine, (void *)thread 1);int *ret NULL;pthread_join(tid, (void**)ret); // 进程等待函数,默认阻塞等待新线程退出cout main thread wait down .... new thread init\n endl;for(int i0;i10;i){coutret[i]endl;}return 0;
}这就显出来了4.线程终止进程可以终止线程可以终止吗他自动停止了。在线程中绝对不能调用exit,他是终止线程的。4.1pthread_exit()线程有他自己的结束函数pthread_exit()当然return也可以线程退出但是这里就不过多的介绍了。4.2pthread_cancel线程取消函数也可以退出线程man pthread_cancel
//q键退出取消那个线程就把它的id传入就行了。主线程可以取消新线程取消成功的线程的退出码一般是-1我们让新线程进入死循环并且把新线程id传给取消函数。using namespace std;void *threadRoutine(void *args)
{int i 0;int *date new int[10];while (true){cout 新线程 (char *)args running... endl;sleep(1);date[i]i;if (i 5){break;}}cout new thread quit... endl;
}
int main()
{pthread_t tid; // 创建线程pthread_create(tid, NULL, threadRoutine, (void *)thread 1);int count0;while(true){coutmain()线程running...endl;sleep(1);count;if(count5)break;}pthread_cancel(tid);coutpthread_cancel:tidendl;int *ret NULL;pthread_join(tid, (void**)ret); // 进程等待函数,默认阻塞等待新线程退出cout main thread wait down .... new thread init(long long)ret\n endl;sleep(3); //让主线程多活几秒
return 0;
}接下来执行命令来监视一波while :; do ps -aL | head -1 ps -aL | grep mythread ; sleep 1 ; done 既然主线程可以取消新线程那反过来其实也可以但是有啥实际意义呢主线程取消谁来帮忙管理其他新线程呢而且内容比较凌乱这里就不过多介绍了。4.3线程id刚才我们看到了一大串的数字他们就被称作线程id但是有些老铁认为这个线程id和线程的lwp其实是一个东西但是他俩真的一样吗其实他俩没啥关系。id后面那一串数字是它的64位。而且id代表的是一个地址所以它才是这一串数字。其实当我们调用pthread函数来实现一些对线程的操作时我们调用的是pthread库而不是调用linux系统。当使用pthread库时这个库就被加载到内存上面而线程通过一系列操作最终被页表映射到内存上面。所以id其实就代表线程被映射到内存上的地址。另外主线程被系统存在cpu的栈里而新线程则被存放在共享区新开辟的栈里。当时单执行流就调用cpu里主线程栈来操作。获取自身线程id的函数pthread_self5.线程分离默认情况下新创建的线程时joinable的线程退出后pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值join是一种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。分离函数pthread_detachint pthread_detach(pthread_t thread);这里就不在细讲了各位有兴趣去搜一下资料吧饿死了