1、概述
本文会涉及多线程编程的方方面面知识,后面有新东西也会陆续更新进来
python对多线程的支持
先看一个概念:
GIL(Global Interpreter Lock,全局解释器锁),官方描述如下:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
由于GIL的存在,python其实无法利用多处理器的优势,任意时刻只会有一个线程运行在解释器中,也就是大计算量的程序在python中通过多线程处理其实不见得会变快。但是IO密集型程序可以很好地利用多线程,比如用python开发一个rest客户端程序,如果单线程实现,假如发送一个http请求服务器端需要耗费5s来处理,串行发送1000个就需要5000s左右;但是开1000个线程,就可以同时发送1000个请求,一起等待相应,基本10s之内就能完成这个过程。
Python中多线程相关的模块包括:thread,threading,Queue。
- thread:多线程的底层支持模块,一般不建议使用【本文暂不涉及】
- threading:对thread进行了封装,将一些线程的操作对象化
- Queue:实现了多生产者(Producer)、多消费者(Consumer)的队列
2、开始使用多线程
2.1、入门例子
python的threading库可以实现在单独的线程中执行任意的python可调用对象,我们通过创建Thread类的实例,然后提供其需要被单独线程执行的可调用对象,就完成目的了,看一个入门例子:
# -*- coding: utf-8 -*-
import threading
from time import sleep
def show(name):
sleep(1)
print('Hello %s' % name)
if __name__ == '__main__':
t1 = threading.Thread(target=show, args=('wing',))
t2 = threading.Thread(target=show, args=('WING',))
t1.start()
t2.start()
print('All done!')
输出结果如下
All done!
Hello wing
Hello WING
创建一个线程实例需要传递给它目标函数的引用,参数元组,然后调用start方法就会开始这个线程的运行。
通过Thread类可以有多种方法创建线程:
- 用一个函数作为参数实例化一个Thread类,多线程执行这个函数(上面例子所示)
- 用一个可调用类作为参数实例化一个Thread类,多线程执行这个“可调用类”(和第一个本质相同)
- 从Thread类派生一个子类,重写run方法(比较常规的用法)
2.2、多线程中的join
观察上面输出会发现“All done!”居然在最开始输出了,我们的本意是线程执行完之后才输出“All done!”,咋办呢?看下面一段代码:
# -*- coding: utf-8 -*-
import threading
from time import sleep
def show(name):
sleep(1)
print('Hello %s' % name)
if __name__ == '__main__':
t1 = threading.Thread(target=show, args=('wing',))
t2 = threading.Thread(target=show, args=('WING',))
t1.start()
t2.start()
t1.join()
t2.join()
print('All done!')
输出如下:
Hello WING
Hello wing
All done!
这里介绍一下这个join,文档中有如下一句话:
"Wait until the thread terminates"
也就是说要等待这个线程执行完毕才开始后续操作,这样就实现了等待子线程完成再进行其他操作的目的。
该函数定义是def join(self, timeout=None): ...
也就是说还可以设置timeout参数,避免子线程出问题一直不结束的情况下父线程无限等待的问题
2.3、用一个可调用类作为参数实例化一个Thread类
先看下面代码:
# -*- coding: utf-8 -*-
import threading
from time import sleep
def show(name):
sleep(1)
print('Hello %s' % name)
class MyThread(object):
def __init__(self, func, args, name=""):
self.func = func
self.args = args
self.name = name
def __call__(self):
self.func(self.args)
if __name__ == '__main__':
t1 = threading.Thread(target=MyThread(show, ('wing',)))
t2 = threading.Thread(target=MyThread(show, ('Wing',)))
t1.start()
t2.start()
t1.join()
t2.join()
print('All done!')
如上,其实这里很好理解,主要注意的一个知识点是类的“call()”方法,这里的target is a callable object, 是一个可调用对象,MyThread(show, ('wing',))实例化了一个类,得到的对象就是这样一个可调用对象,和函数名对应,真正调用的时候就是执行了__call__()方法,这里的__call__()只是简单地执行初始化时传递过来的函数,类来实现相比于函数要灵活很多。
2.4、从Thread类派生一个子类,重写run方法(推荐的方法)
先看下面代码:
# -*- coding: utf-8 -*-
import threading
from time import sleep
def show(name):
sleep(1)
print('Hello %s' % name)
class MyThread(threading.Thread):
def __init__(self, func, args):
super(MyThread, self).__init__()
self.func = func
self.args = args
def run(self):
self.func(self.args)
if __name__ == '__main__':
t1 = MyThread(show, ('wing',))
t2 = MyThread(show, ('Wing',))
t1.start()
t2.start()
t1.join()
t2.join()
print('All done!')
输出结果如下:
Hello wing
Hello Wing
All done!
这里继承了threading模块的Thread类,重写了init和run方法,通过这种方式来实现多线程执行的效果。
3、多线程处理的返回值问题
上面的show函数只是简单的打印操作,但是如果需要多线程处理的函数如下:
def sum(args):
res = 0
for i in args:
res += i
return res
这个时候需要记录函数执行的结果,在上面的实现中并不能达到这样的效果,这个时候我们可以稍微修改一下MyThread类,如下:
# -*- coding: utf-8 -*-
import threading
def sum(args):
res = 0
for i in args:
res += i
return res
class MyThread(threading.Thread):
def __init__(self, func, args):
super(MyThread, self).__init__()
self.func = func
self.args = args
self.result = None
def run(self):
self.result = self.func(self.args)
def get_result(self):
return self.result
if __name__ == '__main__':
t1 = MyThread(sum, (1, 2))
t2 = MyThread(sum, (11, 22))
t1.start()
t2.start()
t1.join()
t2.join()
print('1 + 2 = ', t1.get_result())
print('11 + 22 = ', t2.get_result())
print('All done!')
输出结果如下:
1 + 2 = 3
11 + 22 = 33
All done!
4、python线程同步机制
先看下面示例
# -*- coding: utf-8 -*-
import threading
from time import sleep
count = 2
def show():
global count
if count > 0:
# sleep(1)
count -= 1
if __name__ == '__main__':
threads = list()
for i in range(3):
threads.append(threading.Thread(target=show))
for t in threads:
t.start()
for t in threads:
t.join()
print(count)
最后的结果是0,准确说多次执行发现结果是0,至于0是不是唯一结果,这里先不下结论,如果我们尝试把sleep(1)这一句注释放开,就会发现结果基本变成了-1
其实这里的sleep表示的只是对count操作之前的过程可能会耗时较长,这个时候count可能已经被改变了,而我们的本意是count大于0时才执行一次操作,本线程做这个处理的时候,不希望其他线程同时操作count的值。
再看下面一段代码:
# -*- coding: utf-8 -*-
import threading
from time import sleep
count = 2
lock = threading.Lock()
def show():
global count
# 获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放)
lock.acquire()
# 加上try是为了保证数据处理过程就算发生异常,锁也要被释放
try:
if count > 0:
sleep(1)
count -= 1
finally:
lock.release()
if __name__ == '__main__':
threads = list()
for i in range(3):
threads.append(threading.Thread(target=show))
for t in threads:
t.start()
for t in threads:
t.join()
print(count)
如上,通过加锁实现了线程同步,这里的锁释放还可以用更优雅的方式实现,如下:
with lock:
if count > 0:
count -= 1
---未完待续---