多线程编程
多线程编程可以将任务划分成多个执行流,每个执行流都有一个指定要完成的任务,这些子任务可能需要计算出中间结果,然后合并为最终的输出结果。
进程
有时称为重量级进程
线程
有时候称为轻量级进程,可以将它们认为是在一个主进程或“主线程”中并行运行的一些“迷你进程”。一个进程中的各个线程与主线程共享同一片数据空间。每个新进程都拥有自己的内存和数据栈等,只能采用进程间通信(IPC)的方式共享信息。
全局解释器锁
要想在python中使用多线程编程,必须要知道全局解释器锁的概念。
内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。
这个是由全局解释器锁(GIL)控制的,保证只能有一个线程在运行,它在多线程环境中的执行方式如下。
- 设置GIL
- 切换进一个线程去运行
- 执行操作
- 把线程设置回睡眠状态(切换出线程)
- 解锁GIL
- 重复上述步骤
python多线程模块
包括thread、threading、queue模块等。
thread模块提供了基本的线程和锁定支持;
threading模块提供了更高级别、功能更全面的线程管理;
queue模块创建一个队列数据结构,用于在多线程之间进行共享。
提示:避免使用thread模块
python由于GIL的限制,多线程更适合于I/O密集型应用,I/O释放了GIL,可以允许更多的并发,而不是计算密集型应用。对于后一种情况而言,为了实现更好的并行性,需要使用多进程,以便让CPU的其它内核来执行。
thread模块
Python3中该模块被重新命名为 _thread
在这里使用thread模块只是为了介绍多线程编程
看一个示例(没有控制何时退出)
import _thread
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)来挂起主线程
sleep(6)
print('all DONE at: ', ctime())
if __name__ == '__main__':
main()
starting at: Tue Jan 28 16:27:37 2020
start loop0 at: Tue Jan 28 16:27:37 2020
start loop1 at: Tue Jan 28 16:27:37 2020
loop1 done at: Tue Jan 28 16:27:39 2020
loop0 done at: Tue Jan 28 16:27:41 2020
all DONE at: Tue Jan 28 16:27:43 2020
与不使用线程相比,总共运行时间相差不大,都是6 s左右,Why???
我们没有写让主线程等待子线程全部完成后再继续的代码,即所说的同步。在这个例子中,调用sleep()来作为同步机制。将其设定为6 s是因为我们知道所有线程(4 s、2 s)会在主线程计时到6秒之前完成。如果没有 这句将会得到下面的输出。
starting at: Tue Jan 28 17:05:39 2020
all DONE at: Tue Jan 28 17:05:39 2020
再一次修改代码,引入锁
import _thread
from time import sleep, ctime
loops = [4, 2]
def loop(nloop, nsec, lock):
print('start loop', nloop, 'at:', ctime())
sleep(nsec)
print('loop', nloop, 'done at:', ctime())
lock.release()
def main():
print('starting at: ', ctime())
locks = []
nloops = range(len(loops))
for i in nloops:
lock = _thread.allocate()
lock.acquire()
locks.append(lock)
# 不要在上锁的循环中启动线程
# 同步线程
for i in nloops:
_thread.start_new_thread(loop, (i, loops[i], locks[i]))
for i in nloops:
# 按照顺序检查每个锁
while locks[i].locked():
pass
print('all DONE at: ', ctime())
if __name__ == '__main__':
main()
starting at: Tue Jan 28 17:17:48 2020
start loop 0 at: Tue Jan 28 17:17:48 2020
start loop 1 at: Tue Jan 28 17:17:48 2020
loop 1 done at: Tue Jan 28 17:17:50 2020
loop 0 done at: Tue Jan 28 17:17:52 2020
all DONE at: Tue Jan 28 17:17:52 2020
运行时间4 s
_thread模块对于进程何时退出没有控制
_thread模块拥有的同步原语很少,只有一个
threading模块更加先进,有更好的线程支持
Threading模块
该模块中除了Thread类以外,还包括很多非常好用的同步机制。
守护线程:如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护线程标记。
Thread类
使用Thread类,可以有很多方法来创建线程。
创建Thread实例,传给它一个函数
import threading
from time import ctime, sleep
def loop(nloop, nsec):
print('loop', nloop, 'starting at:', ctime())
sleep(nsec)
print('loop', nloop, 'finished at:', ctime())
def main():
print('starting at:', ctime())
loops = [4, 2]
threads = []
nloops = range(len(loops))
for i in nloops:
# 与_Thread.start_new_thread不同,新线程不会立即开始执行
t = threading.Thread(target=loop, args=(i, loops[i]))
threads.append(t)
for i in nloops:
# 同步执行
threads[i].start()
for i in nloops:
# 主线程直至启动的线程终止之前一直被挂起,自旋锁
threads[i].join()
print('all done at:', ctime())
if __name__ == '__main__':
main()
对于join()方法而言,其另一个重要方面是其实它根本不需要调用。一旦线程启动,它们就会一直执行,直到给定的函数完成后退出。
创建Thread实例,传给它一个可调用的类实例
import threading
from time import ctime, sleep
def loop(nloop, nsec):
print('loop', nloop, 'starting at:', ctime())
sleep(nsec)
print('loop', nloop, 'finished at:', ctime())
class ThreadFunc(object):
def __init__(self, func, args, name=''):
self.func = func
self.args = args
self.name = name
def __call__(self):
self.func(*self.args)
def main():
print('starting at:', ctime())
threads = []
loops = [4, 2]
nloops = range(len(loops))
for i in nloops:
t = threading.Thread(target=ThreadFunc(loop, (i, loops[i]), loop.__name__)) # 不需要再将参数传递给构造函数
threads.append(t)
for i in nloops:
threads[i].start()
for i in nloops:
threads[i].join()
print('all done at:', ctime())
if __name__ == '__main__':
main()
派生Thread的子类,并创建子类的实例
import threading
from time import ctime, sleep
def loop(nloop, nsec):
print('loop', nloop, 'starting at:', ctime())
sleep(nsec)
print('loop', nloop, 'finished at:', ctime())
class MyThread(threading.Thread):
# 对Thread子类化
def __init__(self, func, args, name=''):
threading.Thread.__init__(self)
self.func = func
self.args = args
self.name = name
def run(self):
self.res = self.func(*self.args)
def getResult(self):
return self.res
def main():
print('starting at:', ctime())
threads = []
loops = [4, 2]
nloops = range(len(loops))
for i in nloops:
t = MyThread(loop, (i, loops[i]), loop.__name__)
# 不需要再将参数传递给构造函数
threads.append(t)
for i in nloops:
threads[i].start()
for i in nloops:
threads[i].join()
print('all done at:', ctime())
if __name__ == '__main__':
main()
多线程实战
从网页端爬取一些电影的上映日期
首先看没有使用线程的代码
from atexit import register
import re
from threading import Thread
from time import ctime
import requests
regex = re.compile(r'(?m)class="ellipsis">([\d-]{4,10})')
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'}
url = 'https://maoyan.com/films/'
movie = {'344990': "唐人街探案2",
'247949': '冰雪奇缘2',
'1279731': '宠爱',
'1228869': '半个喜剧',
'1211270': '哪吒之魔童降世'
}
def get_date(data):
text = requests.get('{0}{1}'.format(url, data), headers=headers).text
return regex.findall(text)[0]
def _show(data):
# 函数名最前面的单下划线表示这是一个特殊函数,只能被本模块的代码使用,不能导入到其它文件中被使用
print('- {0} 上映日期: {1}'.format(movie[data], get_date(data)))
def _main():
print('At', ctime())
for i in movie:
_show(i)
# 装饰器,在python解释器中注册一个退出函数,会在脚本退出之前请求调用这个特殊函数
@register
def _atexit():
print('all done at:', ctime())
if __name__ == '__main__':
_main()
At Tue Jan 28 22:03:56 2020
- 唐人街探案2 上映日期: 2018-02-16
- 冰雪奇缘2 上映日期: 2019-11-22
- 宠爱 上映日期: 2019-12-31
- 半个喜剧 上映日期: 2019-12-20
- 哪吒之魔童降世 上映日期: 2019-07-26
all done at: Tue Jan 28 22:03:59 2020
耗时3秒
使用多线程的代码
只用修改_main()即可。
def _main():
print('At', ctime())
for i in movie:
Thread(target=_show, args=(i, )).start()
At Tue Jan 28 22:12:22 2020
- 宠爱 上映日期: 2019-12-31
- 冰雪奇缘2 上映日期: 2019-11-22
- 半个喜剧 上映日期: 2019-12-20
- 哪吒之魔童降世 上映日期: 2019-07-26
- 唐人街探案2 上映日期: 2018-02-16
all done at: Tue Jan 28 22:12:23 2020
耗时1秒,工作效率大大提高
注意,多线程版本按照完成的顺序输出,而单线程版本按照变量的顺序。在单线程版本中,顺序是由字典的键决定的,多线程的查询是并发产生的,输出的先后则会由每个线程完成任务的顺序来决定。
同步原语
一般在多线程代码中,总会有一些特定的函数或代码块不希望被多个线程同时执行,通常包括修改数据库、更新文件或其它会产生竞态条件的类似情况。
这就是需要使用同步的情况。当任意数量的线程可以访问临界区的代码但在给定的时刻只有一个线程可以通过时,就是使用同步的时候了。
这里介绍两种常用的同步原语:锁/互斥、信号量
锁示例
锁有两种状态:锁定和未锁定
from atexit import register
from random import randrange
from threading import Thread, currentThread, Lock, enumerate
from time import sleep, ctime
class CleanOutputSet(set):
# 集合的子类
def __str__(self):
return ', '.join(x for x in self)
lock = Lock()
loops = (randrange(2, 5) for x in range(randrange(3, 7)))
remaining = CleanOutputSet()
def loop(nsec):
myname = currentThread().name
# 使用上下文管理更加方便
with lock:
remaining.add(myname)
print('[%s] Started %s' % (ctime(), myname))
# or
# lock.acquire()
# remaining.add(myname)
# print('[%s] Started %s' % (ctime(), myname))
# lock.release()
sleep(nsec)
with lock:
remaining.remove(myname)
print('[%s] Completed %s (%d secs)' % (ctime(), myname, nsec))
print(' (remaining: %s)' % (remaining or 'NONE'))
# or
# print(' (remaining: %s)' % (enumerate() or 'NONE'))
# enumerate()会返回仍在运行的线程列表
def _main():
for pause in loops:
Thread(target=loop, args=(pause, )).start()
@register
def _atexit():
print('all done at:', ctime())
if __name__ == '__main__':
_main()
[Wed Jan 29 00:25:33 2020] Started Thread-1
[Wed Jan 29 00:25:33 2020] Started Thread-2
[Wed Jan 29 00:25:33 2020] Started Thread-3
[Wed Jan 29 00:25:33 2020] Started Thread-4
[Wed Jan 29 00:25:33 2020] Started Thread-5
[Wed Jan 29 00:25:33 2020] Started Thread-6
[Wed Jan 29 00:25:35 2020] Completed Thread-5 (2 secs)
(remaining: Thread-3, Thread-4, Thread-6, Thread-2, Thread-1)
[Wed Jan 29 00:25:35 2020] Completed Thread-4 (2 secs)
(remaining: Thread-3, Thread-6, Thread-2, Thread-1)
[Wed Jan 29 00:25:36 2020] Completed Thread-6 (3 secs)
(remaining: Thread-3, Thread-2, Thread-1)
[Wed Jan 29 00:25:37 2020] Completed Thread-2 (4 secs)
(remaining: Thread-3, Thread-1)
[Wed Jan 29 00:25:37 2020] Completed Thread-3 (4 secs)
(remaining: Thread-1)
[Wed Jan 29 00:25:37 2020] Completed Thread-1 (4 secs)
(remaining: NONE)
all done at: Wed Jan 29 00:25:37 2020
信号量
信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。threading模块包括两种信号量类:Semaphore和BoundedSemaphore。BoundedSemaphore的一个额外功能是这个计数器的值永远不会超过它的初始值,换句话说,它可以防范其中信号量释放次数多于获得次数的异常用例。
提示:threading模块的同步原语并不是类名,所以不能对它们子类化,因为它们是函数。