一. GIL
1. 基本概念
GIL:global interpreter lock (cpython下)
python中一个线程对应于c语言中的一个线程,GIL使得同一时刻只有一个线程在一个CPU上执行字节码,无法将多个线程映射到多个CPU上执行
2. GIL释放
1)GIL会根据执行的字节码行数以及时间片释放GIL
2)GIL在遇到I/O操作时会主动释放
total = 0
def add():
global total
for i in range(1000000):
total += 1
def desc():
global total
for i in range(1000000):
total -= 1
import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
按理说,最后的结果应该是0,但是多次运行程序得到的值不是0,且每次都是不一样的,证明并非add函数执行完后,才会执行desc函数,也就是在函数add执行期间释放了GIL,去执行了desc函数
3. 下面用一个小例子用字节码来解释为什么上面结果不为0
import dis
def add(a):
a += 1
def desc(a):
a -= 1
print(dis.dis(add))
print(dis.dis(desc))
输出结果
4 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_FAST 0 (a)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
None
7 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 INPLACE_SUBTRACT
6 STORE_FAST 0 (a)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
说明:
add函数字节码前4步, desc类似
1)load a
2)load 1
3)+操作
4)赋值给a
add的字节码和desc的字节码是并行执行的,且全局变量是共用的,所以两个线程的加法和减法操作使的变量a一直变化。GIL释放的时候可能是得到add的结果,也可能是得到desc的结果. 最终的返回值是在两个值间摇摆的
二. 多线程编程-threading
对于I/O操作来说,多线程和多进程性能差别不大
1. 通过Thread类实例化,实现多线程编程
import time
import threading
def eat(x):
print("start eat")
time.sleep(2)
print("end eat")
def drink(x):
print("start drink")
time.sleep(2)
print("end drink")
if __name__ == "__main__":
thread1 = threading.Thread(target=eat, args=("",))
thread2 = threading.Thread(target=drink, args=("",))
start_time = time.time()
thread1.start() #启动线程
thread2.start()
print("last time: {}".format(time.time()-start_time))
输出结果如下
start eat
start drink
last time: 0.0009996891021728516
end drink
end eat
说明:
1)结果显示持续时间不是2,而是接近于0,原因是程序运行print语句时自动生成一个主线程,它和自定义的2个线程是并行的,不需要等待自定义的2个线程结束后才开始执行
2)主线程的print语句执行结束后,会接着执行thread1, thread2,2秒后打印出"end drink", "end eat"; 如果想print语句结束后,自定义的2个线程也立刻结束,可以在定义thread2变量后加下面两条代码
thread1.setDaemon(True) #把thread1变为守护线程
thread2.setDaemon(True)
结果输出
start eat
start drink
last time: 0.0009982585906982422
可以自己测试一下只设置一个为守护线程,并修改sleep时间后的输出
3)如果想先执行thread1, thread2结束后,再执行print语句的主线程,修改代码如下
import time
import threading
def eat(x):
print("start eat")
time.sleep(2)
print("end eat")
def drink(x):
print("start drink")
time.sleep(4)
print("end drink")
if __name__ == "__main__":
thread1 = threading.Thread(target=eat, args=("",))
thread2 = threading.Thread(target=drink, args=("",))
#thread1.setDaemon(True) 这里注释掉,方便测试
#thread2.setDaemon(True)
start_time = time.time()
thread1.start() #启动线程
thread2.start()
thread1.join()
thread2.join()
print("last time: {}".format(time.time()-start_time))
输出结果
start eat
start drink
end eat
end drink
last time: 4.001539468765259
运行时间为4, 说明是按照线程执行之间最长的计算,而不是2个线程的执行时间总和
2. 通过继承Thread来实现多线程
import time
import threading
class Eat(threading.Thread):
# 给线程命名
def __init__(self, name):
super().__init__(name=name)
# 重载run()
def run(self):
print("start eat")
time.sleep(2)
print("end eat")
class Drink(threading.Thread):
def __init__(self, name):
super().__init__(name=name)
# 重载run()
def run(self):
print("start drink")
time.sleep(3)
print("end drink")
if __name__ == "__main__":
thread1 = Eat("chirou")
thread2 = Drink("hejiu")
start_time = time.time()
thread1.start() #启动线程
thread2.start()
thread1.join()
thread2.join()
print("last time: {}".format(time.time()-start_time))
输出
start eat
start drink
end eat
end drink
last time: 3.001695394515991
三. 线程间通信-共享变量和Queue
1. 共享变量--定义全局变量来让两个线程共用
import time
import threading
detail_url_list = []
def get_detail_html():
# 爬取文章详情页
global detail_url_list
if len(detail_url_list):
url = detail_url_list.pop()
print("get detail html started")
time.sleep(2)
print("get detail html end")
def get_detail_url():
# 爬取文章列表页,然后交给详情页函数
global detail_url_list
print("get detail url started")
time.sleep(4)
for i in range(20):
detail_url_list.append("http://projectsedu.com/{id}".format(id=i))
print("get detail url end")
if __name__ == "__main__":
thread_detail_url = threading.Thread(target=get_detail_url)
for i in range(3):
html_thread = threading.Thread(target=get_detail_html)
html_thread.start()
start_time = time.time()
print("last time: {}".format(time.time()-start_time))
问题:线程不安全,不同线程中可能会影响变量值,需要添加锁
2. 使用queue的方式进行线程间同步,队列是线程安全的
from queue import Queue
import time
import threading
def get_detail_html(queue):
#爬取文章详情页
while True:
url = queue.get() #从队列中取数据,如果队列为空会一直停在这一行
print("get detail html started")
time.sleep(2)
print("get detail html end")
def get_detail_url(queue):
# 爬取文章列表页
while True:
print("get detail url started")
time.sleep(4)
for i in range(20):
queue.put("http://projectsedu.com/{id}".format(id=i)) #队列里放数据
print("get detail url end")
if __name__ == "__main__":
detail_url_queue = Queue(maxsize=1000) #设置队列最大值
thread_detail_url = threading.Thread(target=get_detail_url, args=(detail_url_queue,))
for i in range(10):
html_thread = threading.Thread(target=get_detail_html, args=(detail_url_queue,))
html_thread.start()
start_time = time.time()
print("last time: {}".format(time.time()-start_time))
Queue类的几个函数介绍
full():判断队列是否已满
qsize(): 返回队列大小
empty(): 判断队列是否为空
join(): 使队列处于阻塞状态,只有接收到task_done()时,join()函数才会退出。所以这两个函数是成对出现的
四. 线程同步--Lock, RLock
1. Lock:给代码段加锁,此时程序只能执行锁中的代码。只有锁释放后,才能执行另外一段代码
改写第一节中GIL释放问题的代码,加上锁,结果就是0了
from threading import Lock
total = 0
lock = Lock()
def add():
global total
global lock
for i in range(1000000):
lock.acquire() #获取锁
total += 1
lock.release() #释放锁
def desc():
global total
global lock
for i in range(1000000):
lock.acquire()
#lock.acquire() 死锁情况1:连续2次使用lock.acquire(),就会造成死锁,程序一直不执行
total -= 1
lock.release()
import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
注意:
1)获取锁和释放锁都需要时间,所以锁会影响性能
2)锁会引起死锁,死锁情况2如下
A(a, b)
acquire(a) #需要先获得a,然后获得b
acquire(b)
B(a, b)
acquire(b) #需要先获得b, 然后获得a
acquire(a)
如果A(a, b)获得a的同时,B(a, b)获得了b,那么他们都在互相等待资源造成死锁
2. RLock: 可重入锁
同一个线程里面可调用多次acquire(), 解决某函数参数为函数,并且也有lock的情况
from threading import Lock, RLock
total = 0
lock = RLock()
def add():
global total
global lock
for i in range(1000000):
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()
def desc():
global total
global lock
for i in range(1000000):
lock.acquire()
total -= 1
lock.release()
import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
add函数中加锁和释放锁的次数要一样
四. 线程同步--condition使用以及源码分析
condition: 条件变量,用于复杂的线程间同步,比如模拟机器人对话
import threading
class XiaoAi(threading.Thread):
def __init__(self, cond):
super().__init__(name="小爱")
self.cond = cond
def run(self):
with self.cond:
self.cond.wait()
print("{} : 在 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 好啊 ".format(self.name))
self.cond.notify()
class TianMao(threading.Thread):
def __init__(self, cond):
super().__init__(name="天猫精灵")
self.cond = cond
def run(self):
with self.cond:
print("{} : 小爱同学 ".format(self.name))
self.cond.notify()
self.cond.wait()
print("{} : 我们来对古诗吧 ".format(self.name))
self.cond.notify()
self.cond.wait()
if __name__ == "__main__":
from concurrent import futures
cond = threading.Condition()
xiaoai = XiaoAi(cond)
tianmao = TianMao(cond)
#启动顺序很重要
#在调用with cond之后才能调用wait或者notify方法
#condition有两层锁, 一把底层锁会在线程调用了wait方法的时候释放, 上面的锁会在每次调用wait的时候分配一把并放入到cond的等待队列中,等到notify方法的唤醒
xiaoai.start()
tianmao.start()
输出
天猫精灵 : 小爱同学
小爱 : 在
天猫精灵 : 我们来对古诗吧
小爱 : 好啊
说明:
1)线程的启动顺序很重要,如果先执行tianmao.start(),再启动xiaoai.start(),程序就会卡在"天猫精灵:小爱同学"这里,原因是wait()必须要有notify()通知后才能响应。当tianmao线程启动发出notify时,xiaoai线程还没启动,所以里面的wait()一直不能得到响应
2)可以看一下queue的源码来理解conditon的应用
from queue import Queue