【Python】超详细实例讲解python多线程(threading模块)

什么是多线程?

线程(thread)是操作系统中能够进行运算的最小单位,包含于进程之中,一个进程可以有多个线程,这意味着一个进程中可以并发多个线程,即为多线程。

对于一个python程序,如果需要同时大量处理多个任务,有使用多进程和多线程两种方法。在python中,实现多线程主要通过threading模块,而多进程主要通过multiprocessing模块。

这两个模块的主要区别是:threading模块基于线程,而multiprocessing模块基于进程。threading模块使用共享内存来实现多线程,所有线程都共享一样的变量(这点在后续的实例中可以感受到);而multiprocessing基于子进程,每个子进程之间都有独立的变量和数据结构。两者的区别意味着threading更使用于I/O密集型任务(例如需要进行多表格读取操作),multiprocessing模块更适用于包含较多计算的CPU密集型任务(矩阵运算,图片处理类任务)。

需要注意的是,由于python中的GIL锁的存在,Python解释器只允许一个Python进程使用,这意味着对于一个解释器只允许一个进程在运行,这也是为什么threading模块无法适用于CPU密集型这类需要大量CPU资源的任务,因为一个进程的CPU资源有限,无论开启多少个线程,总的资源就只有那些,总耗时不会有太大变化。而multiprocessing模块则可以开多个进程,能够更快速的处理CPU密集型任务。

关于GIL锁和Multiprocessing模块的部分就不继续深入介绍了,本次主要介绍如何使用threading模块实现多线程的相关内容。

线程完整生命周期

一个线程完整的生命周期包括新建——就绪——运行——阻塞——死亡。

  1. 新建:即新创建一个线程对象
  2. 就绪:调用start方法后,线程对象等待运行,什么时候开始运行取决于调度
  3. 运行:线程处于运行状态
  4. 阻塞:处于运行状态的线程被堵塞,通俗理解就是被卡住了,可能的原因包括但不限于程序自身调用sleep方法阻塞线程运行,或调用了一个阻塞式I/O方法,被阻塞的进程会等待何时解除阻塞重新运行
  5. 死亡:线程执行完毕或异常退出,线程对象被销毁并释放内存

主线程与子线程 

我们讲的多线程实际上指的就是只在主线程中运行多个子线程,而主线程就是我们的python编译器执行的线程,所有子线程和主线程都同属于一个进程。在未添加子线程的情况下,默认就只有一个主线程在运行,他会将我们写的代码从开头到结尾执行一遍,后文中我们也会提到一些主线程与子线程的关系。

不扯那么多概念了,接下来直接进入正题!

实例1:直接使用Thread创建线程对象

Thread类创建新线程的基本语法如下:

Newthread = Thread(target=function, args=(argument1,argument2,...))

  • Newthread: 创建的线程对象
  • function: 要执行的函数
  • argument1,argument2: 传递给线程函数的参数,为tuple类型

假设一个任务task(当然task可以替换为其他任何任务,本实例中仅为假设),这个任务实现的功能是每隔1s打印某个字母,我们使用两个子线程,分别同时打印不同的字母a和b,实例如下:

"""
<case1: 直接使用threading中的Thread类创建线程>
Date: 2024/5/15
Author: 猫猫不吃sakana
"""

from threading import Thread
import time
from time import sleep


# 自定义的函数,可以替换成其他任何函数
def task(threadName, number, letter):
    print(f"【线程开始】{threadName}")
    m = 0
    while m < number:
        sleep(1)
        m += 1
        current_time = time.strftime('%H:%M:%S', time.localtime())
        print(f"[{current_time}] {threadName} 输出 {letter}")
    print(f"【线程结束】{threadName}")


thread1 = Thread(target=task, args=("thread_1", 4, "a"))  # 线程1:执行任务打印4个a
thread2 = Thread(target=task, args=("thread_2", 2, "b"))  # 线程2:执行任务打印2个b

thread1.start()  # 线程1开始
thread2.start()  # 线程2开始

thread1.join()  # 等待线程1结束
thread2.join()  # 等待线程2结束

其输出为:

【线程开始】thread_1
【线程开始】thread_2
[13:42:00] thread_1 输出 a
[13:42:00] thread_2 输出 b
[13:42:01] thread_1 输出 a
[13:42:01] thread_2 输出 b
【线程结束】thread_2
[13:42:02] thread_1 输出 a
[13:42:03] thread_1 输出 a
【线程结束】thread_1

