如何在Python中使用线程实现并发?
虽然Python本身是一种解释型语言,其执行效率不如编译型语言高,但是Python提供了多种实现并发的方法,其中使用线程是较为简单的一种。
Python标准库提供了threading模块,可以在同一程序中运行多个并发的线程。
阅读更多:Python 教程
创建线程
在Python中,可以通过以下步骤创建线程:
- 导入threading模块
- 创建线程实例,并传入要执行的函数
- 调用start()方法启动线程
下面是创建一个简单的线程的示例:
import threading
def work():
print('Thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=work)
t.start()
在上面的示例中,我们通过threading模块创建了一个线程,传入了work函数作为线程要执行的任务,随后调用start()方法启动线程。
在work函数中,我们通过调用threading.current_thread()方法来获取当前线程的信息,并输出执行的信息。
线程安全
在多线程的并发执行中,可能会出现线程安全问题(多个线程同时访问同一数据,可能会出现冲突)。
为了避免线程安全问题,可以使用Python中的Lock
对象来实现线程同步。
Lock
是互斥锁,同一时刻只允许一个线程访问共享资源,其他线程需要等待该线程释放锁之后才能访问。
下面是一个运用Lock
对象实现线程同步的例子:
import threading
lock = threading.Lock()
counter = 0
def add():
global counter
lock.acquire()
for i in range(100000):
counter += 1
lock.release()
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)
t1.start()
t2.start()
t1.join()
t2.join()
print(counter)
在该示例中,我们使用了一个共享变量counter。对于该变量的访问需要我们使用Lock
对象进行同步。
在add函数中,我们先调用lock.acquire()方法获取锁,确保当前线程可以执行相应任务,并在任务完成后使用lock.release()方法释放锁,以便其他线程可以争用锁。
该示例中,我们创建了两个线程,每个线程都调用add函数,对变量counter进行累加操作,将counter值增加2 * 100000次。
值得一提的是,如果在多线程的情况下对counter进行累加操作而没有进行线程同步,则最后的输出结果可能不是200000。
线程间通信
除了使用Lock
对象等方式进行线程同步外,Python还提供了Queue对象用于在线程之间通信。
Queue是指一个队列,遵循FIFO(先进先出)原则,即先放入队列中的任务先执行,后放入队列的任务后执行。
如果需要在线程间传递数据,则可以使用Queue对象将数据从一个线程传递到另一个线程。
下面是一个利用Queue对象实现线程间通信的示例:
import queue
import threading
q = queue.Queue()
class ProducerThread(threading.Thread):
def run(self):
for i in range(3):
q.put('Product ' + str(i))
print('Produce product %s...' % str(i))
class ConsumerThread(threading.Thread):
def run(self):
while True:
product = q.get()
if product is None:
break
print('Consume %s...' % product)
ProducerThread().start()
ConsumerThread().start()
ProducerThread().join()
ConsumerThread().join()
在该示例中,我们通过创建两个线程(生产者线程和消费者线程),利用Queue对象实现了线程间的数据传输。
在ProducerThread中,我们通过q.put()方法向队列中添加数据,并输出生产者线程生产产品的信息。
在ConsumerThread中,我们通过q.get()方法从队列中获取数据,并输出消费者线程消费产品的信息。
当队列中没有数据时,线程会暂停等待生产者线程将更多产品添加到队列中。
通过线程间数据传输实现了生产者线程和消费者线程之间的协作,实现了数据的共享和并发执行。
线程池
在多线程的应用场景下,频繁地创建和销毁线程开销较大,而且在实际应用中线程数一般是固定的,因此可以使用线程池来提升并发效率。
Python标准库提供了ThreadPoolExecutor
类,可实现线程池管理。
下面是一个简单的示例,创建一个大小为5的线程池,向线程池提交10个任务:
from concurrent.futures import ThreadPoolExecutor
import threading
def run(task):
print('Thread %s is running task %s...' % (threading.current_thread().name, task))
executor = ThreadPoolExecutor(max_workers=5)
for i in range(10):
executor.submit(run, 'task ' + str(i))
executor.shutdown(wait=True)
在该示例中,我们通过ThreadPoolExecutor类创建了大小为5的线程池,向线程池提交了10个任务(即10个task)。
在run函数中,我们输出了线程名和任务名的信息。
通过executor.submit()方法向线程池中提交任务,由线程池自行管理和执行,并可以通过executor.shutdown()方法关闭线程池。
GIL锁
在Python中,GIL(Global Interpreter Lock)锁是一把全局解释器锁,会在运行 Python 程序时进行加锁。
因为GIL的存在,Python中的多线程并不能在多个CPU核心上并行执行,而是在同一时间只有一个线程执行,使得Python的多线程并不能发挥到多核的优势,甚至有可能使得多线程程序的执行效率不如单线程程序。
但是在一些I/O密集型操作中,由于线程的等待时间比较长,GIL可以保证Python程序的线程执行的协同性,避免出现线程间争抢资源的情况,所以Python的多线程在I/O密集型任务下还是具有一定优势。
结论
在Python中使用线程实现并发,可以使用threading模块创建线程,使用Lock对象实现线程同步,使用Queue对象进行线程间通信,使用ThreadPoolExecutor实现线程池管理。
尽管GIL锁限制了Python多线程运行在多核上并行执行的效率,但在一些I/O密集型任务中,Python的多线程仍然具有优势。
因此,在实际应用中需要根据具体应用场景和需求,选择合适的并发处理方式。