Python多线程

同步和异步概念和区别

​ 我们经常会看到同步、异步、阻塞、非阻塞四种调用方式以及它们的组合。那同步和异步的概念和区别又是什么呢?

同步(Sync)

​ 同步的思想就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。 也就是说所有的操作都做完,才返回给用户。(会给人一种程序还在进行,卡死的感觉),事情需要一件一件的做,等前一件做完了才能做下一件事。

​ 举例:B/S模式中的表单提交,具体过程是:客户端提交请求->等待服务器处理->处理完毕返回,在这个过程中客户端(浏览器)不能做其他事。

异步(Async)

​ 异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

​ 对于通知调用者的三种方式,具体如下:

状态

​ 即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。

通知

​ 当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。

回调

​ 与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。

​ 举例:B/S模式中的ajax请求,具体过程是:客户端发出ajax请求->服务端处理->处理完毕执行客户端回调,在客户端(浏览器)发出请求后,仍然可以做其他的事。

同步和异步的区别:

​ 请求发出后,是否需要等待结果,才能继续执行其他操作。

多线程的动机

​ 在多线程(MT)编程出现之前,程序的运行由一个执行序列组成,执行序列按顺序在主机的中央处理器(CPU)中运行。无论是任务本身要求顺序执行还是整个程序是由多个子任务组成,程序都是按照这种方式执行的,即便子任务相互独立,互相无关(即,一个子任务的结果不影响其他子任务的结果)时也是这样。多线程的目的就是要并行运行这项相互独立的子任务,这样的并行处理可以大幅度的提升整个任务的效率。

​ 多线程对于某些任务是最理想的,这些任务的特点是:**它们本质上就是异步的,需要有多个并发事务,各个事务的运行程序可以是不确定的,随机的,不可预测的。**这样的编程任务可以被分为多个执行流,每个流都要有一个要完成的目标。根据应用的不同,这些子任务可能都要计算出一个中间结果,用于合并得到最后的结果。

​ 使用多线程编程和一个共享的数据结构如Queue(一种多线程队列数据结构),这种程序任务可以用几个功能单一的线程来组织:

  • UserRequestThread:负责读取用户的输入,可能是一个I/O通道。程序可能创建多个线程

    每个客户一个,请求会被放到队列中。

  • RequestProcessor:一个负责从队列中获取并处理请求的线程,它为下面那种线程提供输出。

  • ReplyThrea:负责把用户的输出取出来,如果是网络应用程序就把结果发送出去,否则就保存在本地系统或者数据库中。

    把编程任务用多线程来组织可以降低程序的复杂度,并使得干净,有效和良好组织地程序结构的实现变得可能。并且每个线程的逻辑都不会很复杂,因为它要做的事很清楚。

线程和进程

什么是进程?

