Python 线程:简介

已发表: 2022-10-25

在本教程中,您将学习如何使用 Python 的内置线程模块来探索 Python 中的多线程功能。

从进程和线程的基础知识开始,您将了解多线程在 Python 中的工作原理,同时了解并发和并行的概念。 然后,您将学习如何使用内置的threading模块在 Python 中启动和运行一个或多个线程。

让我们开始吧。

进程与线程:有什么区别?

什么是过程?

进程是需要运行的程序的任何实例。

它可以是任何东西——Python 脚本或网络浏览器(如 Chrome)到视频会议应用程序。 如果您在计算机上启动任务管理器并导航到Performance –> CPU ,您将能够看到当前在您的 CPU 内核上运行的进程和线程。

cpu-proc-线程

了解进程和线程

在内部,一个进程有一个专用的内存来存储与该进程相对应的代码和数据。

一个进程由一个或多个线程组成。 线程是操作系统可以执行的最小指令序列,它代表了执行的流程。

每个线程都有自己的堆栈和寄存器,但没有专用内存。 与进程关联的所有线程都可以访问数据。 因此,数据和内存由进程的所有线程共享。

进程和线程

在具有 N 个内核的 CPU 中,N 个进程可以在同一时间实例并行执行。 但是,同一进程的两个线程永远不能并行执行,但可以同时执行。 我们将在下一节讨论并发与并行的概念。

根据我们目前所学的知识,让我们总结一下进程和线程之间的区别。

特征过程线
记忆专用内存共享内存
执行方式并行,并发同时; 但不平行
执行由操作系统CPython 解释器

Python中的多线程

在 Python 中,全局解释器锁 (GIL) 确保只有一个线程可以获取锁并在任何时间点运行。 所有线程都应该获得这个锁才能运行。 这确保了在任何给定时间点只能执行单个线程,并避免同时执行多线程。

例如,考虑同一进程的两个线程t1t2 。 因为线程在t1读取特定值k时共享相同的数据,所以t2可以修改相同的值k 。 这可能导致死锁和不良结果。 但是只有一个线程可以获取锁并在任何实例上运行。 因此,GIL 也保证了线程安全

那么我们如何在 Python 中实现多线程能力呢? 为了理解这一点,让我们讨论并发和并行的概念。

并发与并行:概述

考虑一个具有多个内核的 CPU。 在下图中,CPU 有四个内核。 这意味着我们可以在任何给定时刻并行运行四种不同的操作。

如果有四个进程,那么每个进程都可以在四个核中的每一个上独立并同时运行。 假设每个进程有两个线程。

多核并行

要了解线程的工作原理,让我们从多核处理器架构切换到单核处理器架构。 如前所述,在特定的执行实例中只能有一个线程处于活动状态; 但处理器内核可以在线程之间切换。

代码

例如,I/O 绑定线程经常等待 I/O 操作:读入用户输入、数据库读取和文件操作。 在这段等待时间内,它可以释放锁,以便其他线程可以运行。 等待时间也可以是简单的操作,比如休眠n秒。

总结:在等待操作期间,线程释放锁,使处理器内核能够切换到另一个线程。 等待期结束后,较早的线程恢复执行。 这个过程,其中处理器内核同时在线程之间切换,促进了多线程。

如果您想在应用程序中实现进程级并行性,请考虑使用多处理。

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("Running some_func...") time.sleep(2) print("Finished running 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 ,因此活动线程数为两个,如输出所示:

 # Output Running some_func... 2 Finished running some_func.

如果我们仔细查看输出,我们会看到在启动thread1时,第一个 print 语句运行。 但在睡眠操作期间,处理器切换到主线程并打印出活动线程的数量——无需等待thread1 1 完成执行。

线程1-ex

等待线程完成执行

如果你想让thread1完成执行,你可以在启动线程后调用它的join()方法。 这样做将等待thread1完成执行而不切换到主线程。

 import threading import time def some_func(): print("Running some_func...") time.sleep(2) print("Finished running some_func.") thread1 = threading.Thread(target=some_func) thread1.start() thread1.join() print(threading.active_count())

现在, thread1在我们打印出活动线程数之前已经完成了执行。 所以只有主线程在运行,这意味着活动线程数为1。

 # Output Running some_func... Finished running 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

– 要从特定数字倒数到零,您可以使用负step值 -1 并将stop值设置为 -1,以便包括零。

– 同样,要计数到n ,您必须将stop值设置为n + 1 。 因为startstep的默认值分别为 0 和 1,所以您可以使用range(n + 1)来获取从 0 到 n 的序列。

接下来,我们定义两个线程thread1和 thread2 来分别运行count_downthread2 count_up 。 我们为这两个函数添加了print语句和sleep操作。

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

 import threading import time def count_down(n): for i in range(n,-1,-1): print("Running thread1....") print(i) time.sleep(1) def count_up(n): for i in range(n+1): print("Running thread2...") 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_down函数在thread1上运行,从 10 倒数到 0。
 # Output Running thread1.... 10 Running thread2... 0 Running thread1.... 9 Running thread2... 1 Running thread1.... 8 Running thread2... 2 Running thread1.... 7 Running thread2... 3 Running thread1.... 6 Running thread2... 4 Running thread1.... 5 Running thread2... 5 Running thread1.... 4 Running thread1.... 3 Running thread1.... 2 Running thread1.... 1 Running thread1.... 0

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

加起来

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

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

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