在前面《python多线程浅析》一文中,我比较笼统的写了关于多线程的一些东西,本文准备比较详细的介绍一下多线程,从最基础的创建多线程开始,探讨关于锁、通信机制、线程池等内容!
单线程、多线程、多进程的对比
首先,我们通过以下四个场景,来对比以下单线程、多线程、多进程处理能力的强弱!四个场景分别是CPU计算密集型、磁盘IO密集型、网络IO密集型、还有我们模拟的IO密集型。代码如下:
import requests
import time
from threading import Thread
from multiprocessing import Process
#CPU计算密集型
def count(x=1,y=1):
c = 0
while c < 50000000:
c += 1
x += 1
y += 1
#磁盘IO密集型
def io_disk():
with open("test.text","w") as f:
for x in range(500000):
f.write("iotest\n")
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
}
base_url = "https://www.tieba.com"
#网络IO密集型
def io_request():
try:
response = requests.get(url=base_url,headers=headers)
html = response.text
return
except Exception as e:
return {"error":e}
#模拟IO密集型
def io_simulation():
time.sleep(2)
#计算前后需要花费时间
def timer(mode):
def wrapper(func):
def deco(*args,**kwargs):
type = kwargs.setdefault('type',None)
t1 = time.time()
func(*args,**kwargs)
t2 = time.time()
costtime = t2 - t1
print("{}-{}花费时间:{}S".format(mode,type,costtime))
return deco
return wrapper
@timer('单线程')
def single_thread(func,type=""):
for i in range(10):
func()
single_thread(count,type="CPU计算密集型")
single_thread(io_disk,type="磁盘IO密集型")
single_thread(io_request,type="网络IO密集型")
single_thread(io_simulation,type="模拟IO密集型")
@timer("多线程")
def multi_thread(func,type=""):
threads = []
for i in range(10):
t = Thread(target=func,args=())
threads.append(t)
t.start()
lt = len(threads)
while True:
for td in threads:
if not td.is_alive():
lt -= 1
if lt <= 0:
break
multi_thread(count, type="CPU计算密集型")
multi_thread(io_disk, type="磁盘IO密集型")
multi_thread(io_request, type="网络IO密集型")
multi_thread(io_simulation, type="模拟IO密集型")
@timer("多进程")
def multi_process(func,type=""):
process_list = []
for x in range(10):
p = Process(target=func,args=())
process_list.append(p)
p.start()
lt = process_list.__len__()
while True:
for proc in process_list:
if not proc.is_alive():
lt -= 1
if lt <= 0:
break
multi_process(count, type="CPU计算密集型")
multi_process(io_disk, type="磁盘IO密集型")
multi_process(io_request, type="网络IO密集型")
multi_process(io_simulation, type="模拟IO密集型")
单线程-CPU计算密集型花费时间:57.472092151641846S
单线程-磁盘IO密集型花费时间:8.722064971923828S
单线程-网络IO密集型花费时间:0.576024055480957S
单线程-模拟IO密集型花费时间:20.030592918395996S
************************
多线程-CPU计算密集型花费时间:59.423059940338135S
多线程-磁盘IO密集型花费时间:13.935372114181519S
多线程-网络IO密集型花费时间:1.0296094417572021S
多线程-模拟IO密集型花费时间:2.0301761627197266S
************************
多进程-CPU计算密集型花费时间:20.213501930236816S
多进程-磁盘IO密集型花费时间:3.1664438247680664S
多进程-网络IO密集型花费时间:0.0712120532989502S
多进程-模拟IO密集型花费时间:2.0086209774017334S
首先是CPU计算密集型,多线程对比于单线程没有什么优势,反而时间增加了,这里面涉及到不断的添加和释放GIL全局变量锁的问题。而对于多进程,由于多个CPU同时工作,使得效率成倍增加。
在IO密集型中,包括磁盘IO、网络IO、数据库IO等都属于同一类。计算量很小,主要是IO等待时浪费时间。理论上来说,应该多线程更快的!但是实验的结果并不是我们想要的啊,这是为什么呢?这是因为我们的测试机是多核的!对于多核状态下的多线程,某个核释放GIL后,其他核的线程都会进行竞争,但是GIL只会被一个核拿到,那么就会导致其他核上被唤醒的线程醒着持续等待,造成线程颠簸,从而导致效率更低;而在单核的条件下,每次释放GIL唤醒的那个线程都能获得GIL锁,可以无缝执行。因此对于多核的情况,还是多进程能够有效的提高效率。
而对于我们模拟的IO密集型,利用sleep来迷你等待时间,是为了体现出多线程的优势。单线程每次需要每个线程睡2秒,10个线程就是20秒,但是在多线程的情况下,它们会切换睡觉,不会造成阻塞,这个时候,就算是10个,睡的时间也就比2秒多一点而已!
多线程的创建
在python3中,python提供了内置模块threading.Thread用于创建多线程。threading.Thread一般接收两个参数:
线程函数名:要放置线程让后台执行的函数,由我们自己定义,注意不要加括号;
线程函数的参数:线程函数名所需的参数,以元组的形式传入。若不需参数,可以不指定。
#使用函数创建多线程
import time
from threading import Thread
#自定义线程函数
def test(name="Test"):
for i in range(2):
print("hello",name)
time.sleep(1)
#创建线程1,不指定参数
thread1 = Thread(target=test)
thread1.start()
#创建线程2,指定参数
thread2 = Thread(target=test,args=("CHEN",))
thread2.start()
#hello Test
#hello CHEN
#hello CHEN
#hello Test
#使用类创建多线程,注意在用类创建多线程的时候,必须继承threading.Thread这个类;必须重写#run方法!
import time
from threading import Thread
class MyThread(Thread):
def __init__(self,name="Python"):
super().__init__()
self.name = name
def run(self):
for i in range(2):
print("hello",self.name)
time.sleep(1)
if __name__ == "__main__":
#创建线程1,不指定参数
thread1 = MyThread()
thread1.start()
#创建线程2,指定参数
thread2 = MyThread("CHEN")
thread2.start()
#hello Python
#hello CHEN
#hello Python
#hello CHEN
线程中常用的方法
t = MyThread()
#启动线程
t.start()
#阻塞子线程,待子线程结束后,再往下执行
t.join()
#判断线程是否在执行状态
t.is_alive()
t.isAlive()
# 设置线程是否随主线程退出而退出,默认为False,也就是设置线程为守护线程
t.daemon = True
#设置线程名
t.name = "name"
线程中的锁
如何使用锁?
import threading
#生成锁对象,全局唯一
lock = threading.Lock()
#获取锁,未获取到会阻塞程序,知道获取到锁才会往下执行
lock.acquire()
#释放锁,这个时候别的线程就可以用了
lock.release()
注意上面的代码,锁的获取和释放必须成对出现,否则可能造成死锁。
但是我们有的时候会忘记释放啊,这个时候,上下文管理器就派上用场了。
import threading
#生成锁对象,全局唯一
lock = threading.Lock()
with lock:
#这里写自己的代码
pass
with语句中会自己获取锁,在执行结束后自动释放锁。
为何使用锁?
既然锁的使用这么的复杂,我们为什么还要使用锁?我们来个例子。定义两个函数,分别在两个线程中执行,这个两个函数共用一个变量n。
import threading
def job1():
global n
for i in range(5):
n += 1
print('job1',n)
def job2():
global n
for i in range(5):
n += 10
print('job2',n)
n = 0
t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()
输出如下:
job1 1
job1 job22 12
job2 22
job2 32
job2 42
job2 52
job1 53
job1 54
job1 55
这个结果可以看到两个程序的结果相互影响,导致输出的结果看起来很乱,不过你也可以看到job1永远在前者的基础上+1,job2在前者的基础上+10。
如果我们给程序加上锁,它又会是什么样子呢?
import threading
lock = threading.Lock()
def job1():
global n,lock
lock.acquire()
for i in range(5):
n += 1
print('job1',n)
lock.release()
def job2():
global n,lock
lock.acquire()
for i in range(5):
n += 10
print('job2',n)
lock.release()
n = 0
t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()
输出如下:
job1 1
job1 2
job1 3
job1 4
job1 5
job2 15
job2 25
job2 35
job2 45
job2 55
加上锁之后,job1首先得到锁,在for循环中,没有人有权限对n进行操作。只有在job1操作完成之后,job2才拿到锁,开始自己的for循环。
可重入锁(RLock)
有时候在同一线程中,我们可能多次请求同一资源,也就是获取同一把锁,俗称锁嵌套。这个时候,如果外面那一层获取了锁,这个时候内层就不可能拿到锁了,从而造成死锁。
如何解决上面的问题呢?threading模块除了提供lock锁之外,还提供可重入锁RLock,专门处理这个问题。
import threading
def main():
n = 0
lock = threading.RLock()
with lock:
for i in range(6):
n += 1
with lock:
print(n)
t1 = threading.Thread(target=main)
t1.start()
防止死锁的加锁机制
死锁的情况会在很多时候出现,比如上面的嵌套获取同一把锁;或者多个线程,不按顺序同时获取多个锁。
那么怎么来解决这个问题呢?只要我们顺序获取就可以了呗,下面这个例子是通过一个辅助函数来对锁进行排序。
import threading
from contextlib import contextmanager
# Thread-local state to stored information on locks already acquired
_local = threading.local()
@contextmanager
def acquire(*locks):
# Sort locks by object identifier
locks = sorted(locks, key=lambda x: id(x))
# Make sure lock order of previously acquired locks is not violated
acquired = getattr(_local,'acquired',[])
if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
raise RuntimeError('Lock Order Violation')
# Acquire all of the locks
acquired.extend(locks)
_local.acquired = acquired
try:
for lock in locks:
lock.acquire()
yield
finally:
# Release locks in reverse order of acquisition
for lock in reversed(locks):
lock.release()
del acquired[-len(locks):]
x_lock = threading.Lock()
y_lock = threading.Lock()
def thread1():
while True:
with acquire(x_lock):
with acquire(y_lock):
print('thread1')
def thread2():
while True:
with acquire(y_lock):
with acquire(x_lock):
print('thread2')
t1 = threading.Thread(target=thread1)
t1.daemon = True
t1.start()
t2 = threading.Thread(target=thread2)
t2.daemon = True
t2.start()
看到没有,表面上thread_1的先获取锁x,再获取锁y,而thread_2是先获取锁y,再获取x。
但是实际上,acquire函数,已经对x,y两个锁进行了排序。所以thread_1,hread_2都是以同一顺序来获取锁的,是不是造成死锁的。
线程的通信机制
线程的通信方法大致有以下三种:事件、Condition、队列。下面我们详细的介绍一下这三种方式。
Event事件
python提供了最简单的通信机制就是Threading.Event,是通用的条件变量。多个线程可以等待某个事件的发生,在事件发生后,所有线程都会被激活。
关于Event的使用就是下面的三个函数。
event = threading.Event()
# 重置event,使得所有该event事件都处于待命状态
event.clear()
# 等待接收event的指令,决定是否阻塞程序执行
event.wait()
# 发送event指令,使所有设置该event事件的线程执行
event.set()
举个栗子!
import time
import threading
class MyThread(threading.Thread):
def __init__(self, name, event):
super().__init__()
self.name = name
self.event = event
def run(self):
print('Thread: {} start at {}'.format(self.name, time.ctime(time.time())))
# 等待event.set()后,才能往下执行
self.event.wait()
print('Thread: {} finish at {}'.format(self.name, time.ctime(time.time())))
threads = []
event = threading.Event()
# 定义五个线程
[threads.append(MyThread(str(i), event)) for i in range(1,5)]
# 重置event,使得event.wait()起到阻塞作用
event.clear()
# 启动所有线程
[t.start() for t in threads]
print('等待5s...')
time.sleep(5)
print('唤醒所有线程...')
event.set()
执行结果如下!
Thread: 1 start at Mon Jul 1 19:01:28 2019
Thread: 2 start at Mon Jul 1 19:01:28 2019
Thread: 3 start at Mon Jul 1 19:01:28 2019
Thread: 4 start at Mon Jul 1 19:01:28 2019
等待5s...
唤醒所有线程...
Thread: 1 finish at Mon Jul 1 19:01:33 2019
Thread: 4 finish at Mon Jul 1 19:01:33 2019
Thread: 2 finish at Mon Jul 1 19:01:33 2019
Thread: 3 finish at Mon Jul 1 19:01:33 2019
可见在所有线程都启动(start())后,并不会执行完,而是都在self.event.wait()止住了,需要我们通过event.set()来给所有线程发送执行指令才能往下执行。
Condition
Condition和Event 是类似的,并没有多大区别。同样,Condition也只需要掌握几个函数即可。
cond = threading.Condition()
# 类似lock.acquire()
cond.acquire()
# 类似lock.release()
cond.release()
# 等待指定触发,同时会释放对锁的获取,直到被notify才重新占有琐。
cond.wait()
# 发送指定,触发执行
cond.notify()
举个网上一个比较趣的捉迷藏的例子来看看。
import threading, time
class Hider(threading.Thread):
def __init__(self, cond, name):
super(Hider, self).__init__()
self.cond = cond
self.name = name
def run(self):
time.sleep(1) #确保先运行Seeker中的方法
self.cond.acquire()
print(self.name + ': 我已经把眼睛蒙上了')
self.cond.notify()
self.cond.wait()
print(self.name + ': 我找到你了哦 ~_~')
self.cond.notify()
self.cond.release()
print(self.name + ': 我赢了')
class Seeker(threading.Thread):
def __init__(self, cond, name):
super(Seeker, self).__init__()
self.cond = cond
self.name = name
def run(self):
self.cond.acquire()
self.cond.wait()
print(self.name + ': 我已经藏好了,你快来找我吧')
self.cond.notify()
self.cond.wait()
self.cond.release()
print(self.name + ': 被你找到了,哎~~~')
cond = threading.Condition()
seeker = Seeker(cond, 'seeker')
hider = Hider(cond, 'hider')
seeker.start()
hider.start()
通过cond来通信,阻塞自己,并使对方执行。从而,达到有顺序的执行。结果如下。
hider: 我已经把眼睛蒙上了
seeker: 我已经藏好了,你快来找我吧
hider: 我找到你了 ~_~
hider: 我赢了
seeker: 被你找到了,哎~~~
Queue队列
从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用put() 和 get() 操作来向队列中添加或者删除元素。
同样,对于Queue,我们也只需要掌握几个函数即可。
from queue import Queue
# maxsize默认为0,不受限
# 一旦>0,而消息数又达到限制,q.put()也将阻塞
q = Queue(maxsize=0)
# 阻塞程序,等待队列消息。
q.get()
# 获取消息,设置超时时间
q.get(timeout=5.0)
# 发送消息
q.put()
# 等待所有的消息都被消费完
q.join()
# 以下三个方法,知道就好,代码中不要使用
# 查询当前队列的消息个数
q.qsize()
# 队列消息是否都被消费完,True/False
q.empty()
# 检测队列里消息是否已满
q.full()
函数会比之前的多一些,同时也从另一方面说明了其功能更加丰富。我们来举个老师点名的例子。
from queue import Queue
from threading import Thread
import time
class Student(Thread):
def __init__(self, name, queue):
super().__init__()
self.name = name
self.queue = queue
def run(self):
while True:
# 阻塞程序,时刻监听老师,接收消息
msg = self.queue.get()
# 一旦发现点到自己名字,就赶紧答到
if msg == self.name:
print("{}:到!".format(self.name))
class Teacher:
def __init__(self, queue):
self.queue=queue
def call(self, student_name):
print("老师:{}来了没?".format(student_name))
# 发送消息,要点谁的名
self.queue.put(student_name)
queue = Queue()
teacher = Teacher(queue=queue)
s1 = Student(name="小明", queue=queue)
s2 = Student(name="小亮", queue=queue)
s1.start()
s2.start()
print('开始点名~')
teacher.call('小明')
time.sleep(1)
teacher.call('小亮')
运行结果如下。
开始点名~
老师:小明来了没?
小明:到!
老师:小亮来了没?
小亮:到!
通过学习上面三种通信方法,我们很容易就能发现Event 和 Condition 是threading模块原生提供的模块,原理简单,功能单一,它能发送 True 和 False 的指令,所以只能适用于某些简单的场景中。
而Queue则是比较高级的模块,它可能发送任何类型的消息,包括字符串、字典等。其内部实现其实也引用了Condition模块(譬如put和get函数的阻塞),正是其对Condition进行了功能扩展,所以功能更加丰富,更能满足实际应用。
消息队列除了上面说的之外,还有另外两个类,分别是queue.LifoQueue和queue.PriorityQueue。这三个类的区别如下:
queue.Queue:先进先出队列
queue.LifoQueue:后进先出队列
queue.PriorityQueue:优先级队列
threading.local类
为什么单独列出这个类呢?主要是有的时候我们需要将同一class对象创建出的同一实例,但是有多个线程,这个时候要隔离不同线程之间的数据呢?这个时候就要用到threading.local这个类了。
下面这个例子是从不用的页面获取数据,我们来计算这个页面的数据量。
import threading
from functools import partial
from socket import socket, AF_INET, SOCK_STREAM
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = AF_INET
self.type = SOCK_STREAM
self.local = threading.local()
def __enter__(self):
if hasattr(self.local, 'sock'):
raise RuntimeError('Already connected')
# 把socket连接存入local中
self.local.sock = socket(self.family, self.type)
self.local.sock.connect(self.address)
return self.local.sock
def __exit__(self, exc_ty, exc_val, tb):
self.local.sock.close()
del self.local.sock
def spider(conn, website):
with conn as s:
header = 'GET / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n'.format(website)
s.send(header.encode("utf-8"))
resp = b''.join(iter(partial(s.recv, 100000), b''))
print('Got {} bytes'.format(len(resp)))
if __name__ == '__main__':
# 建立一个TCP连接
conn = LazyConnection(('www.sina.com.cn', 80))
# 爬取两个页面
t1 = threading.Thread(target=spider, args=(conn,"news.sina.com.cn"))
t2 = threading.Thread(target=spider, args=(conn,"blog.sina.com.cn"))
t1.start()
t2.start()
t1.join()
t2.join()
如何创建线程池
由于在切换线程的时候,需要切换上下文环境,依然会造成cpu的大量开销。为解决这个问题,线程池的概念被提出来了。预先创建好一个较为优化的数量的线程,让过来的任务立刻能够使用,就形成了线程池。
在Python3中,创建线程池是通过concurrent.futures函数库中的ThreadPoolExecutor类来实现的。
import time
import threading
from concurrent.futures import ThreadPoolExecutor
def target():
for i in range(5):
print('running thread-{}:{}'.format(threading.get_ident(), i))
time.sleep(1)
#: 生成线程池最大线程为5个
pool = ThreadPoolExecutor(5)
for i in range(100):
pool.submit(target) # 往线程中提交,并运行
除了使用上述第三方模块的方法之外,我们还可以自己结合前面所学的消息队列来自定义线程池。这里我们就使用queue来实现一个上面同样效果的例子。
import time
import threading
from queue import Queue
def target(q):
while True:
msg = q.get()
for i in range(5):
print('running thread-{}:{}'.format(threading.get_ident(), i))
time.sleep(1)
def pool(workers,queue):
for n in range(workers):
t = threading.Thread(target=target, args=(queue,))
t.daemon = True
t.start()
queue = Queue()
# 创建一个线程池:并设置线程数为5
pool(5, queue)
for i in range(100):
queue.put("start")
# 消息都被消费才能结束
queue.join()