线程thread1和thread2同时开始,thread2打印2个b后结束,而thread1继续打印a直到完成。

 实例2:使用join阻塞线程

在前一个实例中我们可以看到在结尾有thread1.join()和thread2.join()两个语句,这两个语句出现在末尾表示主线程会等待所有的子线程执行完成,当然了,由于默认我们创建的子线程是前台线程(这个概念会在后面提到),如果没有join语句主线程也会等待所有子线程执行完毕才退出。

join方法可以用于阻塞主线程的顺序执行,因此,在主线程中使用可以调整各个子线程的执行顺序,了解完这些之后,我们来看下一个实例。

"""
<case2: 使用join方法阻塞进程>
Date: 2024/5/15
Author: 猫猫不吃sakana
"""

from threading import Thread
import time
from time import sleep


# 自定义的函数,可以替换成其他任何函数
def task(threadName, number, letter):
    print(f"【线程开始】{threadName}")
    m = 0
    while m < number:
        sleep(1)
        m += 1
        current_time = time.strftime('%H:%M:%S', time.localtime())
        print(f"[{current_time}] {threadName} 输出 {letter}")
    print(f"【线程结束】{threadName}")


thread1 = Thread(target=task, args=("thread_1", 6, "a"))  # 线程1:假设任务为打印6个a
thread2 = Thread(target=task, args=("thread_2", 4, "b"))  # 线程2:假设任务为打印4个b
thread3 = Thread(target=task, args=("thread_3", 2, "c"))  # 线程3:假设任务为打印2个c

thread1.start()  # 线程1启动
thread2.start()  # 任务2启动
thread2.join()   # 等待线程2
thread3.start()  # 线程2完成任务后线程3才启动
thread1.join()   # 等待线程1完成线程
thread3.join()   # 等待线程3完成线程

其输出为:

【线程开始】thread_1
【线程开始】thread_2
[13:44:20] thread_2 输出 b
[13:44:20] thread_1 输出 a
[13:44:21] thread_2 输出 b
[13:44:21] thread_1 输出 a
[13:44:22] thread_2 输出 b
[13:44:22] thread_1 输出 a
[13:44:23] thread_2 输出 b
【线程结束】thread_2
[13:44:23] thread_1 输出 a
【线程开始】thread_3
[13:44:24] thread_3 输出 c
[13:44:24] thread_1 输出 a
[13:44:25] thread_1 输出 a
[13:44:25] thread_3 输出 c
【线程结束】thread_3
【线程结束】thread_1

由输出可以看出,由于join的加入,thread2.join使得主进程一直在等待thread2线程完成任务,因此直到线程thread2结束后,thread3才开始任务。

由于这里thread1一共打印6个a,thread2打印4个b,thread3打印2个c。thread1的工作量等于thread2+thread3的工作量之和,因此整个程序可以看成是thread1与thread2+thread3并行运行。

实例3:重写父类threading.Thread创建线程

实例1和2中,我们已经介绍了如何直接导入Thread函数创建线程以及如何利用join方法,但是这种创建线程的方法本质上使用的是其父类的默认设置,具有局限性。在实例3中,将进一步深入探讨如何继承并重写父类threading.Thread类创建子线程。

和实例2相同,我们假设需要用多个线程处理任务task1,thread1打印4个a字母(耗时4s),thread2线程打印2个b字母(耗时2s),如下:

"""
<case3: 重写父类threading.Thread创建线程>
Date: 2024/5/15
Author: 猫猫不吃sakana
"""

import threading
import time
from time import sleep


# myThread继承父类,并进行重写
class myThread(threading.Thread):
    # 重写父类的构造函数
    def __init__(self, number, letter):
        threading.Thread.__init__(self)
        self.number = number  # 添加number变量
        self.letter = letter  # 添加letter变量

    # 重写父类中的run函数
    def run(self):
        print(f"【线程开始】{self.name}")
        task1(self.name, self.number, self.letter)
        print("【线程结束】", self.name)

    # 重写父类析构函数
    def __del__(self):
        print("【线程销毁释放内存】", self.name)


# 自定义的函数,此处可以替换成任何其他想要多线程执行的任务
def task1(threadName, number, letter):
    m = 0
    while m < number:
        sleep(1)
        m += 1
        current_time = time.strftime('%H:%M:%S', time.localtime())
        print(f"[{current_time}] {threadName} 输出 {letter}")

# def task2...
# def task3...


