线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
在 Python 中我们要同时执行多个任务怎么办?
有两种解决方案:
- 启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
- 启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
- 启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。
总结一下就是,多任务的实现有 3 种方式: - 多进程模式;
- 多线程模式;
- 多进程+多线程模式。
多线程
Python 的标准库提供了两个模块:_thread 和 threading,_thread 是低级模块, threading 是高级模块,对 _thread 进行了封装。绝大多数情况下,我们只需要使用 threading 这个高级模块。
import time, threading
# 新线程执行的代码:
def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
结果:
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
Python 的 threading 模块有个 current_thread() 函数,它永远返回当前线程的实例。主线程实例的名字叫 MainThread ,子线程的名字在创建时指定,我们用 LoopThread 命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字 Python 就自动给线程命名为 Thread-1,Thread-2……
线程同步与互斥锁
使用线程加载获取数据,通常都会造成数据不同步的情况。当然,这时候我们可以给资源进行加锁,也就是访问资源的线程需要获得锁才能访问。
其中 threading 模块给我们提供了一个 Lock 功能。
lock = threading.Lock()
在线程中获取锁
lock.acquire()
使用完成后,我们肯定需要释放锁
lock.release()
Condition 条件变量
Python 提供了 Condition 对象。
使用 Condition 对象可以在某些事件触发或者达到特定的条件后才处理数据,Condition 除了具有 Lock 对象的 acquire 方法和 release 方法外,还提供了 wait 和 notify 方法。
线程首先 acquire 一个条件变量锁。如果条件不足,则该线程 wait,如果满足就执行线程,甚至可以 notify 其他线程。其他处于 wait 状态的线程接到通知后会重新判断条件。
其中条件变量可以看成不同的线程先后 acquire 获得锁,如果不满足条件,可以理解为被扔到一个( Lock 或 RLock )的 waiting 池。直到其他线程 notify 之后再重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。
线程间通信
从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put() 和 get() 操作来向队列中添加或者删除元素。
# -*- coding: UTF-8 -*-
from queue import Queue
from threading import Thread
isRead = True
def write(q):
# 写数据进程
for value in ['两点水', '三点水', '四点水']:
print('写进 Queue 的值为:{0}'.format(value))
q.put(value)
def read(q):
# 读取数据进程
while isRead:
value = q.get(True)
print('从 Queue 读取的值为:{0}'.format(value))
if __name__ == '__main__':
q = Queue()
t1 = Thread(target=write, args=(q,))
t2 = Thread(target=read, args=(q,))
t1.start()
t2.start()
结果:
写进 Queue 的值为:两点水
写进 Queue 的值为:三点水
从 Queue 读取的值为:两点水
写进 Queue 的值为:四点水
从 Queue 读取的值为:三点水
从 Queue 读取的值为:四点水
Python 还提供了 Event 对象用于线程间通信,它是由线程设置的信号标志,如果信号标志位真,则其他线程等待直到信号接触。
Event 对象实现了简单的线程通信机制,它提供了设置信号,清楚信号,等待等用于实现线程间的通信。
-
设置信号
使用 Event 的 set() 方法可以设置 Event 对象内部的信号标志为真。Event 对象提供了 isSe() 方法来判断其内部信号标志的状态。当使用 event 对象的 set() 方法后,isSet() 方法返回真 -
清除信号
使用 Event 对象的 clear() 方法可以清除 Event 对象内部的信号标志,即将其设为假,当使用 Event 的 clear 方法后,isSet() 方法返回假 -
等待
Event 对象 wait 的方法只有在内部信号为真的时候才会很快的执行并完成返回。当 Event 对象的内部信号标志位假时,则 wait 方法一直等待到其为真时才返回。
实例
# -*- coding: UTF-8 -*-
import threading
class mThread(threading.Thread):
def __init__(self, threadname):
threading.Thread.__init__(self, name=threadname)
def run(self):
# 使用全局Event对象
global event
# 判断Event对象内部信号标志
if event.isSet():
event.clear()
event.wait()
print(self.getName())
else:
print(self.getName())
# 设置Event对象内部信号标志
event.set()
# 生成Event对象
event = threading.Event()
# 设置Event对象内部信号标志
event.set()
t1 = []
for i in range(10):
t = mThread(str(i))
# 生成线程列表
t1.append(t)
for i in t1:
# 运行线程
i.start()
结果:
1
0
3
2
5
4
7
6
9
8
小结
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。