Python 进程线程协程(1)--概念

年龄就像是验金石。人们到了一定的年龄之后,一类人变得越发有趣,一类人变得越发无聊。前者开始创造生活,后者开始被生活创造。不幸的是,大多数人偷懒,愿意把后半生的命运交给前半生的惯性。幸运的是,一小部分人开始有能力刹住边性,去重新定位方向。

最近的业余时间主要放在了学习Python线程、进程和协程里,第一次用python的多线程和多进程是在两个月前,当时只是简单的看了几篇博文然后就跟着用,没有仔细去研究,第一次用的感觉它们其实挺简单的,最近这段时间通过看书, 看Python 中文官方文档等等相关资料,发现并没有想想中的那么简单,很多知识点需要仔细去理解,Python线程、进程和协程应该是Python的高级用法。Python的高级用法有很多,看看Python 中文官方文档就知道了,当然有时间看看这些模块是怎么实现的对自己的提高是很有帮助的。选择了编程这个行业,就是要不断的学习、思考、归纳总结经验,路漫漫其修远兮,吾将上下而求索,希望能与各位共勉。接下来要花好几篇博文的篇幅来讲讲我学习线程、进程和协程的经验,有讲得不好的地方,希望大家批评指正。这篇博文主要讲讲与之有关的概念。

线程与多线程

线程

  • 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元;
  • 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。有了这些它能够记录自己运行到了什么地方,可以称为线程的上下文;
  • 线程的运行可能被抢占(中断)或暂时的被挂起(也叫睡眠)让其它的线程运行,这叫做让步;
  • 线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行
  • 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不独立拥有系统资源,但它可与同属一个进程的其它线程共享该进程所拥有的全部资源。

多线程

  • 每一个应用程序都至少有一个进程和一个线程。线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的被划分成一块一块的工作,称为多线程;

  • 很好理解,开发软件就是每个人或者每个小组负责一个模块,当所有人(组)相关的模块都编写完后,就开始合并代码,然后就测试,修复bug

    除非代码依赖第方资源,否则在单处理器的机器上使用多线程,不会加速代码的执行速度,甚至会增加一些线程管理的开销;

    实际上,在单处理器的系统中,每个线程会被安排成每次只运行一小会,然后就把CPU让出来,让其它的线程去运行。比如线程切换等等,这些都是要花费资源和时间的,所以在单CPU机器上使用多线程有时候不但感觉不到执行速度变快,反而变慢了;

  • 多线程会从多处理器或者多核的机器上获益,它会在每个处理器上并行执行每个线程,从而提高执行速度;

  • 线程之间可以共享运行结果。但是这样做有一定的危险,比如两个线程更新同一个数据,但是这两个线程运行的结果不一样,这叫做竞态条件,这个会造成竞争危害,会发生不可预测的结果。所以利用锁机制可以保护数据。

Python中的多线程

python的多线程并没有想象中的那么理想,是因为有一个叫GIL的东西在限制。那什么是GIL呢?GIL中文名叫全局解释器锁,是python虚拟机上用作互斥线程的一种机制,它的作用就是要保证在任何情况下虚拟机上只有一个线程被运行,而其它线程都处在等待GIL锁被释放的状态。所以它是个“伪多线程”,它的情况就跟上面说的在单处理器机器上运行多线程一样,不会加速代码的执行速度,甚至会增加一些线程管理的开销。

python虚拟机上多线程是按如下方式执行的:

  • a、设置 GIL
  • b、切换到一个线程去运行;
  • c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
  • d、把线程设置为睡眠状态;
  • e、解锁 GIL;
  • f、再次重复以上所有步骤

在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)。比如带有I/O操作(会调用内建的操作系统C代码,I/O操作就是输入输出操作,要想详细了解它可以参考其它资料)的线程,GIL会在这个I/O操作被调用之前就被释放。

对于纯计算的程序,没有I/O操作,解释器会根据sys.ssetcheckinterval()的设置来自动进行线程间的切换,默认情况下是每隔100个时钟就会释放GIL锁从而轮换到其它线程执行。

那为什么Python中还要在多线程中引入GIL呢?是为了保证对虚拟机内部共享资源访问的互斥性。python对象的对象管理与引用计数器密切相关,当计数器的值为0,该对象会被垃圾回收器回收(不了解这块知识的可以在网上查找相关资料或者看《Python源码解析》这本书),当撤销对一个对象的引用时,python解释器会对该对象以及其计数器管理进行以下两步操作:

  • a、使引用计数器减1
  • b、判断计数器的值是否为0,如果为0,则销毁该对象

假设现在有A、B两个线程同时引用同一个对象obj,这时obj对象的引用计数器的值就为2,如果现在A线程打算撤销对obj的引用,当执行完第一步”使引用计数器值减1“的时候,由于存在多线程调度机制,A恰好在这个关键点被挂起了,而进入了B线程执行的状态,如果这个时候B线程也是要撤销对obj的引用,并且完成了上面的a,b两个步骤,这时obj的引用计数器就是0了,obj对象就被销毁了,内存被释放出来了,麻烦就可能出现了,当A线程再次被唤醒时,它肯定会接着执行上面的b步骤,结果发现已经面目全非了,那么其操作结果完全未知。所以引入了GIL,保证对虚拟机内部共享资源访问的互斥性。

GIL的引入使多线程不能在多核系统中发挥优势,但也带来了一些好处,就是大大简化了Python线程中共享资源的管理。不过Python提供了其它方式绕过了GIL的局限性来充分利用多核的计算能力,比如多进程multiprocessing模块、C语言扩展方式、ctypes库等等。

进程

进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间、内存、数据栈以及其它记录其运行轨迹的辅助数据。操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。进程也可以通过fork和spawn操作来完成其它的任务,不过各个进程有自己的内存空间、数据栈等,所以只能使用进程间通讯(IPC),而不能直接共享信息。

协程

  • 协程是一种用户级的轻量级线程,不同于线程的地方在于协程不是操作系统进行切换,而是由程序员编码进行切换的,也就是说切换是由程序员控制的,这样就没有了线程所谓的安全问题;
  • 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。

python里面怎么使用协程?答案是使用gevent模块。使用协程,可以不受线程开销的限制。所以最推荐的方法,是多进程+协程(可以看作是每个进程里都是单线程,而这个单线程是协程化的)多进程+协程下,避开了CPU切换的开销,又能把多个CPU充分利用起来。

原文: Python:线程、进程与协程

-------------本文结束 感谢您的阅读-------------
作者Magiceses
有问题请 留言 或者私信我的 微博
满分是10分的话,这篇文章你给几分