之前几个梳理主要是基于进程的方式进行并发,那上一个小节也聊过关于如果是基于CPython 的解释器的问题,一个时刻只有一个线程可以执行Python代码。
如果:io的相关操作是不会占用cpu,CPU主要是负责计算,而计算肯定是占cpu滴,如想让你的应用更好的利用多核计算机的计算性能,推荐你使用 multiprocessing 或者 concurrent.futures.ProcessPoolExecuto
如果:你想同时运行多个I/O绑定任务,线程仍然是一个合适的模型。
线程是CPU调度的基本单位
线程是和CPU核绑定的,多CPU多核中,各个线程可以占用不同的CPU。(线程可以分配到不同的CPU上执行)
线程都有一个线程ID,和线程控制块(TCB)
线程也和进程一样,有状态转换:如小就绪、阻塞、运行等
线程几乎不拥有系统资源
同一个进程的不同线程共享进程资源,因共享地址空间,同一个进程中的线程间通信不需要系统干预
同一进程中的线程的切换,不会引发进程状态切换。
但是不同的进程中的线程切换,会引起进程切换。(跨进程之前的跨线程通信)
同一进程中的多条线程将共享该进程中的全部系统资源(如虚拟地址空间,文件描述符和信号处理)
但同一进程中的多个线程有:
一个进程可以有很多线程,每条线程可以并行执行不同的任务。
(自己理解的一种对比)
| 对比点 | 多进程 | 多线程 | 说明 |
|---|---|---|---|
| 操作系统层 | 进程是CPU分配资源的基本单位 | 线程是CPU调度的基本单位 | 概念不同 |
| 数据共享 | 复杂(跨进程) | 简单(同进程内) | 共享机制优劣看情况而定 |
| 数据同步(数据通信) | 差不多一样 | 差不多一样 | 都有LOCK和Rlock和Semaphore等 |
| CPU调度 | 慢(进程的上下文和线程上下文) | 快(仅线程的上下文) | 看场景评估 |
| 状态切换 | 不同的进程中的线程切换,会引起进程切换 | 同一进程中的线程的切换,不会引发进程状态切换 | 看场景评估 |
| 内存和CPU | 进程副本占用内存多,上下文切换复杂,CPU利用率低 | 同一个进程内占用内存少,切换简单,CPU利用率高 | 线程占优势(看场景使用) |
| 创建、销毁、切换 | 编程简单,调试简单(数据独享) | 编程复杂,调试复杂(数据共享) | (看场景评估使用) |
| 允许可靠性 | 进程间独立空间,互不影响 | 同一个空间内,线程的异常会引发进程异常 | (看场景评估使用) |
| 分布式 | 适用于多核、多机,扩展到多台机器简单 | 受GIL影响,多核下,一般仅使用IO等任务 | (看场景评估使用) |
辅助说明:
multiprocess库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。
它完整的复制了一套thread所提供的接口方便迁移。 每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢,通过多进程的方式绕过GIL。
上面提到关于multiprocess库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷,它完整的复制了一套thread所提供的接口方便迁移,
所以其实可以看出我们使用线程的时候,多数方法和我们的使用多进程的方式是一样的,和进程一样有:
线程安全:多线程竞争同一个资源保证数据一致性和完整性的一个过程,通常一般是采取加锁机制,进行相关数据临界点的互斥
线程安全一般是:多线程环境下对一些【全局变量及静态变量】引起的同时具有【写操作权限】的时候会出现数据不一致或数据污染的情况,此时会了避免此类情况的发生,一般采取就是加锁的机制。
如果一般的单纯只是具体【读】操作的权限的,通常这个这个【全局变量及静态变量】是线程安全的。
所以线程不安全一般是强调多线程下对【全局变量及静态变量】等资源的写操作权限的情况下引发问题异常。
总结:真正会引导数据冲突的,其实不是读操作,而是写操作。(所以通常再分析代码的时候,可以看是否存在写操作,且写的操作是否会被拆分成多个字节码进行一个资源的写操作)
在多线程环境中,Python程序的运行过程是:
"原子操作(atomic operation)是不需要synchronized",这是多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切 [1] 换到另一个线程)。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。
将整个操作视作一个整体是原子性的核心特征。
原子性不可能由软件单独保证--必须需要硬件的支持,因此是和架构相关的。
在多进程(线程)的操作系统中不能被其它进程(线程)打断的操作就叫原子操作。
当该次操作无法完成时,会回滚到初始状态,原子操作是不可拆分的,(类似数据库的事务处理机制)。
文件的原子操作是指操作文件时的不能被打断的操作。
计算机进程的控制通常由原语完成。
所谓原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断。
在操作系统中,某些被进程调用的操作,如队列操作、对信号量的操作、检查启动外设操作等,一旦开始执行,就不能被中断,否则就会出现操作错误,造成系统混乱。所以,这些操作都要用原语来实现 原语是操作系统核心(不是由进程,而是由一组程序模块组成)的一个组成部分,并且常驻内存,通常在管态下执行。
原语一旦开始执行,就要连续执行完,不允许中断 [1] 。
由于对汇编不是很了解,此部分的内容主要是整理一下别人提供的思路,作为补充。————————————————
以下文字关于原子操作分析的版权声明:文章为CSDN博主「王燕璇」的原创文章,遵循CC 4.0 BY->SA版权协议,转载请附上原文出处链接及声明。原文链接:https://blog.csdn.net/weixin_28925387/article/details/111902381
GIL保证字节码级别的原子性和线程安全性,因此当个字节码执行一定是安全的,执行结果一定是一致的。
而有些操作,底层需要通过【多个字节码】来完成,这样的操作就不是原子的,因此不是线程安全的。
关于dis.dis的模块使用:
参考官网文档:https://docs.python.org/zh-cn/3/library/dis.html
举个例子,a+=1 。
import dis
dis.dis(compile('a+=1', '', 'exec'))
反编译这个语句,发现它由4个字节码组成:查看输出结果:
1 0 LOAD_NAME 0 (a)
2 LOAD_CONST 0 (1)
4 INPLACE_ADD
6 STORE_NAME 0 (a)
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
这个简单的语句,背后需要 4 个字节码协作完成:LOAD_NAME 将 a 当前的值加载进运行栈;
LOAD_CONST 将常量 1 加载到运行栈;
INPLACE_ADD 对栈上两个操作数进行加法运算;
STORE_NAME 将计算结果保存;
如果你学过汇编的话,你会发现Python字节码跟汇编指令非常像!GIL保证当个字节码的执行不会受到其他线程的任何干扰,但是任何字节码间都可能发生线程切换。
理解字节码执行:
参考通俗易懂:说 Python 里的线程安全、原子操作 作者:王一白 链接:https://juejin.cn/post/6844904159120998408
总结:印证线程安全的概念
线程安全:多线程竞争同一个资源保证数据一致性和完整性的一个过程,通常一般是采取加锁机制,进行相关数据临界点的互斥
线程安全一般是:多线程环境下对一些【全局变量及静态变量】引起的同时具有【写操作权限】的时候会出现数据不一致或数据污染的情况,此时会了避免此类情况的发生,一般采取就是加锁的机制。
如果一般的单纯只是具体【读】操作的权限的,通常这个这个【全局变量及静态变量】是线程安全的。
所以线程不安全一般是强调多线程下对【全局变量及静态变量】等资源的写操作权限的情况下引发问题异常。
总结:真正会引导数据冲突的,其实不是读操作,而是写操作。(所以通常再分析代码的时候,可以看是否存在写操作,且写的操作是否会被拆分成多个字节码进行一个资源的写操作)
早期的实现线程thread模块(已不推荐使用了),建议直接使用threading模块(推荐使用)。
threading.main_thread()
返回主 Thread 对象。一般情况下,主线程是Python解释器开始时创建的线程。
threading.active_count()
返回当前存活的 Thread 对象的数量。返回值与 enumerate() 所返回的列表长度一致。
threading.enumerate()
以列表形式返回当前所有存活的 Thread 对象。该列表包含守护线程,current_thread() 创建的虚拟线程对象和主线程。它不包含已终结的线程和尚未开始的线程。
threading.current_thread()
返回当前对应调用者的控制线程的 Thread 对象。如果调用者的控制线程不是利用 threading 创建,会返回一个功能受限的虚拟线程对象。
threading.get_ident()
返回当前线程的 “线程标识符”。它是一个非零的整数。它的值没有直接含义,主要是用作 magic cookie,比如作为含有线程相关数据的字典的索引。线程标识符可能会在线程退出,新线程创建时被复用。
threading.settrace(func)
为所有 threading 模块开始的线程设置追踪函数。在每个线程的 run() 方法被调用前,func 会被传递给 sys.settrace() 。
threading.setprofile(func)¶
为所有 threading 模块开始的线程设置性能测试函数。在每个线程的 run() 方法被调用前,func 会被传递给 sys.setprofile() 。
threading.stack_size([size])
返回创建线程时用的堆栈大小
threading.TIMEOUT_MAX (3.2 新版功能.)
阻塞函数( Lock.acquire(), RLock.acquire(), Condition.wait(), ...)中形参 timeout 允许的最大值。传入超过这个值的 timeout 会抛出 OverflowError 异常。
threading.local
一个代表线程本地数据的类
线程的创建:
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
参数:
如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.init())。
线程对象几个方法:
不加join的话,主线程和子线程完全是并行的,加了join主线程得等这个子线程执行完毕,才能继续往下走。如果需要获取程序运作总时间,一般需要使用这种方式,不然主进程和子线程并行的时候,获取统计获取具体的时间。
关于join 这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结 -- 不管是正常终结还是抛出未处理异常 -- 或者直到发生超时,超时选项是可选的。
当 timeout 参数存在而且不是 None 时,它应该是一个用于指定操作超时的以秒为>单位的浮点数(或者分数)。因为 join() 总是返回 None ,所以你一定要在 join() 后调用 is_alive()才能判断是否发生超时 -- 如果线程仍然存活,则 join() 超时。
当 timeout 参数不存在或者是 None ,这个操作会阻塞直到线程终结。
一个线程可以被 join() 很多次。
如果尝试加入当前线程会导致死锁, join() 会引起 RuntimeError 异常。如果尝试 join() 一个尚未开始的线程,也会抛出相同的异常。
使用方法的方式:
import threading, time
def my_run(msg):
print("子线程的名称:", threading.current_thread().getName())
print("开始任务", msg)
time.sleep(2)
print("任务结束,", msg)
t1 = threading.Thread(name='线程1', target=my_run, args=("我是你大爷!",))
t2 = threading.Thread(name='线程2', target=my_run, args=("我是你大大的少爷!",))
print("当前每个进程都会有一个主线程,名字是:", threading.main_thread().getName())
start_time = time.time()
t1.start()
t2.start()
# 不加join的话,主线程和子线程完全是并行的,加了join主线程得等这个子线程执行完毕,才能继续往下走。这样才能得到这个程序真正的运行时间。
t1.join()
# 不加join的话,主线程和子线程完全是并行的,加了join主线程得等这个子线程执行完毕,才能继续往下走。这样才能得到这个程序真正的运行时间。
t2.join()
print('运行总耗时:', time.time() - start_time)
输出结果:
当前每个进程都会有一个主线程,名字是:MainThread
子线程的名称: 线程1
开始任务 我是你大爷!
子线程的名称: 线程2
开始任务 我是你大大的少爷!
任务结束, 我是你大爷!
任务结束, 我是你大大的少爷!
运行总耗时:2.0076282024383545
使用面向对象的方式:
#!/usr/bin/evn python
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
文件名称 : 线程1
文件功能描述 : 功能描述
创建人 : 小钟同学
-------------------------------------------------
-------------------------------------------------
"""
import threading,time
class TestThread(threading.Thread):
def __init__(self,msg,sleep_time):
super(TestThread, self).__init__()
self.msg=msg
self.sleeptime=sleep_time
print("获取所属的主线程的名称:", threading.current_thread().getName())
# 需要执行的任务,调用start之后,程序会处于等待操作系统进行调度,到时间的是,会自己调用run
def run(self):
print("子线程的名称:", threading.current_thread().getName())
print("开始任务",self.msg)
time.sleep(2)
print("任务结束,",self.msg)
t1=TestThread("线程1",'我是你大爷!')
t2=TestThread("线程2",'我是你大大的少爷!')
print("当前每个进程都会有一个主线程,名字是:",threading.main_thread().getName())
start_time=time.time()
t1.start()
t2.start()
# 不加join的话,主线程和子线程完全是并行的,加了join主线程得等这个子线程执行完毕,才能继续往下走。这样才能得到这个程序真正的运行时间。
t1.join()
# 不加join的话,主线程和子线程完全是并行的,加了join主线程得等这个子线程执行完毕,才能继续往下走。这样才能得到这个程序真正的运行时间。
t2.join()
print('运行总耗时:',time.time()-start_time)
输出结果:
获取所属的主线程的名称:MainThread
获取所属的主线程的名称:MainThread
当前每个进程都会有一个主线程,名字是:MainThread
子线程的名称:Thread-1
开始任务 线程1
子线程的名称:Thread-2
开始任务 线程2
任务结束, 线程1
任务结束, 线程2
运行总耗时:2.0106277465820312
个人其他博客地址
简书:https://www.jianshu.com/u/d6960089b087
掘金:https://juejin.cn/user/2963939079225608
小钟同学 | 文 【原创】| QQ:308711822
1:本文相关描述主要是个人的认知和见解,如有不当之处,还望各位大佬指正。 2:关于文章内容,有部分内容参考自互联网整理,如有链接会声明标注;如没有及时标注备注的链接的,如有侵权请联系,我会立即删除处理哟。
原文作者:程序员小钟同学;如有侵权,请联系管理员删除。