thread1 = myThread(4, "a")  # 创建线程thread1:任务耗时2s
thread2 = myThread(2, "b")  # 创建线程thread2:任务耗时4s

thread1.start()  # 启动线程1
thread2.start()  # 启动线程2

thread1.join()  # 等待线程1
thread2.join()  # 等待线程2

输出为:

【线程开始】Thread-1
【线程开始】Thread-2
[10:37:58] Thread-1 输出 a
[10:37:58] Thread-2 输出 b
[10:37:59] Thread-1 输出 a
[10:37:59] Thread-2 输出 b
【线程结束】 Thread-2
[10:38:00] Thread-1 输出 a
[10:38:01] Thread-1 输出 a
【线程结束】 Thread-1
【线程销毁释放内存】 Thread-1
【线程销毁释放内存】 Thread-2

从输出中,我们可以清楚的看到两个并行任务从开始到结束,最后一起销毁并释放内存的全过程,很好的体现了线程的一个完整生命周期过程。

最后实现的效果与实例1实现的效果相同,但是使用继承重写父类的方法,可以让我们更加自由的定义各项参数以及定义线程处理的任务,也能让我们对threading模块的理解更加深入。

实例4:前台线程与后台线程(守护线程)

在前面的所有实例中,我们忽略了threading.Thread的daemon参数,其默认为False,表示线程默认就是一个前台线程。

前台线程表示当所有的前台线程都执行完毕时,整个程序才退出。将daemon参数设定为True是表示线程是一个后台线程,此时主进程结束时,所有未执行完成的后台线程也都会直接自动结束。

在上一个实例的基础上,在初始化部分加入self.daemon=True,并去掉末尾的join方法,替换成sleep方法来阻塞主程序的运行,我们来看看结果会变成什么样,实例如下:

"""
<case4: 前台线程与后台线程>
Date: 2024/5/15
Author: 猫猫不吃sakana
"""

import threading
import time
from time import sleep


# myThread继承父类,并进行重写
class myThread(threading.Thread):
    # 重写父类的构造函数
    def __init__(self, number, letter):
        threading.Thread.__init__(self)
        self.number = number  # 添加number变量
        self.letter = letter  # 添加letter变量
        self.daemon = True  # 默认前台线程

    # 重写父类中的run函数
    def run(self):
        print(f"【线程开始】{self.name}")
        task1(self.name, self.number, self.letter)
        print("【线程结束】", self.name)

    # 重写父类析构函数
    def __del__(self):
        print("【线程销毁释放内存】", self.name)


# 自定义的函数,此处可以替换成任何其他想要多线程执行的任务
def task1(threadName, number, letter):
    m = 0
    while m < number:
        sleep(1)
        m += 1
        current_time = time.strftime('%H:%M:%S', time.localtime())
        print(f"[{current_time}] {threadName} 输出 {letter}")

# def task2...
# def task3...


thread1 = myThread(4, "a")  # 创建线程thread1:假设任务耗时2s
thread2 = myThread(2, "b")  # 创建线程thread2:假设任务耗时4s

thread1.start()  # 启动线程1
thread2.start()  # 启动线程2

time.sleep(3)  # 主程序等待3s再继续执行

其输出将变为:

【线程开始】Thread-1
【线程开始】Thread-2
[10:31:45] Thread-1 输出 a
[10:31:45] Thread-2 输出 b
[10:31:46] Thread-1 输出 a
[10:31:46] Thread-2 输出 b
【线程结束】 Thread-2

Process finished with exit code 0

我们用sleep方法强行阻塞了主程序3s,但是由于我们将线程设定为了后台线程,3s过后,主程序将执行完毕,此时两个子线程thread1和thread2无论是否执行完成,都将强行结束。

将daemon参数设定为False,其输出则与实例3相同,如下:

【线程开始】Thread-1
【线程开始】Thread-2
[10:30:14] Thread-1 输出 a
[10:30:14] Thread-2 输出 b
[10:30:15] Thread-1 输出 a
[10:30:15] Thread-2 输出 b
【线程结束】 Thread-2
[10:30:16] Thread-1 输出 a
[10:30:17] Thread-1 输出 a
【线程结束】 Thread-1
【线程销毁释放内存】 Thread-1
【线程销毁释放内存】 Thread-2

实例5:线程同步(线程锁)

我们设想一下这种情况,当多线程同时执行时,由于threading模块的中线程的变量和数据结构共享,可能会出现多个线程同时修改一个数据的情况,这绝对是不行的。

