跳至主要内容

权威指南:Python 线程教程

借助 Python 学习线程的基本概念以及如何使用它
· 12分钟 阅读

使用 AI 探索

如果您刚开始学习 Python 并希望进一步深入,请参加 DataCamp 的《Python 数据科学入门》课程。

graphic

简介

线程允许您让进程中的不同部分并发运行(来源:RealPython)。这些不同的部分通常是独立的,并且在同一进程下拥有各自的执行单元。进程本质上就是正在运行的程序,它包含可以并发运行的独立单元。例如,网页浏览器可以是一个进程,同时运行多路摄像头的应用可以是一个进程;电子游戏也是一个进程的例子。

在一个进程内部,有一种称为多线程的概念,多个线程协同工作以实现共同目标。使用线程最关键的好处是,它能让程序并行运行。

我们用一个例子来理解线程的概念。想象您有一个应用,用来统计进入和离开商场停车场的车辆数量。您的装置有多台摄像头监控出入口,并连接到中央设备。每台摄像头都会有一个监控车流的算法,它们属于同一进程或程序。然而,每台摄像头及其运行的算法都可以属于一个独立的线程。不仅如此,从摄像头读取画面的线程与对画面进行算法预测的线程也可以是两个独立的线程。

另一个例子是电子游戏,进程需要并行运行图形渲染、用户交互以及网络通信(在联机模式下),以确保随时都有良好的响应性。为此就要使用多线程的概念,每个线程负责运行彼此独立的任务。

线程有自己的执行流,这意味着同一进程中会在同一时间发生多件事情。

需要注意的是,每个进程至少有一个线程,即主线程。如果您的程序没有显式定义任何线程,那么它至少会有主线程,也就是进程本身。

如果您查看上方的示意图,可以看到一个进程由包含全局变量的主线程和三个不同的线程 t1、t2 和 t3 组成。每个线程都有自己的局部变量和代码执行流。线程通常共享进程的资源,例如主线程可用的资源也会在 t1、t2 和 t3 三个线程之间共享。所有线程都能访问全局变量,同时各自拥有自己的局部变量。

线程有不同的类型:

  • 内核线程
  • 用户线程
  • 内核线程与用户线程的组合

现在来看看在程序中使用线程的一些优势:

  • 在具有多个 CPU 的前提下,多线程可以加快程序的执行速度。

  • 它还允许您在进行 I/O 操作时执行其他任务,可以通过多个线程或主线程与单个线程配合来实现。例如,从摄像头读取画面和由算法对画面进行推断的速度将由不同线程处理。因此,算法不必等待画面输入,而读取画面的部分也无需等待算法执行完成再读取下一帧。

  • 同一进程内的线程可以共享主线程的内存和资源。

线程的挑战

  • 请记住,Python 基于 CPython 实现,它限制同一时刻只能有一个线程运行,因此线程并不一定能加速所有任务。其根本原因是全局解释器锁(GIL)。

    如果您想了解 GIL,欢迎查看这篇教程。

  • 如果您想加速 CPU 密集型任务,线程可能不是最佳方案。在这种情况下,多进程通常更有用。

  • 要执行包含多个线程的程序,系统需要在各线程之间快速切换;因此,在拥有大量线程的程序中需要关注调度问题。

  • 资源共享也可能成为问题,因为所有线程共享全局变量的同一套资源与内存。因此,一个线程执行的操作可能导致另一个线程发生内存错误,或者其他线程可能得不到足够的内存去完成其任务。

Python 中的线程

  • 在 Python 中,线程模块是一个内置模块,名为 threading,可以直接导入。

  • 由于在 Python 中几乎所有东西都表示为对象,线程在 Python 中也是对象。线程能够:

    • 持有数据,
    • 存储在诸如字典、列表、集合等数据结构中,
    • 可以作为参数传递给函数。

  • 线程也可以作为一个进程来执行。

  • Python 中的线程可以处于多种状态,例如:

    • 等待,
    • 锁定。

