以下文章来源于嵌入式情报局 ,作者情报小哥
什么是线程
线程的诞生
线程和进程是许多初学者比较难理解的概念,在Linux中线程和进程对于内核是一致的,Linux内核是不区分进程和线程,其均有独立的进程控制块(PCB),只在用户层面上进行区分。
由于进程是独立的,且都具有其自身的页目录、页表进而物理页面等,所以他们具有一致的虚拟地址空间,应用程序访问相同的地址,其由于映射关系不同而相互之间不会影响。
而Linux线程与进程是类似的都有各自独立的PCB,但是他们访问着相同的页目录、页表和物理页面等等,所以多个线程之间共享着同一个进程的地址空间,那么地址内容的修改会相互之间影响。
如果你再抽象的理解可以认为线程是多个进程共享一块地址空间而模拟得到的。
线程与进程
两个硬核概念 :
线程是执行和被调度的最小单元。
进程是资源分配的最小单元。
简单理解一下上面两句 : 线程是执行和调度的最小单元,如果把没有多线程的进程看成:一个线程 + 资源,那么线程就可以认为是代码的执行,也是被OS调度的对象,而线程不管资源,所以进程就是分配资源的最小单位。
进程VS线程 :
多线程和多进程都是为了实现任务的并发执行而实现的,从前面的内容看线程就是属于一种轻量级进程。
进程之间是相互独立的,比如我们使用fork其子进程与父进程代码段是共享,不过数据段、堆栈段等非共享,不可相互访问。
对于进程之间数据独立,所以需要通过管道、消息队列等等机制来完成信息交换,这样相对比较浪费时间,且操作麻烦,而线程之间是共享的,所以线程之间的数据交互并不需要像进程之间那么复杂就可以完成交互,比如直接访问变量。
同时在进程切换的过程中需要维护自己的页表项,即重新映射虚拟地址与物理地址等,而线程之间的切换则不需要更新页表项等,开销就大大降低,同时仿佛的进程切换对于CPU缓存也是不友好的,影响程序执行效率。
线程的缺点
当然线程也并没有那么优秀,由于各进程之间的独立性,即使有进程挂了不相关的其他进程也能够继续执行。而多线程之间数据共享,导致线程之间的耦合度增加,这样对于各个线程任务的安全带来了一些挑战,使用同步互斥机制能够很好的在安全和效率上做一个权衡,从而发挥线程并发的最大功效。由于线程的灵活度较大,这也对于进程线程开发的程序员提高了其对任务的把控能力,如果稍有不慎一个线程的挂机就会导致整个进程崩溃!2线程创建
线程库
上一小节小哥跟大家介绍了线程以及线程与进程相关区别的理解,今天主要是体验一下线程相关的操作和效果。
在Linux中一般分为用户级线程和系统级线程,其中用户级主要是一些线程库,管理过程均由用户程序来完成,而系统级线程由操作系统内核管理。其中我们比较常用的是POSIX线程库,一般也叫pthread,该库已经把相应的操作封装好了,我们只需要理解好了线程的原理,然后熟悉该库的API便可以好好使用线程。
线程库创建
首先我们来看一下线程创建API :
参数说明:
thread : 用于返回线程标识符,传递一个pthread变量即可。
attr : 用于设置线程的属性,通常设置为空attr为NULL。
start_routine : 线程启动后要执行函数的起始地址,这个参数我觉得有部分朋友看不太懂,看这段代码颜色你应该可以理解void *(*start_routine)(void *)。
arg : 传给start_routine的参数。
返回值 : 成功返回0,否则失败返回对应的错误码。
3线程创建运行实例
编译及运行结果
解读一下
1 ) 首先在进行编译的时候需要链接-pthread。
2 ) 本例程中由主线程创建了两个子线程thread1和thread2,然后看到输出结果中三个线程都运行起来了。
3 ) 使用了一个变量传递给三个线程进行共享, 从而我们也可以看到各个线程中的累计均会影响到其他线程的显示。
4线程退出
线程正常退出方式
上一小节小哥跟大家一起学习线程的创建等知识,新的线程通过调用start_routine()开始执行,arg作为start_routine()的唯一参数传递,新线程一旦执行,一般会通过如下四种方式进行终止 :
1)调用pthread_exit(3),指定一个退出状态值,该值对同一个进程中调用pthread_join(3)的另一个线程可用。
2)从start_routine()返回,这相当于使用return语句中提供的值调用pthread_exit(3)。
3)已被取消(参见pthread_cancel(3))。
4)进程中的任何线程调用exit(3),或者主线程执行main()的返回。这将导致进程中所有线程的终止。
异常退出
以上提到了线程正常的退出方式,这种是可以预见的,比较常用的pthread_exit和直接线程内部return,既然有正常退出,就存在非正常退出,比如自身运行错误,由于程序运行异常、越界、段错误、访问非法地址,运行非法指令等等,从而不可预知的退出方式。
所以对于正常可预知退出,一个正常的程序都会考虑其退出后资源的释放问题,比如malloc以后需要free等,当然也有一些程序写得不咋样,正常退出没有释放的资源,导致内存泄漏问题;但对于异常退出如果保证资源的释放问题就更为重要了。
5线程的清除
取消清理处理程序
pthread_cleanup_push() : 将例程推送到清理处理程序栈的顶部。当以后调用例程时,将把arg作为它的参数。
pthread_cleanup_pop():函数删除清理处理程序栈顶部的例程。如果execute参数为非零,则可选择性地执行它。
也就是说从push到pop之间的程序段中,一旦发生终止动作,比如pthread_exit()或者是异常终止等,注意不包括线程中return方式的退出,最后均会执行push参数中指定的routine清理函数,有点类似于C++中的析构函数。
"取消清理处理程序"从堆栈弹出有如下三种情况:
1、当一个线程被取消时,所有堆叠的清理处理程序都将弹出并以与它们被推入堆栈的顺序相反的顺序执行。
2、当一个线程通过调用pthread_exit(3)终止时,所有的清理处理程序都按照前面描述的那样执行。(如果线程通过执行线程start函数的return来终止,那么清理处理程序不会被调用。)
3、当一个线程调用pthread_cleanup_pop()时,使用一个非零的execute参数,弹出并执行最顶部的清理处理程序。
最后值得注意的是 : 调用者必须确保对这些函数的调用在同一个函数中成对,并且在相同的词法嵌套级别上成对。(换句话说,清理处理程序只在执行指定的代码段期间建立。)
演示例程
情形1 : 正常情况,pop中execute为0,且在push和pop之间没有退出,则无影响。
pthread_join解读
函数的作用是 : 等待线程指定的线程终止。如果线程已经终止,那么pthread_join()立即返回。线程指定的线程必须是joinable。
如果retval不是NULL,那么pthread_join()将目标线程的退出状态(也就是目标线程提供给pthread_exit(3)的值)复制到由*retval指向的位置。如果目标线程被取消,那么PTHREAD_CANCELED被放在*retval中。
如果多个线程同时尝试连接同一个线程,则结果是未定义的。如果调用pthread_join()的线程被取消了,那么目标线程仍然是joinable而不是detached。
join的作用
从上面的介绍看Linux线程中存在两种资源回收状态,一种是joinable,另外一种是detached,其中:
joinable(默认) :当线程退出的时候均不会释放其占用的资源等等,包括相应的描述符等等,只有通过调用pthread_join函数来堵塞当前线程直到目标进程结束,最后会把相应的资源回收,否则如果没有调用join那么线程的状态就类似于线程的僵死状态。
detached: 该状态其主要是把子线程与主线程分离,当线程退出会自己主动释放相应资源,这里不详细介绍。
pthread_join实例
以上是使用pthread_join来进行等待子线程结束的实验结果,主线程一直等待子线程运行exit才结束,如果我们不加入pthread_join看看如下结果,其子线程根本来不及运行主线程就结束了。
补充说明 :pthread_self比较简单,仅仅是获得所调用线程的线程标识,跟getpid进程pid是类似的。