[Python学习日记-90] 并发编程之多线程 —— 线程理论
[Python学习日记-90] 并发编程之多线程 —— 线程理论
简介
线程的概念
一、什么是线程
二、线程与进程的区别
三、线程的创建与终止
1、创建
2、终止
四、为什么要用多线程
经典的线程模型
POSIX线程
线程的实现
一、在用户空间和内核空间实现的线程
二、用户级与内核级线程的对比
三、用户级与内核级线程混合实现
简介
并发编程是提升系统性能与资源利用率的核心技术,而多线程则是实现并发的重要手段之一。线程作为操作系统调度的最小单位,是进程内部的一条执行路径,可与同一进程中的其他线程共享内存空间与系统资源(如文件句柄、全局变量等),但拥有独立的程序计数器、栈空间和寄存器状态。本篇我们将介绍线程的概念、线程的模型、线程的创建与终止,以及线程的实现。
线程的概念
一、什么是线程
进程和线程的关系我们可以理解为部门和员工的关系。进程就相当于一个部门,是一个资源单位,是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程相当于是部门内的员工,是真正干活的那一个,是在CPU上执行的单位。而在这关系之间有一些特性:
- 进程内至少有一个线程,而传统操作系统的每个进程都有一个地址空间,而且默认就会有一个控制线程;
- 一个进程内能有多个线程(多线程,即多个控制线程,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间),而线程必须属于一个进程;
- 跨进程的线程之间是不共享数据的,但同一进程下的线程是共享数据的;
- 创建进程的开销远大于创建线程的开销。
在进程之间是竞争关系,线程之间是协作关系。进程(部门)之间是直接竞争的关系,例如迅雷和360,迅雷抢占了其他程序的网络带宽,而360就把迅雷当作病毒给结束了。在一个进程当中不同的线程是协同工作的关系,即同一个进程的线程之间是合作关系,是同一个程序写的程序内开启动,迅雷内的线程是合作关系,不会自己抢自己的网速。
二、线程与进程的区别
维度 | 线程 | 进程 |
---|---|---|
地址空间 | 线程共享创建它的进程的地址空间 | 进程拥有自己独立的地址空间 |
数据访问 | 线程可直接访问其所属进程的数据段 | 进程则拥有父进程数据段的独立副本 |
通信 | 线程能够直接与同一进程中的其他线程进行通信 | 进程必须通过进程间通信机制(IPC)与其他兄弟进程通信 |
创建过程 | 新线程的创建过程更为简便 | 新进程的创建需要复制父进程的资源 |
控制 | 线程能够对同一进程中的其他线程实施较多控制 | 进程仅能对其子进程进行控制 |
外部影响 | 主线程的变化(如取消、优先级更改等)可能会影响进程中其他线程的行为 | 父进程的变化则不会影响子进程 |
三、线程的创建与终止
1、创建
在 Python 中,线程的创建基于threading
模块或_thread
模块(后者为低级接口,不推荐使用),主要有以下两种方式:
通过传入 target 函数创建:
直接实例化 threading.Thread 对象,通过 target 参数指定线程执行的函数,通过 args 或 kwargs 传递函数参数。这种方式更为灵活,适用于简单的函数执行场景。
通过继承 Thread 类创建:
定义一个类继承自 threading.Thread,并重写 run 方法,在 run 方法中编写线程的执行逻辑。这种方式将线程执行代码封装在自定义类中,便于管理和复用。
此外还能使用 concurrent.futures 模块的 ThreadPoolExecutor 来创建线程池管理,通过线程池预先创建一定数量的线程,按需分配任务。这种方式便于资源控制和复用,避免频繁创建和销毁线程带来的开销,如下所示
import concurrent.futuresdef task():return "任务完成"with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:future = executor.submit(task)result = future.result()
2、终止
Python 线程的终止没有像其他语言那样提供强制终止的方法,通常会有以下几种情况会导致线程终止:
正常执行完毕:
线程执行完 run 方法或 target 函数中的所有代码后,自动结束生命周期,这是最理想的终止方式。例如,当线程完成文件读取、网络请求等任务后,线程会正常退出。
通过标志位控制:
在线程内部定义一个标志变量,外部线程通过修改该变量通知目标线程退出。这种方式需要目标线程主动检查标志位,适用于需要优雅退出的场景,如下所示
import threadingstop_flag = Falsedef my_thread_func():global stop_flagwhile not stop_flag:print("线程正在运行")print("线程退出")t = threading.Thread(target=my_thread_func)
t.start()# 外部线程修改标志位
import time
time.sleep(2)
stop_flag = True
异常终止:
当线程执行过程中抛出未捕获的异常时,线程会终止。例如,线程执行除法操作时出现除零错误,或访问不存在的变量,都会导致线程异常退出。开发者可通过 try...except 语句捕获异常,进行资源清理,避免程序崩溃。
守护线程自动终止:
将线程设置为守护线程(daemon=True),当主线程结束时,所有守护线程会被强制终止。守护线程适合执行辅助性任务,例如,后台日志记录、监控等,但需注意其可能在任务未完成时被突然终止。
四、为什么要用多线程
多线程是指在一个进程当中开启了多个线程,多个线程公用同一个地址空间,这可以大大的提高程序的工作效率。我们要使用多线程的原因有以下四点:
- 多线程共享一个进程的地址空间;
- 线程相比起进程更加轻量级,也更容易创建和撤销,在大多数操作系统当中创建线程比创建进程快得多,在大量线程需要动态调整和快速修改时,这非常关键;
- 程序存在大量的计算和I/O处理时多线程能让这些活动彼此重叠运行,从而加快执行速度。但如果是CPU密集型的程序那并不能获得性能上的增强;
- 在多核CPU的系统中,若想最大限度的利用多核,那开启多线程要比开启多进程的开销要小得多,但这一条并不适用与 Python。
其实多线程非常常见,在我们日常使用计算机的时候就有用到,例如,我们打开文本文档打字时,那文本文档的进程干的事情会包含监听键盘输入、处理文字、定时保存文字到硬盘等。而这写操作都是在同一块数据上做的,因而不能用多进程。如果时单线程则会发生键盘输入时文字不能自动保存,自动保存时又不能处理文字。这将非常反人类。
经典的线程模型
多线程是多个线程共享一个进程的地址空间中的资源,在用户的角度来看,多个线程和多个进程所实现的效果是没有差别的,所以有时也会把线程称之为轻量级的进程。而在一台计算机上内存、硬盘、CPU、打印机等这类物理资源都是共享的,在使用这些物理共享资源时多线程和多进程类似,是CPU在多个线程之间快速切换来实现的。