现在我们来学习如何在 Python 中实现线程。

Python3 中的 thread 模块

请注意,Python3 向后兼容 Python2.7 中的 thread 模块。在 Python3 中,它可以作为 _thread 模块导入。下面通过一个例子来理解 _thread 模块。

import _thread #thread module imported
import time #time module

我们来定义一个名为 thread_delay 的函数,它接收两个参数作为输入,即线程名称和延迟。在该函数内部,您将:

  • 将计数器定义为 0,
  • 然后使用 while 循环循环三次,
  • 在 while 循环中使用 time.sleep() 添加延迟,这个延迟有助于理解线程的执行。单位为秒,
  • 然后将计数器加 1,
  • 为了查看线程执行是否顺利,打印线程名称以及线程执行时的时间。
def thread_delay(thread_name, delay):
    count = 0
    while count < 3:
        time.sleep(delay)
        count += 1
        print(thread_name, '-------->', time.time())

现在要在上述函数上添加线程功能,或者在线程中运行上述函数,您将使用 _thread 模块中的 start_new_thread 方法。

我们来看一下 start_new_thread 方法的文档字符串。

?_thread.start_new_thread
Threading in Python Tutorial

我们传入不带括号的函数 thread_delay 以及两个参数,即线程名和延迟(用于可视化线程执行,因为它非常快)。

_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 3))
10916



t1 --------> 1588077856.4221454
t1 --------> 1588077857.4418247
t2 --------> 1588077858.422453
t1 --------> 1588077858.4431
t2 --------> 1588077861.4371448
t2 --------> 1588077864.438915

从上述输出可以看到,线程 t1 先开始执行。与此同时,线程 t2 因设置了 3 秒延迟而等待,当延迟结束后 t2 开始执行,并且 t1 的延迟只有 1 秒。

现在将 t2 的延迟改为 5 秒,这样 t2 会在 t1 执行完成之后再启动,因为 t1 只需要 3 秒就能完成执行。

_thread.start_new_thread(thread_delay, ('t1', 1))
_thread.start_new_thread(thread_delay, ('t2', 5))
14632



t1 --------> 1588078001.4087512
t1 --------> 1588078002.4272866
t1 --------> 1588078003.432218
t2 --------> 1588078005.4082909
t2 --------> 1588078010.4268317
t2 --------> 1588078015.4283602

使用 Threading 模块实现线程

我们继续使用上面的示例,不过这次使用 threading 模块,而不是 _thread 模块。

import threading
import time
def thread_delay(thread_name, delay):
    count = 0
    while count < 3:
        time.sleep(delay)
        count += 1
        print(thread_name, '-------->', time.time())

threading 模块中有一个 Thread 类,与 _thread 模块的 start_new_thread 函数有相似的精神。

我们查看一下 Thread 类的文档字符串,它接收多个参数,如 group、target(如函数)、args 等。

?threading.Thread
Threading in Python Tutorial

在 Thread 类构造函数中,您将传入目标函数 thread_delay 以及该函数的参数。

t1 = threading.Thread(target=thread_delay, args=('t1', 1))
t2 = threading.Thread(target=thread_delay, args=('t2', 3))

在 threading 模块中,为了运行或执行线程,您要使用 start() 方法,它负责启动线程。

?t1.start
Threading in Python Tutorial
t1.start()
t2.start()
t1 --------> 1588081121.7790291
t1 --------> 1588081122.7962046
t2 --------> 1588081123.7789369
t1 --------> 1588081123.7980876
t2 --------> 1588081126.7961557
t2 --------> 1588081129.7995002

您还会使用 join 方法,它表示等待直到所有线程执行完成。因此,join 方法之后的代码只有在线程终止后才会执行。

?t1.join
Threading in Python Tutorial
t1.start()
t2.start()

t1.join()
t2.join()

print("Thread execution is complete!")
t1 --------> 1588081798.7118263
t1 --------> 1588081799.7231226
t2 --------> 1588081800.7129223
t1 --------> 1588081800.730732
t2 --------> 1588081803.7298765
t2 --------> 1588081806.7309563
Thread execution is complete!

