1. threading模块简介
在 Python 中,多线程是一种非常常用的并发编程方式,可以实现多个线程同时执行,通常用于处理一些 I/O 密集型的任务,比如网络通信、文件读写等操作。Python 的 threading 模块就是专门用于实现多线程的库,提供了许多与线程相关的函数和类。
1.1 threading模块的基本用法
threading 模块主要包括以下几个类:Thread、Lock、RLock、Condition、Semaphore 和 Event。其中,Thread 是最常用的类,用于创建线程对象,其他几个类则用于线程同步。
下面是一个简单的 threading 模块使用例子,创建了两个线程,分别输出数字 0 到 9:
import threading
def print_num():
for i in range(10):
print(i)
t1 = threading.Thread(target=print_num)
t2 = threading.Thread(target=print_num)
t1.start()
t2.start()
可以看到,上面的代码创建了两个线程,分别启动后并行运行,输出数字 0 到 9。
1.2 线程同步
在多线程编程中,因为多个线程可能同时访问同一个共享的资源,可能会产生一些问题,比如数据竞争(Data Race)、死锁(Deadlock)等等。为了避免这些问题,Python 提供了一些同步机制,使得多个线程能够在访问共享资源时协调彼此的行为。
1.2.1 Lock 和 RLock
Lock(互斥锁)是一个最基本的同步原语,用于保护共享资源的访问。在多个线程同时访问共享资源时,Lock 可以确保每一时刻只有一个线程可以访问该资源。RLock(可重入锁)则是一种特殊的 Lock,支持同一线程多次获取锁而不会死锁的情况。
下面是一个使用 Lock 来同步的例子,实现了一个简单的计数器,保证每一次自增操作都是原子性的:
import threading
class Counter:
def __init__(self):
self._value = 0
self._lock = threading.Lock()
def increment(self):
with self._lock:
self._value += 1
print(self._value)
counter = Counter()
t1 = threading.Thread(target=counter.increment)
t2 = threading.Thread(target=counter.increment)
t1.start()
t2.start()
可以看到,上述代码使用了 Lock 保护了自增操作,每次只有一个线程可以进入 increment 函数并完成自增操作,从而避免了数据竞争问题。
1.2.2 Condition
Condition(条件变量)是一种复杂的同步机制,允许线程在满足特定条件之前挂起等待,一旦条件满足则可以唤醒挂起的线程。它是基于 Lock 和 Event 的,因此可以与 Lock 和 RLock 配合使用。
下面是一个使用 Condition 来同步的例子,实现了一个阻塞队列,当队列为空时消费者线程会等待直到队列不为空:
import threading
import queue
class BlockingQueue:
def __init__(self):
self._queue = queue.Queue()
self._condition = threading.Condition()
def put(self, item):
with self._condition:
self._queue.put(item)
self._condition.notify()
def get(self):
with self._condition:
while self._queue.empty():
self._condition.wait()
return self._queue.get()
def producer(queue):
for i in range(10):
queue.put(i)
def consumer(queue):
while True:
item = queue.get()
print(item)
queue = BlockingQueue()
t1 = threading.Thread(target=producer, args=(queue,))
t2 = threading.Thread(target=consumer, args=(queue,))
t1.start()
t2.start()
可以看到,上述代码使用了 Condition 保护了队列操作,当队列为空时,消费者线程会调用 wait 方法来陷入等待状态,直到生产者线程往队列中添加新元素并唤醒等待线程。
1.2.3 Semaphore 和 Event
Semaphore(信号量)和 Event(事件)也是常用的同步原语,它们都可以用于多个线程之间的协调与同步。
Semaphore 是一种计数器,用于控制并发访问的数量。在多个线程同时执行的情况下,Semaphore 可以确保并发访问的数量不超过某一个阈值,从而避免了一些资源抢占的问题。下面是一个使用 Semaphore 来同步的例子,表示同时只能允许两个线程访问资源:
import threading
semaphore = threading.Semaphore(2)
def do_work():
with semaphore:
# 访问共享资源
pass
Event 则是一种通信机制,用于线程之间的事件通知与等待。Event 对象提供了两种方法:set 和 clear。set 方法会将 event 对象的内部状态设置为 True,而 clear 方法则会将状态设置为 False。wait 方法用于等待事件的发生,只有当事件状态为 True 时才会返回,否则会一直阻塞直到超时或收到中断信号。
下面是一个使用 Event 来同步的例子,实现了生产者与消费者模式:
import threading
event = threading.Event()
queue = []
def producer():
for i in range(10):
queue.append(i)
event.set()
event.clear()
def consumer():
while True:
event.wait()
item = queue.pop(0)
print(item)
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
上述代码中,生产者线程向队列中添加元素后,会调用 event.set() 方法来通知消费者线程开始工作。消费者线程在执行完打印操作后,会调用 event.clear() 方法将事件状态重新设置为 False,等待下次的通知。
2. GIL 与多线程
在 Python 中,有一个重要的概念就是 GIL(Global Interpreter Lock,全局解释器锁),它是一个线程级别的锁,用于保护 Python 解释器中的共享内存数据结构。GIL 在 Python 中起到了一个重要的作用,它可以确保同一时刻只有一个线程可以执行 Python 字节码,从而避免了并发访问共享数据结构时的竞争问题。
然而,GIL 在多线程编程中也存在一些问题。因为 Python 解释器中只有一个线程可以执行 Python 字节码,所以在多个线程同时执行 CPU 密集型任务时,效率并不会得到提高。与此同时,GIL 也会导致一些线程同步的问题,比如死锁、饥饿(Starvation)等等。
因此,在 Python 中,多线程主要是针对 I/O 密集型任务进行优化的,即那些需要等待外部操作才能继续运行的任务。这些任务使用多线程可以让 Python 程序更加高效,因为在等待外部操作的时候,单个线程会被阻塞,而其他线程可以继续执行。
3. 总结
本文介绍了 Python 中的 threading 模块,包括了基本用法、线程同步的机制以及 GIL 对多线程的影响。Python 中的多线程编程适用于 I/O 密集型任务,能够提高程序的效率,但在 CPU 密集型任务中效果不尽如人意。在实际编程过程中,需要根据任务类型和需求来选择适合的并发编程方式,才能发挥出最佳的性能和效果。