​ 计算机程序只不过是磁盘中可执行的,二进制(或其他类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。进程(有时叫重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。OS管理在其上运行的所有进程,并为这些进程公平的分配时间。进程也可以通过fork和spawn操作来完成其它的任务。不过各个进程都有自己的内存空间,数据栈等,因此只能使用进程间通信(IPC),而不能直接共享信息。

进程和线程的概念、区别和联系

深入理解线程和进程

什么是线程?

​ 线程(有时也叫轻量级进程)和进程有点像,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。

​ 线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断),或暂时的被挂起(睡眠),让其他的线程运行,这叫做让步。

​ 一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。线程一般都是并发执行的,正是由于这种并行和数据共享的机制使得多个任务的合作变为可能。

​ 在单CPU的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来,让其他的线程去运行。在进程的整个运行过程中,每个线程都只做自己的事,在需要的时候跟其他的线程共享运行的结果。当然,这样的共享是存在危险的,如果多个线程共同访问同一片数据,则由于数据的访问顺序不一样,有可能造成结果不一致问题。这就是竞态条件(race condition)。但可以通过大多数线程库带有一系列的同步原语,来控制线程的执行和数据的访问。还有就是由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情况下,这种"贪婪"的函数会让CPU的时间分配有所倾斜,导致各个线程分配的运行时间可能不尽相同,不尽公平。

Python线程和GIL

全局解释锁(GIL)

​ Python代码的执行是由Python虚拟机(也叫解释器主循环)来控制,Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行,就像单CPU系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地道理,虽然Python解释器中可以运行多个线程,但在任意时刻,只有一个线程在解释器中运行。

​ 对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程的环境下,Python虚拟机按以下方式执行:

  1. 设置GIL

  2. 切换到一个线程去运行

  3. 运行

    a.指定数量的字节码指令,或者,

    b.线程主动让出控制(可以调用time.sleep(0))

  4. 把线程设置为睡眠状态

  5. 解锁GIL

  6. 再次重复以上所有步骤

​ 在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(因为在这期间内有Python的字节码被运行,所以不会做线程切换),编写扩展的程序员可以主动解锁GIL。

​ 对于I/O密集型的Python程序比计算密集型程序更能充分利用多线程环境的好处。这是因为,对所有面向I/O的(会调用内建的OS的C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其它的线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。

GIL的一点源码

退出线程

​ 当一个线程结束计算,它就退出了。线程可以调用thread.exit()之类的退出函数,也可以使用Python退出进程的标准方法,如sys.exit()或抛出一个SystemExit异常等。但是,我们不可以直接kill掉一个进程。

​ 有两个和线程相关的模块thread和threading,但不建议直接使用thread模块,显而易见的一个原因是当主进程退出的时候,所有其它线程没有被清除就退出了。但另一个模块threading就能确保所有"重要的"子线程都退出后,进程才会结束。

​ 而且,主线程应当作为一个好的管理者,它要了解每个线程都要做些什么事,线程都需要什么数据和参数,以及在线程结束的时候,它们都提供了什么结果。这样,主线程就可以把各个线程的结果组合成一个有意义的最后结果。

​ 实际上Python提供了几个用于多线程编程的模块,包括thread,threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,而threading提供了更高级别,功能更强的线程管理功能。Queue模块允许用户创建一个用于多个线程之间共享数据的队列数据结构。

没有线程支持的情况
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding:utf-8 -*-
#onethr.py
#!/usr/bin/env python
from time import sleep,ctime

def loop0():
    print('start loop0 at:',ctime())
    sleep(4)
    print('loop0 done at:',ctime())

def loop1():
    print('start loop1 at:', ctime())
    sleep(2)
    print('loop1 done at:', ctime())

def main():
    print('starting at:', ctime())
    loop0()
    loop1()
    print('all done at:',ctime())

if __name__ == "__main__":
    main()

​ 我们会使用time.sleep()函数来演示线程是怎样工作的。time.sleep()需要一个浮点型参数,来指定"睡眠"的时间(单位秒)。这就意味着,程序的运行会被挂起指定的时间。

​ 我们要创建两个"计时循环"。一个睡眠4秒钟,一个睡眠2秒钟,分别是loop0()和loop1().(我们命名为"loop0"和"loop1"表示我们将有一个循环的序列)。

​ 如果我们在一个进程或者一个线程中,顺序的执行loop0()和loop1(),那运行的总时间为6秒。在启动loop0(),loop1(),和其它的代码时,也要花去一些时间,所以,我们看到的总时间也有可能会是7秒钟。

​ 假定loop0和loop1里做的不是睡眠,而是各自独立的,不相关的运算,各自的运算结果到最后将会汇总成一个最终的结果。这时,如果能让这些计算并行执行的话,那这样就会减少总的运行时间,这就是多线程编程的前提条件。

避免使用thread模块

​ 不建议使用thread模块,首先是因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading模块出现冲突。其次,低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多。

​ 另一个不要使用thread原因是,对于你的进程什么时候应该结束完全没有控制,当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。而threading模块能确保重要的子线程退出后进程才退出。

​ 只建议那些有经验的专家想访问线程的底层结构的时候,才使用thread模块。

thread模块和threading模块

thread模块

​ thread模块除了产生线程外,thread模块也提供了基本的同步数据结构锁对象(lock object,也叫原语锁,简单锁,互斥锁,互斥量,二值信号量),同步原语与线程的管理是密不可分的。

​ 下表列出常用的线程函数以及LockType类型的锁对象的方法。

函数 描述
thread模块函数
start_new_thread(function,args,kwargs=None) 产生一个新的线程,在新线程中用指定的参数和可选的kwargs来调用这个函数。
allocate_lock() 分配一个LockType类型的锁对象
exit() 让线程退出
LockType类型锁对象方法
acquire(wait=None) 尝试获得锁对象
locked() 如果获取了锁对象返回True,否则返回False
release() 释放锁

​ start_new_thread()函数式thread模块的一个关键函数,它的语法与内建的apply()函数完全一样,其参数为:函数,函数的参数以及可选的关键字参数。不同的是,函数不是在主线程里运行,而是产生一个新的线程来运行这个函数。

​ 这里我们执行的是和onethr.py中一样的循环,不同的是,这一次我们使用的是thread模块提供的多线程的机制。两个循环并发的执行(显然,短的那个先结束)。总的运行时间为最慢的那个线程的运行时间,而不是所有线程的运行时间之和。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# -*- coding:utf-8 -*-
#!/usr/bin/env python
#mtsleep1.py
import thread  #需要python2环境
from time import sleep,ctime

def loop0():
    print('start loop0 at:',ctime())
    sleep(4)
    print('loop0 done at:',ctime())

def loop1():
    print('start loop1 at:', ctime())
    sleep(2)
    print('loop1 done at:', ctime())

def main():
    print('starting at:', ctime())
    thread.start_new_thread(loop0,())
    thread.start_new_thread(loop1,())
    sleep(6)
    print('all done at:',ctime())

if __name__ == "__main__":
    main()

​ 这个程序的输出与之前的输出大不相同,之前是运行了6,7秒,而现在则是4秒,是最长的循环的运行时间和其它代码的时间之和。睡眠4秒和睡眠2秒是并发执行的,这样就使得总的运行时间被缩短了。loop1在loop0前面就结束了,而且程序多了一个sleep(6)的函数调用。为什么要加入sleep(6),这是因为如果没有让主线程停下来,那主线程就会运行下一条语句,显示"all done",然后就关闭了运行着的loop0()和loop1()的两个线程,退出了。

​ 的确这里应该有更好的管理线程的办法,而不是在主线程里面做一个额外的延时6s的操作。也就是说,我们要写让主线程停下来等所有子线程结束之后再继续运行的代码,这也是线程需要同步的原因。用sleep()函数做线程的同步操作是不可靠的,因为如果有循环的执行时间不能事先确定的话,这可能会造成主线程过早或过晚退出,这就要用到锁了。

Python多进程

Python多进程和多线程参考

扩展阅读

Python并发编程理论部分1

并发编程理论部分2

Python模块学习:threading多线程控制和处理

Python中的multiprocessing和threading

Python 多线程 threading和multiprocessing模块

Python并发编程之协程/异步IO

python多进程

Python多进程基本说明

实现多进程的几种方式

Python多进程使用说明

python多线程

Python核心编程-多线程

Python的一种线程池模型

多种方式实现Python线程池

假的多线程

一个进程内运行多个python虚拟机

​ 说明:如果想让python多线程,真正支持多核,那么应该 1、重写python编译器(官方cpython)如使用:PyPy解释器 2、调用C语言的链接库 。

​ 而实际上,多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低 。

python协程

​ Python并行的最好策略应当是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。 。

Python协程概念

Python协程的一点剖析