1 - 线程 - Windows 10 - CPython 多线程总纲 - 杂货版

发布时间 2023-03-24 16:46:39作者: Loki_Severus

@

测试环境:

操作系统: Window 10
工具:Pycharm
Python: 3.7

这里就不讲太多复杂的逻辑,直接就是总结,后面就解析,深挖线程。

一、Cpython - 进程与线程的关系 —— 进程并行/线程并发

  1. 进程供给线程资源,而线程帮进程做事;

  2. 进程是线程的容器,线程是执行者;

  3. 进程是计算机基本的资源分配单位,而线程是计算机运行的基本单位 —— 指的是所有的程序其实都是线程组成的,而进程则是给这些线程划分了一个又一个的资源组,一个资源组对应一个进程,这些线程就在各自的资源组内获取对应的资源供给,得以存活并工作(运行);

  4. 一个进程能有很多很多个线程,代码如下所示:

import threading

thread_list = []

def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))


for i in range(100):
    t = threading.Thread(target=action, args=(100,), name='线程'+str(i))  
    thread_list.append(t)
for t in thread_list:
    t.start()
  1. 同一个进程创建的多个线程并发是通过实际推理得出来比较现实点的结果。从我们人的角度来看,我们看到线程的运行很快,但是其实是计算机的运行更快,光速传播,每次运行一个线程的时间可能只有仅仅几毫秒的时间(1秒 =1000 毫秒),几千个线程也不过几秒的时间罢了,所以在我们看来实际上线程的并发已经是很满足我们的需求的了,不太可能想着同一时刻的同一进程内的线程并行,那样不实际,还有我们通常所说的并发是在微观层面发生的 —— 属于计算机的层面,对于计算机来讲几毫秒可能相当于几分钟甚至是几小时;

  2. 既然线程的运行是随机的,而CPU又是只有4个核心,意味着在同一时刻CPU只能同时运行处理 4 个线程,那么如果CPU接下来有5436个线程要处理,那么CPU应该要怎么处理这么多的线程,这个就要提到操作系统的线程调度器,它是一个操作系统服务,会自动分配线程到CPU运行,所以在我看来,所谓的线程并行不太可能,不实际,除非是进程并行,不同的进程分别各自有一个主线程,那么此时就属于一个线程并行,虽然是主线程并行,但也是线程并行,同一个进程内的线程,如果线程数量大于CPU的核心数,那么你觉得能不能在同一时刻内,并行所有的线程,想必是不行的,而且你觉得就算是只有4个线程,那么CPU的4个核心,又怎么可能都分配给同一个进程的线程们,因为线程的运行是随机的,不太可能,概率不高,从微观层面来讲,在同一个时间段内,同一个进程所创建的线程并发还是比较实际一点的

三、多线程的作用

  1. 创建多线程的目的是解决程序延时问题,而在python中的实现方法是创建线程、线程池 或 协程,注意就目前来讲(2022.4.16 - ?),只有python才有协程,其他编程语言还没有出现官方的协程;.

  2. 当CPU处理线程处理不过来时,就会有卡顿一说;

  3. 主线程和子线程在没有其他的方法的影响下,是正常各自运行的,无论是主线程先运行完毕,还是子线程线运行完毕,这些都没有问题,因为开多线程的最终目的就是多开任务,那么就算有一个任务完成了,其他任务也不会跟着退出执行,这是守护线程的作用,但没有其他方法的干涉,多任务执行才是常态,多进程也是如此;

四、多线程如何实现

  1. 如果一个CPU里面有4个核心,那么一个核只能处理一个线程 —— 4核4线程,除非使用了超线程技术,可以让拥有4个核的CPU实现4核心处理8个线程 —— 4核8线程

  2. 线程池指的是用列表存储创建好的线程对象,如上代码所示,用列表存储 100个线程对象,然后用 for 循环将它们全部运行,就是将它们放到一个数据结构内(一个池子内),然后全部运行,不过要注意这里的线程运行是并发运行 —— 同属一个进程内的多个线程并发执行,在这个过程中,线程的运行是随机的,不会有顺序一说,如按线程id号之类的,随便运行哪个线程都可以的;

  3. Python 的线程模块分 _threadthreading_thread模块是thread的兼容模块,thread模块已经在 python3被放弃了,现在使用_thread模块来代替 thread模块,推荐使用 threading模块;

  4. threading模块,使用步骤,import threading ——> 变量名=threading.Thread(tartget=funtion,args=(,),name=) 使用Thread 对象创建线程,target是指定执行的函数,args是传递给函数的参数,name 是指线程名 ——> 线程名.start() 方法开启线程,实际上 start() 方法,是在调用线程类 threading.Thread 内的 run() 方法

