Python 线程:简介 – techblik.com

探索 Python 中的多线程

在本指南中,您将深入了解 Python 多线程的功能,利用其内置的 threading 模块。

我们将从进程和线程的基础概念开始,阐明 Python 中多线程的工作原理,同时解释并发和并行的概念。 随后,我们将学习如何使用 threading 模块在 Python 中启动和管理多个线程。

让我们开始探索吧!

进程 vs 线程:区别是什么?

什么是进程?

进程指的是一个正在运行的程序的实例。

它可以是任何事物,比如 Python 脚本、网页浏览器(如 Chrome)或视频会议应用。 您可以通过打开计算机的任务管理器,查看“性能”选项卡下的 CPU 部分,来查看 CPU 内核上当前运行的进程和线程。

理解进程和线程

在计算机内部,每个进程都拥有独立的内存空间,用于存储其代码和数据。

一个进程可以包含一个或多个线程。 线程是操作系统能够执行的最小指令序列,代表了执行的路径。

每个线程都有自己的堆栈和寄存器,但没有独立的内存空间。 同一个进程内的所有线程可以访问相同的数据。 因此,数据和内存都是由该进程的所有线程共享的。

在具有 N 个内核的 CPU 上,最多可以并行执行 N 个进程。 然而,同一个进程的两个线程永远无法并行执行,但是它们可以并发执行。 我们将在后续章节中详细讨论并发与并行的概念。

根据以上内容,让我们总结一下进程和线程之间的差异。

特性 进程 线程
内存 独立内存空间 共享内存空间
执行方式 并行、并发 并发;但不是由操作系统直接并行执行,而是由 CPython 解释器处理

Python 中的多线程

在 Python 中,全局解释器锁 (GIL) 确保在任何给定时间点只有一个线程可以获得锁并执行。 所有线程都需要获取这个锁才能运行,这保证了在同一时刻只有一个线程在执行,从而避免了多线程同时运行带来的问题。

例如,假设同一进程中有两个线程 t1 和 t2。 当线程 t1 读取某个变量 k 的值时,线程 t2 可能会修改 k 的值。这可能会导致死锁和不正确的结果。 但是,由于每次只有一个线程可以获得锁并执行,GIL 保证了线程安全。

那么,我们如何在 Python 中实现多线程呢? 要理解这一点,我们需要先了解并发和并行的概念。

并发与并行:概述

假设我们有一个多核 CPU。 在下面的图中,CPU 有四个内核,这意味着我们可以在任何给定时刻同时并行执行四个不同的操作。

如果有四个进程,那么每个进程都可以在四个内核上独立地并行运行。 如果每个进程又有两个线程呢?

为了理解线程的工作方式,让我们从多核处理器切换到单核处理器。 如前所述,在特定的执行时刻,只能有一个线程处于活动状态。 但处理器内核可以在线程之间切换。

例如,I/O 密集型线程常常需要等待 I/O 操作,比如读取用户输入、数据库读取和文件操作。 在等待期间,它可以释放锁,以便其他线程可以运行。 等待时间也可以是一些简单的操作,如休眠几秒钟。

总结:当线程等待操作时,它会释放锁,从而使处理器内核能够切换到另一个线程。 等待期结束后,之前的线程恢复执行。 这个过程,处理器内核在多个线程之间切换,实现了多线程。✅

如果您想在应用程序中实现进程级别的并行性,可以考虑使用多进程。

Python 线程模块:入门

Python 自带了一个 threading 模块,您可以将其导入到 Python 脚本中。

import threading

要在 Python 中创建线程对象,可以使用 Thread 构造函数:threading.Thread(...)。 这是满足大多数线程需求的通用语法:

threading.Thread(target=..., args=...)

其中,

  • target 是一个关键字参数,它代表 Python 中可调用的对象(例如,函数)。
  • args 是目标函数接受的参数元组。

您需要 Python 3.x 才能运行本教程中的代码示例。 您可以下载代码并跟着操作。

如何在 Python 中定义和运行线程

让我们定义一个运行目标函数的线程。

目标函数为 some_func

import threading
import time

def some_func():
    print("正在运行 some_func...")
    time.sleep(2)
    print("some_func 运行完成。")

thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())