而不同的进程之间是竞争关系,它们之间是充满火药味的,都想要多点CPU、内存和硬盘资源,就如迅雷会和浏览器下载抢网络资源是同一回事。而同一个进程内的线程是由同一个开发者创建的,那它们当然是协同工作的关系,而进程内的线程都能共享所在进程的资源。但是在一些独属于某一线程的数据也要有相应的隔离性,所以同一进程下的每个线程都有一个自己的堆栈,这一点与进程是类似的,但不同的是线程库无法利用时钟中断强制线程让出CPU,需要调用 thread_yield 让线程自动放弃 CPU,然后让给另一个线程。

线程总体看来是有益的,但是也存在一些问题,主要是给程序的设计增加了不小的难度,问题有以下几点:
- 在父进程有多个子线程的情况下,开启的子进程是否需要完全复制父进程当中的多个子进程;
- 在创建子进程的时候,如果父进程中的某个线程被阻塞了,那该进程复制到子进程时,是否需要把阻塞状态也一起复制过去;
- 在同一个进程中的不同线程进行读写操作时还可能带来数据一致性的问题,例如,线程 a 读取了文件数据但未到达用户时,线程 b 又更新了线程 a 刚读取的数据,那线程 a 读取的数据就会与当前硬盘当中存储的数据产生不一致的问题;
- 在同一个进程中的不同线程还存在操作协同的问题,如果一个线程关闭了问题,而另外一个线程正准备往该文件内写内容,那写入的线程就会报错;
- 同时系统的资源分配也存在问题,例如,线程发现没有内存了,并开始分配更多的内存,在工作一半时,发生线程切换,新的线程也发现内存不够用了,又开始分配更多的内存,这样内存就被分配了多次。
POSIX线程
POSIX 线程(POSIX Threads),通常简称为 Pthreads,是 IEEE 在 IEEE 标准 1003.1c 中定义了的线程标准,是为了实现可移植的线程程序,它是一套基于标准 C 语言的线程 API,用于在 Unix、Linux、macOS 等系统上创建和管理多线程程序。它提供了线程创建、同步、调度等功能,使得开发者可以充分利用多核处理器的性能。简单介绍如下
线程调用 | 描述 |
---|---|
Pthread_create | 创建一个新线程 |
Pthread_exit | 结束调用的线程 |
Pthread_join | 等待一个特定的线程退出 |
Pthread_yield | 释放 CPU 来运行另外一个线程 |
Pthread_attr_init | 创建并初始化一个线程的属性结构 |
Pthread_attr_destroy | 删除一个线程的属性结构 |
线程的实现
一、在用户空间和内核空间实现的线程
线程的实现可以分为用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread)两类,后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。
用户空间(用户级线程):
用户级线程内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核 CPU,目前 Linux pthread 用的就是用户级线程。在用户空间模拟操作系统对进程的调度,来调用线程,每个进程中都会有一个运行时系统用来调度线程。此时当该进程获取 CPU 时,进程内再调度出一个线程去执行,但同一时刻只有一个线程执行。
内核空间(内核线线程):
内核级线程的切换就是由内核来控制的,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用 SMP,即利用多核 CPU。Windows 线程就是这样做的。

二、用户级与内核级线程的对比
用户级线程和内核级线程的区别:
用户级线程 | 内核级线程 |
---|---|
OS内核不可感知 | OS内核可感知 |
创建、撤消和调度不需要 OS 内核的支持,是在语言(如 Java)这一级处理的 | 创建、撤消和调度都需 OS 内核提供支持,而且与进程的创建、撤消和调度大体是相同的 |
执行系统调用指令时将导致其所属进程被中断 | 执行系统调用指令时,只导致该线程被中断 |
用户级线程的系统内,CPU 调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行 | 内核支持线程的系统内,CPU 调度以线程为单位,由 OS 的线程调度程序负责线程的调度 |
程序实体是运行在用户态下的程序 | 程序实体是可以运行在任何状态下的程序 |
用户级线程的优缺点:
- 优点:
1.线程的调度不需要内核直接参与,控制简单;
2.可以在不支持线程的操作系统中实现;
3.创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多;
4.允许每个进程定制自己的调度算法,线程管理比较灵活;
5.线程能够利用的表空间和堆栈空间比内核级线程多;
6.同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。 - 缺点:资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用。
内核级线程的优缺点:
- 优点:当有多个处理机时,一个进程的多个线程可以同时执行。
- 缺点:由内核进行调度。
三、用户级与内核级线程混合实现
用户级线程与内核级线程多路复用,使用内核统一调度内核级线程,每个内核级线程对应调度多个用户级线程,如下图所示
