写在前面:
由于 Global Interpreter Lock(全局解释器锁) 的存在,Python 的多线程是无法实现多个线程并行,而是多个线程并发。这也就是 Python 多进程”鸡肋“的地方。关于Python GIL 的一些故事,可以到这篇文章了解一下。
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持,更推荐使用 threading。
- _thread:其是 Python3 为了兼容 Python2 时代的 thread 提供的,其仅包含低级别的、原始的线程以及一个简单的锁。功能比较有限的。
- threading:除了包含 _thread 的所有功能外。还提供了一套新的线程处理方案。更加推荐使用。
一、threading 的 Thread
threading 的 Thread 和 multiprocessing 的 Process 非常相似,其也提供了 run、start等等一些方法:
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
二、使用多线程去执行任务
import time
import threading
def worker(flag):
print("before sleep---%s" % flag)
time.sleep(3)
print("after sleep---%s" % flag)
if __name__ == '__main__':
p_one = threading.Thread(target=worker, args=("one",))
p_two = threading.Thread(target=worker, args=("two",))
p_one.start()
p_two.start()
输出结果:
before sleep—one
before sleep—two
after sleep—one
after sleep—two
三、线程锁
通过 threading.Lock() 来实现,在执行操作前通过 acquire() 加锁,结束后通过 release() 来释放锁。也可以使用 with 语句避免显示加锁。
import time
import threading
def worker_one(lock, flag):
# 显式加锁
lock.acquire()
print("one 加锁")
time.sleep(3)
print("one 释放")
# 释放锁
lock.release()
def worker_two(lock, flag):
# 通过 with 使用锁,不用手动释放
with lock:
print("two 加锁")
time.sleep(3)
print("two 释放")
if __name__ == '__main__':
# 声明锁
lock = threading.Lock()
p_one = threading.Thread(target=worker_one, args=(lock, 1))
p_two = threading.Thread(target=worker_two, args=(lock, 2))
p_one.start()
p_two.start()
输出结果:
one 加锁
… 等待一会
one 释放
two 加锁
… 等待一会
two 释放
四、信号量
可以通过 Semaphore 来实现对有限资源的使用控制,避免同同时启动太多进程跑崩系统。(可以实现类似线程池的概念)
import time
import threading
def worker_pool(semaphore, flag):
semaphore.acquire()
print("I am %d" % flag)
time.sleep(3)
semaphore.release()
if __name__ == '__main__':
semaphore = threading.Semaphore(2)
n = 0
while n < 6:
p = threading.Thread(target=worker_pool, args=(semaphore, n))
p.start()
n += 1
输出结果:每两行之间会间隔大约三秒
I am 0
I am 1
I am 2
I am 3
I am 4
I am 5
五、线程间通信
通过事件(Event)来实现线程间的通信。主要用于主线程控制其他线程的执行,有点类似 Golang 中的 channel。事件主要提供了四个方法 set、wait、clear、is_set。
常用方法
- .set() 设置事件为 True,用在控制器处。
- .wait(timeout=Nont) 等待事件为 True,用在监控状态执行处。其接受一个 int 型参数(超时事件,单位为秒)。
- .cleat() 将事件设置为False,所有 wait 态接收到 False 结束。
- .is_set() 检查事件状态,返回 True 或 False。
import time
import threading
def worker(e):
print("worker: starting\n")
e.wait()
print("worker: .is_set():%s\n" % e.is_set())
def worker_for_timeout(e):
print("worker_for_timeout:starting\n")
e.wait(2)
print("wait_for_event_timeout: .is_set():%s\n" % e.is_set())
if __name__ == "__main__":
e = threading.Event()
w1 = threading.Thread(target=worker, args=(e,))
w2 = threading.Thread(target=worker_for_timeout, args=(e,))
w1.start()
w2.start()
time.sleep(3)
# 在三秒后设置事件 所有等待两秒的或超时
e.set()
print("Main e.set()\n")
输出结果:
worker: starting
worker_for_timeout:starting
wait_for_event_timeout: .is_set():False
Main e.set()
worker: .is_set():True
六、队列
注意: 这里与进程有一个区别。在多进程中使用的是 multiprocessing.Queue()。多线程中使用queue.Queue()
Queue是多线程安全的队列,可以使用 Queue 实现多线程之间的数据传递。其通过 .put() 向队列中添加数据,.get() 方法从队列中取走数据。.put() 和 .get() 都接收两个参数 timeout、blocked,若 blocked 为 False 或 blocked 为 True 且等待 timeout 时间后,队列空一直没有成功从队列中取出数据 或 队列满一直没成功将数据加入到队列中,会抛出 Queue.Empty 或 Queue.Full 异常。
import queue
import threading
def process_push(q):
q.put(1, block=False)
def process_pull(q):
print(q.get(block=True, timeout=3))
if __name__ == "__main__":
q = queue.Queue()
reader = threading.Thread(target=process_pull, args=(q,))
reader.start()
writer = threading.Thread(target=process_push, args=(q,))
writer.start()
reader.join()
writer.join()
输出结果:
1
七、线程池
注意: 在 threading 中并没有 Pool 类的。但是惊奇的发现,在 multiprocessing.pool 中有一个 ThreadPool 类可以实现线程池。其和 multiprocessing.Pool 的使用方法是一样的。
ThreadPool 常用方法
- .apply() 阻塞式添加任务,任务顺序执行
- .apply_async() 非阻塞式添加任务,任务顺序执行
- .close() 关闭池,不可再想池中添加任务
- .join() 等待池中的所有任务执行完毕
- .terminate() 强制关闭池,所有进程结束
import time
from multiprocessing.pool import ThreadPool
def worker(flag):
print("before sleep---%s\n" % flag)
time.sleep(3)
print("after sleep---%s\n" % flag)
return flag
if __name__ == '__main__':
# 线程池
pool = ThreadPool(processes=3)
result = []
for i in range(4):
# 因为 GIL 的存在,池中的所有进程并发执行, 返回 ApplyResult 实例,可以通过 .get() 获取结果
# pool.apply_async(worker, (i,))
# 阻塞式添加,池中的进程顺序执行,返回任务的 return 值
# pool.apply(worker, (i,))
# 进程执行结束后会返回结果给主进程
result.append(pool.apply_async(worker, (i,)))
# 关闭池,不允许在添加
pool.close()
# 等待池中所有进程执行完毕
pool.join()
# 遍历所有结果集 pool.ApplyResult
for i in result:
print("结果集 %s\n" % i.get())
输出结果:
before sleep—0
before sleep—1
before sleep—2
after sleep—0
after sleep—1
after sleep—2
before sleep—3
after sleep—3
结果集 0
结果集 1
结果集 2
结果集 3
因为 GIL 的存在,对于计算密集型操作采用多线程不利于性能提高,而 IO 密集型使用多线程对性能有一定的提升。更具体的原因可以一下篇文章。