如何在Python中使用线程实现并发?

如何在Python中使用线程实现并发?

虽然Python本身是一种解释型语言,其执行效率不如编译型语言高,但是Python提供了多种实现并发的方法,其中使用线程是较为简单的一种。

Python标准库提供了threading模块,可以在同一程序中运行多个并发的线程。

阅读更多:Python 教程

创建线程

在Python中,可以通过以下步骤创建线程:

  1. 导入threading模块
  2. 创建线程实例,并传入要执行的函数
  3. 调用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的多线程仍然具有优势。

因此,在实际应用中需要根据具体应用场景和需求,选择合适的并发处理方式。

Camera课程

Python教程

Java教程

Web教程

数据库教程

图形图像教程

办公软件教程

Linux教程

计算机教程

大数据教程

开发工具教程