古之立大事者,不惟有超世之才,亦必有坚忍不拔之志。

Python高级编程和异步I/O并发编程笔记 10 多线程和多进程

Python admin 638℃ 0评论

python中的GIL

python的多线程编程需要首先了解全局解释器锁GIL。在cpython解释器中,python中的一个线程对应C语言的一个线程,python前期的设计为了简单,使用了GIL,作用就是同一时刻仅允许一个线程在一个CPU上执行字节码,无法将多个线程映射到多个CPU上执行,无法充分利用CPU资源。由于python前期的大量第三方包都使用了cpython,所以python的去GIL在很长一段时间很难实现,为了去除GIL,python也有很多其他的解释器,比如pypy。

虽然存在GIL,但是python的多线程编程并不是安全的,也需要考虑线程间的同步,原因在于GIL在某些情况下会释放。

GIL分配给某个线程后,释放并转交给其他线程的时间并不取决于线程是否执行完毕,即并不是整个过程的完全占有,而是会在适当的时间释放。释放的机制可以使结合了字节码执行的行数,如执行了100行字节码后会选择释放;也可以是结合时间片进行划分,如允许15毫秒后释放转交GIL锁。总结来看,python的GIL会根据执行的字节码行数以及时间片释放,在遇到I/O的操作时候主动释放。由于后面的释放策略,所以python多线程操作在处理I/O密集型操作中效率很高。

python多线程编程

多线程编程是几乎所有的编程语言都会遇到的问题。操作系统能够切换和调度的最小单元是线程,最初最小单元是进程,但由于进程消耗的资源非常大,所以演变成线程,线程是依赖于进程的。

对于I/O操作而言,多线程和多进程性能差别不大,由于对于操作系统而言,线程调度比进程调度更加轻量级,所以多线程的性能可能会略优于多进程。python的多线程编程有以下几种方法:

线程间通信——共享变量和Queue

在某些应用场景下,线程间必须相互协作才能完成任务,这时候就需要使用线程间通信,有两种方式能够实现,分别是共享变量和Queue,其中共享变量是指定义一个全局变量,通过它实现线程间的通信。

通过共享变量进行通信在多线程中可以使用,但是在多进程中是行不通的。同时共享变量这种方式并不是线程安全的,如pop出一个元素时可能会引发问题,所以并不推荐使用共享变量这种方法,而是使用Queue方式进行线程间同步。

线程同步

线程同步是多线程编程中必须要面对的问题,如在上文中介绍GIL中的例子,累加和累减两个操作多次执行结果不能保持一致,解决的方法就是几种锁机制。

1. Lock和RLock

使用锁(即Lock)最大的问题在于性能损失,锁的获取和释放都需要时间,另外锁可能会引发“死锁”问题。为了解决这个问题,python提供了RLock(可重入的锁),这种锁的特点是在同一个线程里可以连续多次获取和释放,当然acquire和release的数目必须匹配。

2. Condition条件变量

Condition即条件变量,是python提供的另一种用于复杂线程间同步的锁,是线程间最复杂的同步锁。

Condition中提供了一些函数,可以完成一个线程主动通知提醒另一个线程执行的功能。同样上例使用Condition完成如下:

3. Semaphore

Semaphore是用于控制临界区域的锁,如文件的读写,写一般仅允许一个线程操作,而读可以运行多个线程操作,即写操作是互斥的、读操作是非互斥的。

concurrent线程池

concurrent.futures是在python 3.2之后引入的,主要用来做线程池和进程池编程的,是一个非常底层的包,使我们进行多线程和多进程编程变得非常简单,同时两种编程的接口非常一致。

为了能够更好地管理线程,如控制线程并发的数目、自动调度(当线程池空了可以自动调度任务,而当线程池满了可以阻塞)等,除了数量控制还有一些其他功能,如主线程中可以获取某一个线程的状态或者某一个任务的状态,以及返回值;当一个线程完成的时候我们主线程能立即知道。

解析一下线程池的源码机制:

Future对象被称为未来对象,是任务返回的对象,当前可能未完成但是将来会完成,可以理解为task的返回容器,即任务执行结果存储。这种设计理念非常优秀,在线程池、进程池和协程池中都有应用,即Future对象贯穿python异步编程始终。

多进程编程

多进程参考:http://www.cnblogs.com/kaituorensheng/p/4445418.html

1. 多进程创建方式

(1)方式一

(2)方式二

2. 进程池

(1)方式一

(2)方式二

3.进程下的pool

(1)使用进程池(非阻塞)

函数解释:

  • apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞,apply(func[, args[, kwds]])是阻塞的(理解区别,看例1例2结果区别)
  • close()    关闭pool,使其不在接受新的任务。
  • terminate()    结束工作进程,不在处理未完成的任务。
  • join()    主进程阻塞,等待子进程的退出, join方法要在close或terminate之后使用。

执行说明:创建一个进程池pool,并设定进程的数量为3,xrange(4)会相继产生四个对象[0, 1, 2, 4],四个对象被提交到pool中,因pool指定进程数为3,所以0、1、2会直接送到进程中执行,当其中一个执行完事后才空出一个进程处理对象3,所以会出现输出“msg: hello 3”出现在”end”后。因为为非阻塞,主函数会自己执行自个的,不搭理进程的执行,所以运行完for循环后直接输出“mMsg: hark~ Mark~ Mark~~~~~~~~~~~~~~~~~~~~~~”,主程序在pool.join()处等待各个进程的结束。

(2) 使用进程池(阻塞):

4. 进程间通信 – Queue、Pipe,Manager

(1) Queue队列 ( multiprocessing自带的 Queue  )

multiprocessing中的Queue不能用于multiprocessing下的pool进程池,pool中的进程间通信需要使用manager中的queue。

目前我们已经说到了三个Queue:

  • from queue import Queue :用于线程通信
  • from multiprocessing import Queue :用于进程通信
  • from multiprocessing import Manager → q=Manager().Queue() :用于multiprocessing下的pool进程池的进程通信

(2)Pipe 管道( multiprocessing下的Pipe ,Pipe只能用于两个进程间的通信,Pipe的性能高于Queue的 )

GIL锁知识点:

  • Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
  • 即使在多核上运行多线程,同一时刻也只有一核会运行一个线程。
  • 遇到CPU密集型这种,建议不使用多线程,而改用多进程处理,但多进程更耗成本

转载请注明:北凉柿子 » Python高级编程和异步I/O并发编程笔记 10 多线程和多进程

喜欢 (1)or分享 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址