为了将各个线程同步,我们引入线程锁的概念。当某个线程访问数据时,先对其加锁,其他线程若再想访问这个数据就会被阻塞,直到前一个线程解锁释放。在threading模块中,加锁和释放锁主要使用Lock类,使用其中的acquire()和release()方法:

Lock = threading.Lock()  # 在threading模块中获得锁类
Lock.acquire()  # 设置锁
Lock.release()  # 释放锁

在介绍线程锁实例时,我们就不使用前面几个实例用的打印字母的任务了。为了让各位更加直观地体会到线程锁的作用,我们使用多线程对一个列表list进行数据删改。

假设此时有多个线程都需要对这个列表进行修改操作,实例如下:

"""
<case5: 线程同步,线程锁>
Date: 2024/5/15
Author: 猫猫不吃sakana
"""

import threading
import time


# 子类myThread继承父类threading.Thread,并进行重写
class myThread(threading.Thread):
    # 重写父类构造函数
    def __init__(self, number):
        threading.Thread.__init__(self)
        self.number = number

    # 重写父类run函数,在调用start()时自动调用run函数
    def run(self):
        print(f"【线程开始】{self.name}")
        Lock.acquire()  # 设置线程锁
        edit_list(self.name, self.number)
        Lock.release()  # 释放线程锁

    # 重写父类析构函数
    def __del__(self):
        print("【线程销毁】", self.name)


# 自定义的任务函数
def edit_list(threadName, number):
    while number > 0:
        time.sleep(1)
        data_list[number-1] += 1
        current_time = time.strftime('%H:%M:%S', time.localtime())
        print(f"[{current_time}] {threadName} 修改datalist为{data_list}")
        number -= 1
    print(f"【线程{threadName}完成工作】")


data_list = [0, 0, 0, 0]
Lock = threading.Lock()

# 创建3个子线程
thread1 = myThread(1)
thread2 = myThread(2)
thread3 = myThread(3)

# 启动3个子线程
thread1.start()
thread2.start()
thread3.start()

# 主进程等待所有线程完成
thread1.join()
thread2.join()
thread3.join()

print("【主进程结束】")

输出为:

【线程开始】Thread-1
【线程开始】Thread-2
【线程开始】Thread-3
[09:55:22] Thread-1 修改datalist为[1, 0, 0, 0]
【线程Thread-1完成工作】
[09:55:23] Thread-2 修改datalist为[1, 1, 0, 0]
[09:55:24] Thread-2 修改datalist为[2, 1, 0, 0]
【线程Thread-2完成工作】
[09:55:25] Thread-3 修改datalist为[2, 1, 1, 0]
[09:55:26] Thread-3 修改datalist为[2, 2, 1, 0]
[09:55:27] Thread-3 修改datalist为[3, 2, 1, 0]
【线程Thread-3完成工作】
【主进程结束】
【线程销毁】 Thread-1
【线程销毁】 Thread-2
【线程销毁】 Thread-3

 当三个线程都需要使用同一个数据时,我们只需要对线程的run方法中进行加锁和释放锁的操作即可。此时三个子线程将会进行顺序操作,前一个子线程执行完成释放锁后,后一个线程才会继续执行。要注意的是,这三个子线程使用的需要是同一把锁。

 写在最后

这是我第一次尝试发文,写文章的初衷就是和大家能够一起进步,有什么不足大家也可以在评论区提出来,后续可能会继续更新python的一些知识点以及自己的学习感悟,与各位共勉。

threading模块还有很多可选参数和方法可供使用,详情可参见threading模块的官方文档

