这个问题当时是在一个群里看到的,Python到底有没有多线程。群里甚至还有两个人因为这个玩意吵起来,要现实干仗了。233333333
然后我就把那个乌烟瘴气的群退了。由此学习记录一下python中的多线程和多进程。
1.Python中的GIL锁
首先,要谈多进程,就要说一下这个锁的问题。
GIL锁全名是,全局解释器锁(Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行,即便是在多核心处理器上,使用GIL的解释器也只允许同一时间执行一个线程。常见的使用GIL的解释器有CPython和Ruby MRI。
所以,解释的通俗一点,就是即时你开了多线程,但是如果执行CPU密集型的任务去请求CPU运算时,还是要获得这个GIL锁才能请求到CPU,但是这个GIL锁,只有一把,因此每次都是只有一个线程能请求到这个锁,然后使用CPU执行相应任务,这个锁的获取,是通过时间片轮转算法来获得的,所以,说到底,这玩意还是一个一个的执行,和单线程裸奔无异,但是前提是请求CPU执行任务,即CPU密集型任务。看一下这个经典的图。
我可能说的不够好,知乎中有一篇文章也有相关介绍,下面是链接。
https://zhuanlan.zhihu.com/p/97218985
2.代码的测试
单线程裸奔
import time
# 单线程裸奔
def start():
i = 0
for i in range(10000000):
i += 1
return
def main():
s = time.time()
for i in range(10):
start()
print(time.time() - s)
if __name__ == '__main__':
main()
多线程执行
import time
import threading
# 多线程执行
def start():
i = 0
for i in range(10000000):
i += 1
return
def main():
s = time.time()
ts = {}
for i in range(10):
t = threading.Thread(target=start)
t.start()
ts[i] = t # 线程存储在字典中
for i in range(10):
ts[i].join() # 执行完毕后阻塞
print(time.time() - s)
if __name__ == '__main__':
main()
效果并没有很好,造成这种情况的原因就是GIL锁,由于存在这个GIL锁,所以在CPU密集型任务上,消耗的时间反而会更多IO密集型则可以达到较好的效果,因为不会用到CPU做很多运算,所以就不会收到GIL锁的干扰
要是想更好的解决,就是用多进程,而不是多线程,每个进程都有自己独立的GIL,不会出现进程之间GIL的争抢
多进程的创建和销毁开销会很大,成本会更高,而且进程之间无法看到对方的数据,需要使用栈或者队列进行获取,从而提升编程复杂度
3.补充有关线程的几个小点
join()方法
import threading
import time
def start():
time.sleep(5)
print(threading.current_thread().name) # 线程名
print(threading.current_thread().isAlive()) # 线程是否存活
print(threading.current_thread().ident)# 线程ID
print('start')
# 注意,这里的参数是方法名,start,而不是start(),加了()就会执行函数,而不是传参,
t =threading.Thread(target=start,name='number one')
t.start()
print('stop')
出现这种情况的原因是,定义的线程为非守护线程,线程的执行并不会随着主线程结束而结束。
使用join()方法
import threading
import time
def start():
time.sleep(5)
print(threading.current_thread().name) # 线程名
print(threading.current_thread().isAlive()) # 线程是否存活
print(threading.current_thread().ident)# 线程ID
print('start')
# 注意,这里的参数是方法名,start,而不是start(),加了()就会执行函数,而不是传参,
t =threading.Thread(target=start,name='number one')
t.start()
t.join() # join()方法,作用是阻塞,等待子线程结束,join方法有一个参数是timeout,即如果主线程等待timeout,
# 子线程还没有结束,则主线程强制结束子线程。
print('stop')
守护线程
# 守护线程,会伴随主线程一起结束
# setDaemon设置为True即可,但是如果守护线程使用了 join方法,还是会等线程执行完毕,再执行主线程
import threading
import time
def start():
time.sleep(5)
print(threading.current_thread().name) # 线程名
print(threading.current_thread().isAlive()) # 线程是否存活
print(threading.current_thread().ident)# 线程ID
print('start')
# 注意,这里的参数是方法名,start,而不是start(),加了()就会执行函数,而不是传参,
t =threading.Thread(target=start,name='number one')
t.setDaemon(True) # 设置为守护线程,主线程结束,守护线程无论执行完毕与否,都会结束
t.start()
print('stop')
有关线程对共享资源的读取问题
import threading
number = 0
def addNumber():
global number
for i in range(1000000):
number += 1
def downNumber():
global number
for i in range(1000000):
number -= 1
print("start")
t = threading.Thread(target=addNumber)
t2 = threading.Thread(target=downNumber)
t.start()
t2.start()
t.join()
t2.join()
print(number)
print("stop")
按照常理来说,最后这个number应该是等于0,但是却出现了这种情况,原因这里解释下,这个原因的解释也是来源于上面知乎中的文章,那篇文章中的解释比较清楚。
上面的程序对n做了同样数量的加法和减法,那么n理论上是0。但运行程序,打印n,发现它不是0。问题出在哪里呢,问题在于python的每行代码不是原子化的操作。比如n = n+1这步,不是一次性执行的。如果去查看python编译后的字节码执行过程,可以看到如下结果。
19 LOAD_GLOBAL 1 (n)
22 LOAD_CONST 3 (1)
25 BINARY_ADD
26 STORE_GLOBAL 1 (n)
从过程可以看出,n = n +1 操作分成了四步完成。因此,n = n+1不是一个原子化操作。
1.加载全局变量n,2.加载常数1,3.进行二进制加法运算,4.将运算结果存入变量n。
根据前面的线程释放GIL锁原则,线程a执行这四步的过程中,有可能会让出GIL。如果这样,n=n+1的运算过程就被打乱了。最后的结果中,得到一个非零的n也就不足为奇。
解决方法:加锁,在对共享资源访问时进行加锁。
# 解决方法,加锁
import threading
lock = threading.Lock()
number = 0
def addNumber():
global number
for i in range(1000000):
lock.acquire()
number += 1
lock.release()
def downNumber():
global number
for i in range(1000000):
lock.acquire()
number -= 1
lock.release()
print("start")
t = threading.Thread(target=addNumber)
t2 = threading.Thread(target=downNumber)
t.start()
t2.start()
t.join()
t2.join()
print(number)
print("stop")
一种控制锁粒度的递归锁
# 递归锁,为了将锁的粒度控制的更小,更精准,需要使用递归锁
# 缺点:很慢
import time
import threading
class Test:
rlock = threading.RLock()
def __init__(self):
self.number = 0
def add(self):
with Test.rlock: # 这里加了一把锁,执行execute方法,执行后释放
self.execute(1)
def down(self):
with Test.rlock:
self.execute(-1)
def execute(self,n):
# with关键字的使用与打开文件的功能类似,实现自开合效果,
# 会自动的加锁和释放
with Test.rlock: # 这里又加了一把锁,等到执行完加法之后释放
self.number += n
def add(test):
for i in range(10000000):
test.add()
def down(test):
for i in range(10000000):
test.down()
if __name__ == '__main__':
test = Test()
# args传递方法执行所需要的参数
t1 = threading.Thread(target=add,args=(test,))
t2 = threading.Thread(target=down,args=(test,))
t1.start()
t2.start()
t1.join()
t2.join()
print(test.number)
这个会运行好久,因为递归锁很慢。。
线程池
# 线程池的包
import time
import threadpool
# 执行较为耗时的函数,需要开启多线程
def get_html(url):
time.sleep(3)
print(url)
# 使用多线程执行telnet函数
urls = [i for i in range(10)]
# 建立线程池
pool = threadpool.ThreadPool(10)
# 提交任务给线程池
requests = threadpool.makeRequests(get_html,urls)
# 开始执行任务
for req in requests:
pool.putRequest(req)
pool.wait()
4.多进程
多进程则与多线程不同了,多进程在进行CPU密集型计算时,并不会出现GIL锁问题。多个进程并发执行,而且在进程中可以开启线程。
但缺点就是:进程是资源分配的基本单位,因此多进程开启会比较慢。
import multiprocessing
import time
"""
多进程
"""
def start(i):
time.sleep(1)
print(i)
print(multiprocessing.current_process().name)
print(multiprocessing.current_process().pid)
print(multiprocessing.current_process().is_alive())
if __name__ == '__main__':
print("start")
p = multiprocessing.Process(target=start,args=(1,),name="Mul process")
p.start()
# p.join()
print("stop")
多进程中的操作,与多线程类似。
进程通信
# 进程通信
# Python多进程之间默认是无法通信的,因为是并发执行的,所以需要借助其他数据结构
# 这里借助队列,一个往队列中写,一个从队列中读,实现消息队列
from multiprocessing import Process,Queue
def write(q):
print("Process to write : %s" % Process.pid)
for i in range(100):
print("Put %d to queue..." % i)
q.put(i)
def read(q):
print("Process to read : %s" % Process.pid)
while True:
value = q.get()
print("Get %d from queue." % value)
if __name__ == '__main__':
# 父进程创建Queue,并传递给各个子进程
q = Queue()
pw = Process(target=write,args=(q,))
pr = Process(target=read,args=(q,))
pw.start()
pr.start()
进程池
使用multiprocessing模块实现进程池操作
# 进程池
import multiprocessing
def function_square(data):
result = data * data
return result
if __name__ == '__main__':
inputs = list(range(100))
pool = multiprocessing.Pool(processes=4) # 大小为4
# map把任务交给进程池处理
pool_outputs = pool.map(function_square,inputs)
# apply每次提交一个任务
# pool_outputs = pool.map(function_square,args=(10,))
pool.close()
pool.join()
print("Pool :",pool_outputs)
5.concurrent.futures模块中,对三种执行方式的对比
# 单线程裸奔执行CPU密集型任务
import concurrent.futures
import time
number_list = [1,2,3,4,5,6,7,8,9,10]
def evaluate_item(x):
result = count(x)
return result
def count(number):
for i in range(0,10000000):
i+=1
return number
if __name__ == '__main__':
# 单线程裸奔
s1 = time.time()
for item in number_list:
print(evaluate_item(item))
print("单线程裸奔:",time.time() - s1)
# 多线程,线程池执行
s2 = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(evaluate_item,item) for item in number_list]
for future in concurrent.futures.as_completed(futures):
print(future.result())
print("多线程执行:",time.time() - s2)
# 多进程,进程池执行
s3 = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(evaluate_item, item) for item in number_list]
for future in concurrent.futures.as_completed(futures):
print(future.result())
print("多进程执行:", time.time() - s3)