1.多线程的理解
在操作系统原理的术语中,线程是进程的一条执行路径。线程在Unix系统下,一般被称为轻量级的进程
,线程其实不是进程,但却可以看作是Unix进程的堂弟,所有的线程都是在同一进程空间
运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和讯号处理等等。但同一进程中的多个线程有各自的调用栈(call
stack),自己的寄存器环境(registercontext),自己的线程本地储存(thread-localstorage)。
一个进程可以有好多线程,每条线程并行执行不同的任务。
线程可以提升应用程序在多核环境下处理例如文件I/O或则socket
I/O等会形成堵塞的情况的表现性能。在Unix系统中,一个进程包含好多东西,包括可执行程序以及一大堆的例如文件描述符地址空间等资源。在好多情况下,完成相关任务的
不同代码间须要交换数据。倘若采用多进程的方法,进程的创建所花的时间片
要比线程大些,另外进程间的通讯比较麻烦,须要在用户空间和内核空间进行频繁的切换,开支很大。而且假如使用多线程的方法,由于可以使用共享的全局变量,所以线程间的通讯(数据交换)显得十分高效。
多进程和多线程的区别
多线程:主线程是你爸爸的卧室,子线程是你的卧室,大家要吃月饼,都只要在书房上面拿就可以。
多进程:你妈妈的卧室在一层楼,你的卧室在另外一层楼,两个卧室的通信用IPC合同,我们用的socket就是这些合同。
(所以多线程比多进程便捷,由于大家在同一个房屋上面。)
2.创建线程
一个进程创建后,会首先生成一个缺省的线程,一般称这个线程为主线程(或称控制线程),C/C++程序中,主线程就是通过main函数步入的线程,由主线程调用
pthread_create()创建的线程称为子线程,子线程也可以有自己的入口函数,该函数由用户在创建的时侯指定。每位线程都有自己的线程ID,可以通过
pthread_self()
函数获取。最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,互相之间就是对等关系,不存在蕴涵的层次关系。每位进程可创建的最大线程数由具体实现决定。
无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行就会中止
。这时整个进程结束或僵死,部份线程保持一种中止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他特别形式中止,线程步入中止态,并且为线程分配的系统资源不一定释放,可能在系统重启之前,始终都不能释放,中止态的线程,仍然作为一个线程实体存在于操作系统中,哪些时侯销毁,取决于线程属性。在这些情况下马哥linux,主线程和子线程一般定义以下两种关系:
*可会合
(joinable):这些关系下,主线程须要明晰执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作以后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,虽然子线程才能在主线程之前执行完毕,步入中止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。
*相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这些情况下,子线程一旦步入中止状态,这些方法常用在线程数较多的情况下
,有时让主线程挨个等待子线程结束,或则让主线程安排每位子线程结束的等待次序,是很困难或不可能的,所以在并发子线程较多的情况下,这些方法也会时常使用。
线程的分离状态决定一个线程以哪些样的形式来中止自己,在默认的情况下,线程是非分离状态的,这些情况下,原有的线程等待创建的线程结束,只有当
pthread_join
函数返回时,创建的线程才算中止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就中止了,马上释放系统资源。
3.pthread_create()函数
36行和39行调用pthread_create()函数拿来创建了两个子线程,该函数的原型是:
#include
intpthread_create(pthread_t*thread,const
pthread_attr_t*attr,void*(*start_routine)(void*),void*arg);Compileand
linkwith-pthrea
说明:pthreand_create()拿来创建一个线程,并执行第三个参数start_routine所指向的函数。
*第三个参数start_routine是一个函数表针,它指向的函数原型是voidfunc(void),这是所创建的子线程要执行的任务(函数);
*第四个参数arg就是传给了所调用的函数的参数,倘若有多个参数须要传递给子线程则须要封装到一个结构体里传进去;
*第一个参数thread
是一个pthread_t类型的表针,他拿来返回该线程的线程ID。每位线程都还能通过**pthread_self()**来获取自己的线程ID(pthread_t类型)
*第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:
须要设定的是线程的分离状态,假如有必要也须要更改每位线程的栈大小。每位线程创建后默认是joinable状态,该状态须要主线程调用pthread_join
等待它退出linux运维面试题,否则子线程在结束时,显存资源不能得到释放导致显存泄露。所以我们创建线程时通常会将线程设置为分离状态,具体有两种方式:
*线程上面调用pthread_detach(pthread_self())这个方式最简单
*在创建线程的属性设置里设置PTHREADCREATEDETACHED
代码15行我们定义了创建线程的属性变量thread_attr,在对该属性进行设置前,我们须要先调用pthread_attr_init函数初
始化它(第18行),在第24行我们设置线程的栈大小为120K,同时在第30行设置线程的属性为分离状态。第36行创建线程时使用了该属性创建线程,这时创建的子进程就是分离状态了。线程属性在使用完以后,我们应当调用
pthread_attr_destroy(第42行)把他捣毁释放。
而代码39行创建子线程时并没有使用该线程,同时在thread_worker2()里并没有调用pthread_detach()设置线程为分离状
态。这时就须要主线程在45行处调用pthread_join()等待第二个子线程退出。其实主线程也就阻塞在这儿不会往下继续执行
了。
在创建两个线程时,我们都通过第四个参数将主线程栈中的shared_var变量地址传给了子线程linux 线程,由于所有线程都是在同一进
程空间中运行,而只是子线程有自己独立的栈空间,所以这时所有子线程都可以访问主线程空间的shared_var变量。
如今我们编译运行一下程序瞧瞧疗效,注意对于多线程编程在编译时,一定要加上-lpthread选项告诉链接器在链接的时侯要连
接pthread库:
gccthread.c-othread-lpthread./thread
主线程创建子线程后到底是子线程还是主线程先执行,或到底那个子线程先运行系统并没有规定,这个依赖操作系统的进程
调度策略。其实由于代码45行处主线程调用了pthread_join
会造成主线程阻塞,所以主线程不会往下继续执行while(1)循环。我们再来深入剖析各个线程的代码:
我们在创建子线程以后,在子线程的执行函数里通常还会用while(1)的死循环来让子线程仍然运行,否则子线程将按代码次序
执行,执行完毕就线程退出了。同样的,我们主线程也应当要用一个while(1)循环仍然运行,否则主线程退出会造成进程退出,而进程退出会造成所有子线程退出了。
接出来我们在剖析一下子线程所做的事,在两个子线程中做的任务是首先复印一下当前shared_var变量的值之后让它自加linux 线程,
然后睡眠2s后再复印一下这两个值,代码如下:
从前面的运行结果白色标明的部份我们可以看见,thread_worker1在创建后首先开始运行,在开始自加之前值为初始值
1000,之后让该值自加后休眠2秒后再复印该值发觉不是1001而是1002了。这是因为shared_var这个变量会被两个子线程同时访问
更改引起。假如一个资源会被不同的线程访问更改,这么我们把这个资源称作临界资源,这么对于该资源访问更改相关的代码就称作临界区。这么如何解决多个线程之间共享同一个共享资源,是多线程编程须要考虑的一个问题。
4.互斥锁
试想一下,我们教室/实验室只有一个洗手间,那多个人是如何解决便器共享的问题?对,那就是锁
的机制!在这儿便器就是临界资源,我们在步入到洗手间(临界区)后,就首先上锁;
之后用完离开洗手间(临界区)以后,把锁释放供他人使用。假如有人想去洗手间时发觉锁具上了,他也有两种策略:1,在洗手间那儿等(阻塞);
2,暂时先离开等会再过来看(非阻塞);
互斥锁锁的是一个临界区,在临界区开头(加锁)lock,在临界区结尾(关锁)unlock。
死锁
假如多个线程要调用多个对象,则在上锁的时侯可能会出现“死锁”。举个反例:A、B两个线程会同时使用到两个共享变量m和n,同时每位变量都有自己相应的锁M和N。
这时A线程首先领到M锁访问m,接出来他须要拿N锁来访问变量n;而假如此时B线程拿着N锁等待着M锁的话,就导致了线程“死锁”。
死锁形成的4个必要条件:
*互斥:某种资源一次只容许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直至该进程访问结束。
*占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
*不可占领:他人早已占有了某项资源,你不能由于自己也须要该资源,就去把他人的资源抢过来。
*循环等待:存在一个进程链,致使每位进程都占有下一个进程所需的起码一种资源。当
以上四个条件均满足,必然会导致死锁,发生死锁的进程难以进行下去,它们所持有的资源也未能释放。这样会引起CPU
的吞吐量增长。所以死锁情况是会浪费系统资源和影响计算机的使用性能
的。这么,解决死锁问题就是相当有必要的了。形成死锁须要四个条件,这么,只要这四个条件中起码有一个条件得不到满足,就不可能发生死锁了。因为
互斥条件是非共享资源所必须的,除了不能改变,还应加以保证,所以,主要是破坏形成死锁的其他三个条件。
a、破坏“占有且等待”条件
方式1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所须要的全部资源。
优点:简单易施行且安全。
缺点:由于某项资源不满足,进程未能启动,而其他早已满足了的资源也不会得到借助,严重减少了资源的借助率,导致
资源浪费。使进程常常发生饥饿现象。
方式2
:该方式是对第一种方式的改进,容许进程只获得运行早期须要的资源,便开始运行,在运行过程中逐渐释放掉分配到的早已使用完毕的资源,之后再去恳求新的资源。这样的话,资源的借助率会得到提升,也会降低进程的饥饿问题。
b、破坏“不可占领”条件
当一个早已持有了一些资源的进程在提出新的资源恳求没有得到满足时,它必须释放早已保持的所有资源
,待之后须要使用的时侯再重新申请。这就意味着进程已占有的资源会被短暂地释放或则说是被占领了。该种方式实现上去比较复杂,且代价也比较大。释放早已保持的资源很有可能会造成进程之前的工作实效等,反复的申请和释放资源会造成进程的执行被无限的延后,这除了会延长进程的周转周期,都会影响系统的吞吐量。
c、破坏“循环等待”条件
可以通过定义资源类型的线性次序来防治,可将每位资源编号,当一个进程占有编号为i的资源时,这么它下一次申请资源只
能申请编号小于i的资源。
5.多线程改写服务器程序
多线程编程模型和多进程编程模型的工作方法完全一致,在了解Linux下多进程编程以后,我们就可以使用多进程编程模型改写
服务器的多线程实现,其流程图和程序代码如下:
在多进程基础上写多线程