点击链接:threading --- Thread-based parallelism — Python 3.12.3 文档icon-default.png?t=N7T8https://docs.python.org/zh-cn/3/library/threading.html#

  • 61
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: 很抱歉,我之前回答的还是这个问题。以下是一个稍微复杂一点的 Python 多线程实例,可以用来计算一个矩阵的乘积: ```python import threading import numpy as np # 定义矩阵的大小 MATRIX_SIZE = 1000 # 初始化两个随机矩阵 matrix_a = np.random.rand(MATRIX_SIZE, MATRIX_SIZE) matrix_b = np.random.rand(MATRIX_SIZE, MATRIX_SIZE) # 初始化结果矩阵 result_matrix = np.zeros((MATRIX_SIZE, MATRIX_SIZE)) # 定义一个函数,用来计算矩阵的乘积 def multiply(start, end): for i in range(start, end): for j in range(MATRIX_SIZE): for k in range(MATRIX_SIZE): result_matrix[i][j] += matrix_a[i][k] * matrix_b[k][j] # 定义线程数量 NUM_THREADS = 4 # 计算每个线程需要计算的行数 rows_per_thread = MATRIX_SIZE // NUM_THREADS # 创建线程列表 threads = [] # 启动线程 for i in range(NUM_THREADS): start = i * rows_per_thread end = (i + 1) * rows_per_thread t = threading.Thread(target=multiply, args=(start, end)) threads.append(t) t.start() # 等待所有线程结束 for t in threads: t.join() # 检查结果是否正确 expected_result = np.dot(matrix_a, matrix_b) assert np.array_equal(result_matrix, expected_result) # 输出 Done 表示程序执行完毕 print("Done") ``` 这个例子首先初始化两个随机矩阵,然后定义一个函数 multiply,用来计算这两个矩阵的乘积。然后,它将矩阵分成若干个部分,每个部分分配给一个线程来计算。最后,它等待所有线程结束,检查结果是否正确,并输出 Done 表示程序执行完毕。 希望这个例子能够帮助您更好地理解 Python 多线程的使用方法。 ### 回答2: Python多线程实例是指在Python编程语言中使用多线程进行并发编程的实例。在Python中,可以使用内置的threading模块来创建和管理多线程。 通过创建多线程,可以在程序中同时执行多个任务。这对于需要同时处理多个任务的情况非常有用。例如,在下载大文件时,可以使用多线程同时下载多个文件,加快下载速度。此外,多线程还可以用于处理网络请求、图像处理、数据处理等耗时操作,提高程序的运行效率。 使用Python多线程的主要步骤如下: 1. 导入threading模块。 ``` import threading ``` 2. 创建一个线程对象,可以通过继承threading.Thread类或使用threading.Thread()函数创建。 ``` class MyThread(threading.Thread): def __init__(self, name): super().__init__() self.name = name def run(self): # 线程执行的代码 print("Hello, " + self.name) thread1 = MyThread("Thread 1") thread2 = threading.Thread(target=func, args=("Thread 2",)) ``` 3. 启动线程。 ``` thread1.start() thread2.start() ``` 4. 等待线程结束。 ``` thread1.join() thread2.join() ``` 以上代码演示了两种创建多线程的方法:1)继承threading.Thread类,重写run方法;2)使用函数作为线程的执行内容。线程的启动调用start()方法,等待线程结束使用join()方法。 需要注意的是,Python多线程的并发程度受到全局解释器锁(GIL)的限制,因此对于计算密集型的任务,多线程并不能发挥出多核的优势。如果需要发挥多核性能,可以考虑使用多进程编程。 总之,Python多线程实例能够提高程序的并发处理能力,适用于需要同时处理多个任务的场景。通过合理设计线程的数量和任务分配,可以提高程序的性能和效率。 ### 回答3: Python多线程实例是指通过使用多线程的技术来提高Python程序的运行效率和性能。在Python中,我们可以使用threading模块来实现多线程多线程技术可以同时执行多个任务,提高程序的运行速度。在Python中,我们可以通过创建Thread对象并调用start()方法来启动一个线程。下面是一个简单的例子: import threading def print_numbers(): for i in range(1, 11): print(i) def print_letters(): for letter in ['A', 'B', 'C', 'D', 'E']: print(letter) # 创建两个线程 t1 = threading.Thread(target=print_numbers) t2 = threading.Thread(target=print_letters) # 启动两个线程 t1.start() t2.start() # 等待两个线程结束 t1.join() t2.join() # 主线程继续执行 print("主线程结束") 以上代码中,我们创建了两个线程,分别执行print_numbers()和print_letters()函数。通过调用start()方法启动线程,并通过join()方法等待两个线程执行完毕。最后,主线程继续执行并打印出一段文字。 需要注意的是,多线程并不一定能提高程序的运行速度,因为在Python中,全局解释器锁(Global Interpreter Lock,GIL)会限制同一时间只能有一个线程执行Python字节码。因此,在CPU密集型任务中,多线程并不能真正实现并行计算。但是,在IO密集型任务中,多线程能够提高程序的运行效率。 总结起来,Python多线程实例可以通过使用threading模块来实现。多线程能够提高IO密集型任务的运行效率,但在CPU密集型任务中并不能真正实现并行计算。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值