一、线程的创建
原理:线程抢夺cpu时间片,谁抢到了谁就执行,在python中,当一个线程运行100个字节后,会自动释放时间片。重新抢夺。
- 启动多个线程(函数方式)
- 在Python3中,Python提供了一个内置模块 threading.Thread,可以很方便地让我们创建多线程。 一般接收两个参数
- 线程函数名:要放置线程让其后台执行的函数,由我们自已定义,注意不要加();
- 线程函数的参数:线程函数名所需的参数,以元组的形式传入。若不需要参数,可以不指定。
import threading
import time
def run(n):
print('task{}'.format(n))
time.sleep(2)
t1 = threading.Thread(target= run,args = (1,))
t2 = threading.Thread(target = run,args = (2,))
t1.start()
t2.start()
- 这样我们会发现,两个线程一起执行,同时得出结果。
- 启动多个线程(类方式)
class MyThread(threading.Thread):
def __init__(self,name):
super(MyThread,self).__init__()
self.name = name
def run(self):
print('task',self.name)
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.start()
t2.start()
重写了init方法,所以只需要传一个参数,默认运行run方法里的内容
二、互斥锁
为什么要锁?
- 当多个线程对同一个属性进行操作的时候,就会出现线程安全问题,这个时候就需要互斥锁。锁的是代码块,只有拿到锁的线程才能执行。
- 被同一个锁锁住的代码块,不管有多少,在同一时刻都只能执行一个,因为同样的锁在全局中只有一个
创建锁的两种方式:
import threading
# 生成锁对象,全局唯一
lock = threading.Lock()
# 获取锁。未获取到会阻塞程序,直到获取到锁才会往下执行
lock.acquire()
# 释放锁,归回倘,其他人可以拿去用了
lock.release()
- 需要注意的是,lock.acquire() 和 lock.release()必须成对出现。否则就有可能造成死锁。
import threading
lock = threading.Lock()
with lock:
# 这里写自己的代码
pass
- with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。
获取一把锁
lock = threading.Lock()
lock1 = threading.Lock()
lock2 = threading.Lock()
class Test:
def fun1(self):
with lock1:
print('fun1')
time.sleep(1)
def fun2(self):
with lock2:
print('fun2')
t = Test()
def run1():
t.fun1()
print(11111)
def run2():
t.fun2()
t.fun1()
print(22222)
t1 = threading.Thread(target=run1)
t2 = threading.Thread(target=run2)
t1.start()
t2.start()
# 输出结果
# fun1
# 11111fun2
#
# fun1
# 22222
- 锁必须配套使用,两个被同一把锁锁住的方法同时只能有一个执行,两个被不同锁锁住的方法可以同时执行。当有一个线程执行fun1、时,拿走了lock1,第二个线程想要执行,就会寻找lock1锁,当线程执行完with代码块时,第一个线程会自动返回这个锁。每一个被创建的锁都只有一把,拿到的线程才能执行对应的代码块,拿不到的则不能执行。
- 把fun2的lock2改成lock1,那么当t1线程执行fun1中的代码块时,拿走lock1,当t2执行fun2中的代码块时,发现也需要这个锁,但是已经被t1拿走了,所以只能等待t1返还这个锁才能拿到锁执行代码。
- 锁一旦被创建,全局就只有一把,谁拿到谁就能执行被这个锁锁住的代码块。
三、GIL(全局锁)
多线程实际上只能由一个线程执行,就算cpu是多核的也只能运行一个核运行,这是因为有GIL(全局锁)
什么是GIL呢?
任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
四、线程通信
从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用put() 和 get() 操作来向队列中添加或者删除元素。
原理图:
生产者消费者模式
from queue import Queue
from time import sleep
from random import randrange
import threading
q = Queue(maxsize=10)
def producer():
for i in range(0,10):
product = '生产者生产产品' + str(i)
print(product)
q.put(product)
sleep(randrange(3))
def consumer():
for i in range(0,10):
product = '消费者消费产品' + str(i)
print(product)
q.put(product)
sleep(randrange(5))
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
- 也可以实例化多个消费者
四、线程池
线程池原理:
线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致 Python 解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。
future对象:在未来的某一时刻完成操作的对象. submit方法可以返回一个future对象.
from concurrent.futures import ThreadPoolExecutor
def run(a,b):
print('{}+{}={}\t{}'.format(a,b,a + b,threading.currentThread().ident))
sleep(b)
return a + b
ex = ThreadPoolExecutor(max_workers=3)#创建一个线程池
f1 = ex.submit(run,2,3)# 将run带参数2,3放入线程池运行
f2 = ex.submit(run,2,2)# 将run带参数2,2放入线程池运行
print(f1.done())# 检测线程f1是否结束
print(f1.result())# 获取线程f1的返回值
# 输出结果
# 2+3=5 1352
# 2+2=4 12228
# False
# 5
- 线程池的map函数
语法:
线程池对象.map(函数(线程),可迭代对象)
作用:
将可迭代对象里的值每个都放入线程中执行,返回的值顺序与放入的顺序一致
list1 = ['aaa','bbb','ccc','ddd','eee','fff']
def run(a):
print(a[0])
return a[0]
ex = ThreadPoolExecutor(max_workers=3)
result = ex.map(run,list1)
sleep(1)
print('map的返回值')
for i in result:
print(i,end=' ')
- as_completed()函数
- 线程池可以同时处理多个线程,每个线程都会返回一个future对象,若我们要挨个对这些对象操作就会显得很繁琐,as_completed()就可以吧这些对象全部存在一个序列
import concurrent.futures
def run(a,b):
return a + b
ex = concurrent.futures.ThreadPoolExecutor(max_workers=3)
f1 = ex.submit(run,1,2)
f2 = ex.submit(run,2,3)
complete = concurrent.futures.as_completed([f1,f2])
for i in complete:
print(i.result())
# 输出结果
# 3
# 5
- 线程池的wait()
wait 方法可以让主线程阻塞,直到满足设定的要求。wait 方法接收3个参数,等待的任务序列、超时时间以及等待条件。等待条件 reture_when 默认为 ALL_COMPLETED,表明要等待所有的任务都结束。可以看到运行结果中,确实是所有任务都完成了,主线程才打印出 main。等待条件还可以设置为 FIRST_COMPLETED,表示第一个任务完成就停止等待,默认是等待全部任务完成。
glo = 0
def run(n):
global glo
glo += 1
print('第{}个完成的线程'.format(glo))
sleep(n)
ex = concurrent.futures.ThreadPoolExecutor(max_workers=3)
task = [ex.submit(run,n) for n in range(1,3)]
concurrent.futures.wait(task)
print('所有线程执行完毕')
最后说一下回调:add_done_callback(fn) , 回调函数是在调用线程完成后再调用的,在同一个线程中.
def run():
print('11111111')
print('222222222')
print('tid:',threading.currentThread().ident)
def call_back(obj):
print('->>>>>>>>>call_back , tid:',threading.currentThread().ident, ',obj:',obj)
ex = concurrent.futures.ThreadPoolExecutor(max_workers=3)
f1 = ex.submit(run)
f1.add_done_callback(call_back)
# 输出结果
# 11111111
# 222222222
# tid: 5372
# ->>>>>>>>>call_back , tid: 10992 ,obj: <Future at 0x2897b6ec820 state=finished returned NoneType>