现在我们用线程来计算立方体和正方形的体积,这非常直观。

def volume_cube(a):
    print ("Volume of Cube:", a*a*a)
def volume_square(a):
    print ("Volume of Square:", a*a)
t1 = threading.Thread(target=volume_cube, args=(2))
t2 = threading.Thread(target=volume_square, args=(3))
t1.start()
t2.start()

t1.join()
t2.join()

print("Thread execution is complete!")
Thread execution is complete!


Exception in thread Thread-9:
Traceback (most recent call last):
  File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
    self.run()
  File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
TypeError: volume_square() argument after * must be an iterable, not int
Exception in thread Thread-8:
Traceback (most recent call last):
  File "C:\Program Files\Anaconda3\lib\threading.py", line 926, in _bootstrap_inner
    self.run()
  File "C:\Program Files\Anaconda3\lib\threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
TypeError: volume_cube() argument after * must be an iterable, not int

等等,怎么回事?一调用 start 方法就报错了。这是因为 args 参数期望的是元组,而 volume_cube 和 volume_square 只期望一个参数,因此在为函数指定第一个参数后,您需要在 args 参数中加上逗号。

t1 = threading.Thread(target=volume_cube, args=(2,))
t2 = threading.Thread(target=volume_square, args=(3,))
t1.start()
t2.start()

t1.join()
t2.join()

print("Thread execution is complete!")
Volume of Cube: 8
Volume of Square: 9
Thread execution is complete!

现在我们来学习将线程作为子类来使用。

将线程作为子类

本节您将学习如何从 threading 模块中的 Thread 类创建子类,然后基于该类创建线程实例 t1 和 t2。

这里同样会使用 thread_delay 函数。

class DataCampThread(threading.Thread):
    def __init__(self, name, delay):
        threading.Thread.__init__(self)
        self.name = name
        self.delay = delay


    def run(self):
        print('Starting Thread:', self.name)
        thread_delay(self.name,self.delay)
        print('Execution of Thread:', self.name, 'is complete!')


t1 = DataCampThread('t1', 1)
t2 = DataCampThread('t2', 3)    

t1.start()
t2.start()

t1.join()
t2.join()

print("Thread execution is complete!")
Starting Thread: t1
Starting Thread: t2
t1 --------> 1588089083.4879806
t1 --------> 1588089084.489222
t1 --------> 1588089085.4909666
Execution of Thread: t1 is complete!
t2 --------> 1588089085.4979634
t2 --------> 1588089088.499365
t2 --------> 1588089091.5012596
Execution of Thread: t2 is complete!
Thread execution is complete!

现在我们逐步理解上述代码:

  • 您定义了一个名为 DataCampThread 的类,并在类的继承列表中传入了 threading.Thread 类。这样做的原因是您想从 threading 模块创建一个子类。这样便可以使用 threading.Thread 类中提供的所有方法,这类似于继承的概念。

  • 接着,您定义了 DataCampThread 类的 init 方法,并传入函数及延迟。init 是一个构造函数,类似于传给 threading.Thread 类的参数,这里您将参数传给 DataCampThread 类。

  • 然后初始化主类 threading.Thread 的 init 方法,并设置 name 和 delay。

  • 接着定义 run 方法。回想这里您调用了 start 方法,它在底层会调用 run 方法。由于您继承了主类,因此可以对 run 方法进行修改。因此,当您调用 start 方法时,在 run 方法中所做的任何修改都会生效。

结语

恭喜您完成本教程。

本教程对 Python 中的线程做了基础介绍。然而,线程是一个非常广泛且重要的主题,仍有一些概念未深入展开。您可以继续了解更多线程方法,如活动计数、当前线程等。

如果您想进一步学习,也可查看 Python 的基于线程的并行文档。

如果您刚开始学习Python并希望进一步深入,请参加 DataCamp 的《Python 数据科学入门》课程。

主题