问题:
Python多线程为什么耗时更长?
现在的PC都是多核的,使用多线程能充分利用CPU来提供程序的执行效率。
线程是一个基本的CPU执行单元。它必须依托于进程存活。一个线程是一个execution context(执行上下文) ,即一个CPU执行时所需要的一串指令。
进程是指一个程序在给定数据集合上的一次执行过程,是系统进行资源分配和运行调用的独立单位。可以简单地理解为操作系统中正在执行的程序。也就是说,每个应用程序都有一个自己的进程。
每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。
线程因作用可以划分为不同的类型。大致可分为:
对于其他语言,CPU是多核时是支持多个线程同时执行。但在Python中,无论是单核还是多核,同时只能由一个线程在执行。其根源是GIL的存在。
GIL的全称是GLobal Interpreter Lock(全局解释器锁),来源是Python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。
而目前Python的解释器有多种,例如:
GIL只在CPython中才有,而在PyPy和Jython中是没有GIL的。
每次释放GIL锁,线程进行锁竞争、切换线程,消耗资源。这就导致打印线程执行时长,会发现耗时更长的原因。
并且由于GIL锁存在,Python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,Python的多线程效率并不高的根本原因。
Python提供两个模块进行多线程的操作,分别是thread
和threading
,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。
threading.Thread()
import threading
def run(n):
print(f'current tast: {n}')
if __name__ == '__main__':
t1 = threading.Thread(target=run, args=("thread 1", ))
t2 = threading.Thread(target=run, args=("thread 2", ))
t1.start()
t2.start()
threading.Thread
来自定义线程类,重写run
方法import threading
class MyThread(threading.Thread):
def __init__(self, n):
super(MyThread, self).__init__() # 重构run函数必须要重写
self.n = n
def run(self):
print(f"current task: {self.n}")
if __name__ == '__main__':
t3 = MyThread("thread 3")
t4 = MyThread("thread 4")
t3.start()
t4.start()
Join
函数执行顺序是逐个执行每个线程,执行完毕后继续往下执行。主线程结束后,子线程还在运行,Join
函数使得主线程等到子线程结束时才退出。
import threading
def count(n):
while n > 0:
print(f'current {n}')
n -= 1
if __name__ == "__main__":
print('start')
t1 = threading.Thread(target=count, args=(100000, ))
t2 = threading.Thread(target=count, args=(100000, ))
t1.start()
t2.start()
# 将t1和t2加入到主线程中
t1.join()
t2.join()
print('stop')
线程之间数据共享的。当多个线程对某一个共享数据进行操作时,就需要考虑到线程安全问题。threading
模块中定义了Lock
类,提供了互斥锁的功能来保证多线程情况下数据的正确性。
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire([timeout])
# 释放
mutex.release()
其中,锁定方法acquire
可以有一个超时时间的可选参数timeout
。如果设定了timeout
,则在超时后通过返回值可以判断是否得到了锁,从而可以进行一些其他的处理。
具体用法如下:
import threading
import time
num = 0
mutex = threading.Lock()
class MyThread(threading.Thread):
def run(self):
global num
time.sleep(1)
if mutex.acquire(1):
num = num + 1
msg = self.name + ': num value is ' +str(num)
print(msg)
mutex.release()
if __name__ == "__main__":
for i in range(5):
t = MyThread()
t.start()
为了满足在同一线程中多次请求同一资源的需求,Python提供了可重入锁(RLock)。RLock
内部维护着一个Lock
和一个counter
变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
具体用法如下:
# 创建 RLock
mutex = threading.RLock()
class MyThread(threading.Thread):
def run(self):
if mutex.acquire(1):
print("thread " + self.name + "get mutex")
time.sleep(1)
mutex.acquire()
mutex.release()
mutex.release()
如果希望主线程执行完毕之后,不管子线程是否执行完毕都随着主线程一起结束。我们可以使用setDeamon(bool)
函数,它跟join
函数是相反的。它的作用是设置子线程是否随主线程一起结束,必须在start()
之前调用,默认为False
。
如果需要规定函数在多少秒后执行某个操作,需要用到Timer
类。具体用法如下:
from threading import Timer
def show():
print("Python")
# 指定一秒钟之后执行 show 函数
t = Timer(1, show)
t.start()
Python要进行多进程操作,需要用到muiltprocessing
库,其中的Process
类跟threading
模块的Thread
类很相似。所以直接看代码熟悉多进程。
Process
,代码如下:import time
from multiprocessing import Process
def show(name):
print(f"Process name is {name}")
if __name__ == "__main__":
proc = Process(target=show, args=('subprocess',))
proc.start()
proc.join()
Process
来自定义进程类,重写run
方法,代码如下:mport time
from multiprocessing import Process
def show(name):
print(f"Process name is {name}")
class MyProcess(Process):
def __init__(self, name):
super(MyProcess, self).__init__()
self.name = str(name)
def run(self):
print(f"process name : {self.name}")
time.sleep(1)
if __name__ == "__main__":
# proc = Process(target=show, args=('subprocess',))
# proc.start()
# proc.join()
for i in range(3):
p = MyProcess(i)
p.start()
for i in range(3):
p.join()
进程之间不共享数据的。如果进程之间需要进行通信,则要用到Queue
模块或者Pipe
模块来实现。
Queue是多进程安全的队列,可以实现多进程之间的数据传递。它主要有两个函数,put
和get
。
put()用以插入数据到队列中,put还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
get()可以从队列读取并删除一个元素。同样,get有两个可选参数:blocked和timeout。如果blocked为True(默认值),那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常。
具体用法如下:
from multiprocessing import Process, Queue
def put(queue):
queue.put('Queue 用法')
if __name__ == "__main__":
queue = Queue()
pro = Process(target=put, args=(queue,))
pro.start()
print(queue.get())
pro.join()
Pipe的本质是进程之间的用管道数据传递,而不是数据共享,这和socket有点像。pipe() 返回两个连接对象分别表示管道的两端,每端都有send() 和recv()函数。
如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据。
具体用法如下:
from multiprocessing import Process, Pipe
def show(conn):
conn.send('Pipe 用法')
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
pro = Process(target=show, args=(child_conn,))
pro.start()
print(parent_conn.recv())
pro.join()
创建多个进程,我们不用傻傻地一个个去创建。我们可以使用Pool
模块来搞定。
Pool 常用的方法如下:
方法 | 含义 |
---|---|
apply() | 同步执行(串行) |
apply_async() | 异步执行(并行) |
terminate() | 立刻关闭进程池 |
join() | 主进程等待所有子进程执行完毕。必须在close或terminate()后使用 |
close() | 等待所有进程结束后,才关闭进程池 |
from multiprocessing import Pool
def show(num):
print('num : ' + str(num))
if __name__=="__main__":
pool = Pool(processes = 3)
for i in xrange(6):
# 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
pool.apply_async(show, args=(i, ))
print('====== apply_async ======')
pool.close()
# 调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
pool.join()
在这个问题上,首先要看下你的程序是属于哪种类型的。一般分为两种CPU密集型和I/O密集型。
- CPU密集型:程序比较偏重于计算,需要经常使用CPU来运算。例如科学计算的程序,机器学习的程序等。
- I/O密集型:顾名思义就是程序需要频繁进行输入输出操作。爬虫程序就是典型的I/O密集型程序。
如果程序是属于CPU密集型,建议使用多进程。而多线程更适合应用于I/O密集型程序。