Python多线程应用

  • 前言
  • 一、多线程定义
  • 二、Python中的多线程
  • 2.1 全局解释锁
  • 2.1.1 定义
  • 2.1.2 必要性
  • 2.1.3 局限性
  • 2.1.4 不同版本编译器
  • 2.2 标准库threading
  • 2.3 标准库concurrent.futures模块
  • 总结
  • 参考文献



前言

在工作中时常会遇到需要提升产品的工作效率,降低程序运行时间的需求。若在流程中能够并行处理的模块使其并行执行,则能够大大减少程序运行的时间消耗。程序多进程运行或者多线程运行均为有效的实现方法,在本人工作中使用了多线程的处理方法,因此本文主要介绍Python多线程的基础内容。


以下是本篇文章正文内容

一、多线程定义

线程是指从软件或硬件上实现多个线程并发执行的技术。 具有多线程计算能力的计算机因有硬件支持而能够在同一时间执行 多于一个线程,进而整体提升处理性能。

软件上的多线程,即使计算机的处理器只有一个核,也可通过让线程以极短的时间间隔交替执行,从而实现"伪并行"。真正的并行只能在多核计算机上实现。

二、Python中的多线程

即使在多核计算机中,Python中实现的多线程也为上述所说的“伪并行”的多线程, 而这一特性是由于Python中的全局解释锁(Global Interpreter Lock)即GIL决定的,下文简称GIL。

本文将根据工作中遇到不同的应用场景,着重介绍在不同场景下实现Python实现多线程的方式。

2.1 全局解释锁

2.1.1 定义

全局解释锁是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何一个时刻都仅有一个线程在运行。即便在多核心处理器上,使用GIL的解释器也只允许同一时间执行一个线程。常见的使用GIL的解释器有CPython和Ruby MRI

2.1.2 必要性

Python使用GIL是为了避免出现多个线程争夺空间全局共享变量的问题,因为CPython的内存管理是线程不安全的,线程不安全指的是CPython的内存管理机制不提供数据访问保护,有可能多个线程先后对同一数据数据进行更改从而导致数据的失真和无效化。

2.1.3 局限性

GIL使得CPython的多线程程序不能充分发挥多核处理器的性能,并且其他功能也依赖于GIL所提供的功能保障,使得其很难在不破坏许多官方和非官方Python依赖包和模块的情况下删除GIL。

2.1.4 不同版本编译器

  • Jpython和IronPython不存在GIL。
  • Pypy和Cpython一样,存在GIL。
  • 在Cpython中,虽然GIL存在,但是可以通过with语句,暂时释放。

2.2 标准库threading

在Python3中,标准库thread已被摒弃,可以通过标准库threading,创建线程。

代码示例:

import time
import threading

def work(process):
	print(f'{process} start running')
	time.sleep(1)
	print(f'{process} end')

if __name__="__main__":
	t1 = time.time()
	task_1 = threading.Thread(target=work, args=('Process 1',))
    task_2 = threading.Thread(target=work, args=('Process 2',))
    task_1.start()
    task_2.start()
    t2 = time.time()
    print(f'process total time:{t2 - t1}')

代码执行结果如下所示:

Process 1 start running
Process 2 start running
process total time:0.00023865699768066406
Process 1 end running
Process 2 end running

可以看到程序执行时间要远远小于程序单线程执行预期的时间2s。从日志也可以看出,子线程执行期间,主线程仍然向下执行,而不会等子线程执行结束。

在一些工作场合中,我们需要等待子线程执行结束,再继续往下执行主线程,这时候我们需要用到join()方法。join()方法的作用是阻塞主线程直到该子线程执行结束。

代码示例:

import time
import threading

def work(process):
    print(f'{process} start running')
    time.sleep(1)
    print(f'{process} end running')

if __name__ == "__main__":
    t1 = time.time()
    task_1 = threading.Thread(target=work, args=('Process 1',))
    task_2 = threading.Thread(target=work, args=('Process 2',))
    task_1.start()
    task_2.start()
    task_2.join()
    t2 = time.time()
    print(f'process total time:{t2 - t1}')

代码执行结果如下所示:

Process 1 start running
Process 2 start running
Process 1 end running
Process 2 end running
process total time:1.0043089389801025

主线程等待子线程Process2执行结束后,主线程才继续往下执行。

2.3 标准库concurrent.futures模块

在实际的工作场合中,我们常常需要执行的函数执行结束后给到我们程序执行后产生的返回值,使用标准库threading简单的多线程控制执行模式难以实现这一点。因此需要引入标准库的concurrent.futures模块。

concurrent.futures为异步执行可调用对象提供了一个高级接口。

在concurrent.futures模块中异步执行可以使用ThreadPoolExecutor通过线程执行。也可以使用ProcessPoolExecutor在单独的进程中执行。两者都实现了相同的接口,该接口由抽象的Executor类定义。

本文将重点介绍ThreadPoolExecutor类,通过该类创建线程池,实现对线程的管理,以及获取线程执行函数返回值。

代码示例:

import concurrent.futures
import time

def work(process, num):
    print(f'{process} start running')
    time.sleep(1)
    print(f'{process} end ')
    return num

if __name__ == "__main__":
    # create thread pool
    t1 = time.time()
    thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=5, thread_name_prefix='work_threads')
    # submit thread
    thread_sub = []
    for i in range(5):
        thread_sub.append(thread_pool.submit(work, f'process {i}',i))
    # get the return value of sub thread function
    result = []
    for future in concurrent.futures.as_completed(thread_sub):
        result.append(future.result())
    t2 = time.time()
    print(f'return value: {result}')
    print(f'total time: {t2 - t1}')

代码执行结果如下所示:

process 0 start running
process 1 start running
process 2 start running
process 3 start running
process 4 start running
process 0 end 
process 2 end 
process 1 end 
process 3 end 
process 4 end 
return value: [0, 2, 1, 3, 4]
total time: 1.0074539184570312

总结

本文主要介绍了在Python中多线程的使用方法,介绍了标准库Threading和标准库concurrent.futures模块。通过使用这两个标准库和模块,可在使用Python的工作场合中,应对一些需要使用多线程提高工作效率的需求。