让我们解析一下上面的代码片段都做了什么:

  • 导入了 threadingtime 模块。
  • 函数 some_func 包含描述性的 print() 语句,和一个两秒的休眠操作: time.sleep(n) 使函数休眠 n 秒。
  • 接下来,我们定义了一个线程 thread_1,其目标函数为 some_functhreading.Thread(target=...) 创建了一个线程对象。
  • 注意:指定函数名而不是函数调用; 使用 some_func 而不是 some_func()
  • 创建线程对象不会启动线程; 你需要在线程对象上调用 start() 方法才能启动它。
  • 要获取当前活动线程的数量,我们使用 active_count() 函数。

Python 脚本在主线程上运行,我们创建了另一个线程 (thread1) 来运行函数 some_func,因此当前活动线程的数量为两个,如输出所示:

# 输出
正在运行 some_func...
2
some_func 运行完成。

仔细观察输出,你会看到当 thread1 启动时,第一个打印语句会运行。 但在休眠操作期间,处理器切换到主线程并打印出活动线程的数量 – 而无需等待线程 1 完成执行。

等待线程完成执行

如果你希望 thread1 完成执行,你可以在启动线程后调用其 join() 方法。 这将等待 thread1 完成执行,而不会切换到主线程。

import threading
import time

def some_func():
    print("正在运行 some_func...")
    time.sleep(2)
    print("some_func 运行完成。")

thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())

现在,thread1 在我们打印活动线程数之前已经完成了执行。 因此,只有主线程在运行,这意味着当前活动线程的数量为 1。✅

# 输出
正在运行 some_func...
some_func 运行完成。
1

如何在 Python 中运行多个线程

接下来,让我们创建两个线程来运行两个不同的函数。

这里,count_down 是一个函数,它接受一个数字作为参数,并从该数字倒数到零。

def count_down(n):
    for i in range(n,-1,-1):
        print(i)

我们定义 count_up,另一个 Python 函数,从零数到给定的数字。

def count_up(n):
    for i in range(n+1):
        print(i)

📑 使用语法 range(start, stop, step)range() 函数时,默认情况下会排除终点 stop

– 要从特定数字倒数到零,可以使用负步进值 -1 并将停止值设置为 -1,以便包含零。

– 类似地,要数到 n,必须将 stop 值设置为 n + 1。因为 startstep 的默认值分别为 0 和 1,可以使用 range(n + 1) 得到序列 0 到 n。

接下来,我们定义两个线程,thread1thread2,分别运行函数 count_downcount_up。 我们为这两个函数添加了打印语句和休眠操作。

创建线程对象时,请注意目标函数的参数应该指定为一个元组 – 传递给 args 参数。 因为这两个函数 (count_downcount_up) 都接受一个参数,你必须在值后显式地插入逗号。 这确保了参数仍然作为元组传递,否则,后续的元素会被推断为 None

import threading
import time

def count_down(n):
    for i in range(n,-1,-1):
        print("运行线程1....")
        print(i)
        time.sleep(1)

def count_up(n):
    for i in range(n+1):
        print("运行线程2...")
        print(i)
        time.sleep(1)

thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()

在输出中:

  • 函数 count_upthread2 上运行,从 0 开始数到 5。
  • 函数 count_downthread1 上运行,从 10 倒数到 0。
# 输出
运行线程1....
10
运行线程2...
0
运行线程1....
9
运行线程2...
1
运行线程1....
8
运行线程2...
2
运行线程1....
7
运行线程2...
3
运行线程1....
6
运行线程2...
4
运行线程1....
5
运行线程2...
5
运行线程1....
4
运行线程1....
3
运行线程1....
2
运行线程1....
1
运行线程1....
0

你可以看到 thread1thread2 交替执行,因为它们都涉及到等待操作(休眠)。 一旦 count_up 函数计数到 5 完成,thread2 就不再处于活动状态。 所以我们得到的输出只对应于 thread1

总结

在本教程中,你学习了如何使用 Python 的内置 threading 模块来实现多线程。 以下是关键要点的总结:

  • Thread 构造函数可以用来创建线程对象。 使用 threading.Thread(target=<callable>, args=( <tuple of args> )) 创建一个线程,该线程会使用 args 中指定的参数来运行目标可调用对象。
  • Python 程序在主线程上运行,所以你创建的线程对象是附加的线程。 你可以调用 active_count() 函数返回当前活动线程的数量。
  • 你可以使用线程对象上的 start() 方法来启动一个线程,然后使用 join() 方法等待它完成执行。

你可以通过调整等待时间、尝试不同的 I/O 操作等来编写更多的示例。 确保在即将到来的 Python 项目中实现多线程。 编码愉快!🎉