五、多线程的特点

  1. 线程分主线程子线程,主线程是进程的第一个线程,子线程是由主线程创建的,当运行一个py文件,如果要创建子线程,就可以在这个py文件内写创建子线程代码,这个py文件就是主线程;

  2. 通常而言,在你使用类unix系统查看进程时(如命令 ps),每一个进程都有一个pid,而每一个线程都会共享一个id号,也就是说每一个线程的 id 号是一样的,因为同一个进程内的线程们是共享所属进程资源的,操作系统不会为线程分配独一无二的id,这和多进程不同,多进程的每一个子进程都有一个独一无二的 id

  3. 进程资源,一般包括系统分配的内存空间、磁盘、权限等等,首先需要了解程序的运行,包括了各种函数/变量的初始化,而通常最容易发生线程冲突的,是在主线程内全局变量这个部分,因为这个时候,主线程的变量们是可以被子线程们调用的,但是局部变量在函数返回后,就消掉了局部变量,无法被其他线程调用了,所以对于线程的共享问题,就需要注意到这一点;
    参考链接:详解 5 种 Python 线程锁

  4. 线程之间是共享数据的,变量共享,一样的内存地址,即共享内存(线程锁的存在意义 —— 解决线程冲突 —— 但是要小心线程死锁问题);
    参考链接:
    python多线程调用同一个函数_python 传递函数给多个线程使用 会不会发生冲突

  5. 线程的通信,由于多线程之间共享数据,所以只需要在主线程这里设置一个数据结构,就可以接收其他线程的数据,这里的数据结构可以是 列表 list、字典dict,元组tuple可以尝试去测试下,这里笔者就不过多讲解;

六、 多线程与线程锁(同步线程 —— 解决线程安全问题)

  1. 在调用某一个相同的变量时,由于线程的共享,需要线程锁,为线程绑定该变量,不被其他线程修改,只有当最先获得线程锁的线程运行完且释放线程锁后,才可以被其他线程获得该线程锁,如此循环线程们交接线程锁 —— 线程同步
    参考链接:Cpython的两种方法解决线程冲突问题

  2. 线程同步,是为了解决线程调用了同一个函数 或者全局变量之类的,注意这里不包含重定向重构方法(知识链接),让这些东西不被其他线程调用后,被处理得面目全非;

七、多线程与异步线程

  1. 有同步,就应该有异步,异步线程是为了解决程序延时问题而提出来的,每当一个程序的运行后发生延时问题,那么就可以考虑异步线程,实现的方法是线程.

  2. 多线程能够并发也能够并行,并发的实现方法是线程锁,正常情况下是线程是可以并行的,如果没有加什么锁的话,比如当前python语言的主流解释器 —— Cpython 解释器内部就有一个机制 GIL (Global Interpreter Lock 全局解释锁),所以在python语言中,多线程,实际上是并发执行的多线程并行在我看来应当是多个进程的主线程并行,当然因为线程运行是随机的,假设一个进程的两个线程在同一时刻被CPU的两个核心同时处理,那么就说是线程并行的,我认为那也没有问题

  3. 线程锁应对 线程同步协程应对 线程异步

  4. 在python语言中,同步对应着堵塞,异步对应着不堵塞,这里的堵塞指的是使用了 time.sleep()线程睡眠方法,要知道python的主流解释器 Cpython 解释器是由一个GIL (Global Interpreter Lock 全局解释锁)的,每次为执行进程中其中一个分发一个 GIL,但是,一个进程只有一个 GIL ,所以每当线程睡眠了,就无法归还 GIL (释放 GIL 锁),所以其他线程也只能跟着一起等待,直到等待的时间结束及该线程完成任务,释放GIL 锁,且如果再加上了一个线程锁,指的是一个线程池内所有的线程都是调用同一个函数,那么在前一个线程完成所有任务前,无法被另外的线程调用该函数,锁住了该函数;

  5. 正常情况下,python创建的多线程其实就是属于一个异步线程,如果不去加 time.sleep()join() 或者设置 daemonTrue,正常创建子线程,那么就会自动运行其他的线程;
    参考链接:Python - 守护线程 / 后台线程 / 精灵线程

八、多线程与守护线程

  1. 所有的线程,无论是主线程还是子线程,其所拥有的 daemon 属性的初始值(默认值)是 False

  2. 线程又可以分为前台线程和后台线程,前台线程是指 daemon 属性默认为 False,后台线程是代码配置daemon属性为 True

  3. 后台线程被称之为守护线程精灵线程,后台只能与前台线程们一同存在,无法独立存在,前台线程都结束运行,那么后台线程也只能跟着结束运行,即使是强制退出运行;

  4. 后台线程配置方法 ——> 线程名.daemon=True 或者 线程名.setDaemon(True)

其他如果还有我会后续补充下