当前位置: 首页 > news >正文

线程、进程、协程

线程、进程和协程是计算机科学中并发编程的三个核心概念

进程、线程、协程

进程

进程是操作系统进行资源分配和保护的基本单位。你可以把它理解为一个独立运行的程序实例(比如一个.exe程序)。当一个程序被运行时,操作系统会为其创建一个进程,并分配独立的内存地址空间、系统资源(如打开的文件、网络连接等)以及一个被称为进程控制块(PCB)​ 的数据结构,PCB是进程存在的唯一标志,操作系统通过它来管理和调度进程。

进程不是指CPU的一个核,而是指运行中的一个应用程序(比如一个.exe程序),CPU的核心是硬件执行单元,而进程是操作系统管理的软件实体,一台机器上同时存在的进程数量动态变化,远多于CPU核心数。

    线程

    线程是进程中实际执行任务的基本单位,也是操作系统调度和CPU执行的基本单位​。一个进程至少有一个线程(主线程),也可以创建多个线程。所有同属于一个进程的线程共享该进程的所有资源​(如内存空间、全局变量、打开的文件等)。但每个线程有自己独立的线程上下文​(如程序计数器、寄存器集合和栈),用于记录自己的执行状态。

    在CPU的单核上,操作系统通过时间片轮转方式实现并发。操作系统调度器会为每个就绪状态的线程分配一个极短的时间片(通常是几毫秒到几十毫秒)。当一个线程的时间片用完后,CPU会切换到下一个线程。由于切换速度极快,用户感知上就像是多个线程在同时运行。(关于时间片轮转的详细说明参考下文“并发编程”)

    在多核CPU系统中,每个核心的运行队列上,可能会有来自不同进程的多个线程在排队等待执行。操作系统为每个CPU核心都维护着一个独立的运行队列(Runqueue)​。这个队列里存放的是所有处于“就绪”状态、等待该核心执行的任务(线程)。调度器会根据复杂的算法(如Linux中的CFS)来决定队列中线程的排序和选择。

    同一个进程的多个线程会优先分到不同核心上还是分配到同一个核上?调度器会持续监控所有核心的负载,目标是将工作负载尽可能均匀地分布到所有核心上,这通常意味着默认情况下,同一个进程的多个线程会被倾向于分配到不同的核心上并行执行,以最大化利用CPU资源,提升整体性能。但是,当一个线程在某个核心上执行后,其数据和指令会残留在该核心的缓存中(CPU的缓存体系(L1、L2、L3缓存)),如果下次它还能被调度到同一个核心上运行,就能命中缓存,大大减少访问内存的延迟,提升效率,频繁地在核心间迁移线程会导致缓存失效,性能下降。因此,调度器总是在追求全局负载均衡和尽量保持线程的缓存亲和性之间做权衡

      进程与线程

      特性

      进程 (Process)

      线程 (Thread)

      本质

      资源分配的容器

      CPU执行的单元

      与CPU的关系

      不直接在被CPU调度和执行

      直接作为任务被CPU调度和执行

      资源拥有

      拥有独立的地址空间、资源(如文件句柄、内存)

      基本上不拥有系统资源,但共享其所属进程的资源

      开销

      创建、切换开销大

      创建、切换开销小

      独立性

      进程间相互独立,一个进程崩溃通常不会影响其他进程

      同一进程内的线程共享进程资源,缺乏独立性

      通信

      进程间通信(IPC)复杂,需通过操作系统(如管道、消息队列)

      线程间通信简单,可直接读写共享的进程数据(但需同步)

      进程更像一个容器或舞台,它提供了程序运行所需的一切资源和一个独立的执行环境。

      线程调度机制

      当CPU核心空闲下来需要挑选下一个执行的线程时,它取决于操作系统采用的调度策略​​和线程的优先级。

      • 完全公平调度(CFS - Completely Fair Scheduler):这是Linux中针对普通进程的默认调度策略。它的目标是给所有线程提供“公平”的CPU时间份额
        • 其核心思想是为每个线程计算一个虚拟运行时间(vruntime)​,它表示该线程已消耗的CPU时间经过其优先级(权重)调整后的值。
        • 调度器总是选择vruntime最小的线程来执行,这样可以补偿那些之前获得CPU时间较少的线程,从而实现“完全公平”
        • 线程的优先级(nice值)​​ 会影响vruntime的增长速度,高优先级线程的vruntime增长更慢,从而有机会获得更多的CPU时间。
      • 实时调度(RT - Real-Time Scheduling):对于有严格时限要求的任务(如工业控制、音频处理),操作系统提供了实时调度策略
        • SCHED_FIFO​:相同优先级的线程按先来先服务顺序执行,一旦占用CPU便会一直运行直到结束、主动让出或被更高优先级的线程抢占。不同优先级的线程,​更高优先级的线程总是可以抢占低优先级线程
        • SCHED_RR​:与FIFO类似,但相同优先级的线程会以时间片轮转(Round-Robin)​​ 的方式执行,每个线程分配一个时间片,用完后排到同优先级队列的末尾,等待下次调度
        • 实时线程的优先级通常最高,一旦就绪,可以抢占所有CFS调度类的普通线程。

      特性

      CFS (完全公平调度)

      SCHED_RR (实时轮转)

      SCHED_FIFO (实时先进先出)

      设计目标

      公平性​ (Fairness),所有进程按权重比例分享CPU

      确定性​ (Determinism),为实时任务提供有保证的响应

      确定性​ (Determinism),最高优先级的任务立即运行直至完成

      调度策略

      选择虚拟运行时间(vruntime)​最小的进程

      固定优先级​ + ​时间片轮转

      固定优先级,​无时间片,可无限运行直至阻塞或让出

      优先级范围

      普通优先级​ (nice值 -20 到 19,映射到优先级100-139)

      实时优先级​ (1 到 99,数字越大优先级越高)

      实时优先级​ (1 到 99,数字越大优先级越高)

      适用场景

      绝大多数普通进程​ (如桌面应用、服务器后台任务)

      软实时任务​ (需要定期轮转执行,如多媒体流、控制循环)

      硬实时任务​ (要求立即响应且运行时间短,如中断处理、紧急信号)

      抢占关系

      可被任何实时进程抢占

      可被更高优先级的实时进程抢占,同优先级按时间片轮转

      可被更高优先级的实时进程抢占,同优先级按FIFO顺序执行

      Linux内核的调度器是一个分层结构。不同类型的进程被放入不同的调度类(sched_class),这些调度类按固定的优先级顺序进行检查:实时调度类(SCHED_FIFO/SCHED_RR) > 完全公平调度类(CFS/SCHED_OTHER) > 空闲调度类(SCHED_IDLE)

      调度器工作的核心流程是:总是优先检查最高级别的调度类中是否有待运行的进程。​只有当所有实时进程(SCHED_FIFO/SCHED_RR)都处于休眠或阻塞状态时,CFS进程才有机会获得CPU

      SCHED_RR的时间片是固定、绝对的,操作系统给每个相同优先级的实时进程分配一个固定的时间片(比如100ms),时间片用完就必须让出CPU,排到同优先级队列的末尾,等待下次轮转,这是一种强制性的公平。​CFS的“时间片”是动态、相对的CFS没有传统意义上的固定时间片,它的核心是虚拟运行时间(vruntime)​,CFS的目标是让所有进程的vruntime尽可能相等。CFS也会计算一个“时间片”,但这个时间片是动态的,其大小与进程的权重(由nice值决定)成正比,并受调度延迟和最小粒度等参数影响,是一种补偿性的公平;SCHED_FIFO的策略与“时间片轮转”的概念截然不同,因为它服务于最高优先级、最需要确定性的任务,一个SCHED_FIFO进程一旦开始运行,就会一直运行下去,直到它自己主动放弃CPU​(比如调用了sched_yield()、进入了休眠状态等待I/O),或者被一个优先级更高的实时进程抢占。

      协程

      协程(Coroutine)本质上是一种用户态的轻量级线程,它允许在单个线程中通过协作式多任务处理来实现并发。与进程和线程不同,协程的调度完全由用户程序控制,而非操作系统内核。可以将其理解为一种可以暂停和恢复执行的函数。它能在执行过程中主动让出(yield)CPU,并在需要时恢复(resume)执行,从而在单个线程内实现多个任务的交替执行。协程本质是单线程并发,无法利用多核。

      协程的核心在于挂起(Suspend)​​ 和恢复(Resume)​​:

      • 挂起​:协程在执行过程中,可以在特定点(如遇到I/O操作或通过yieldawait等关键字)主动暂停执行,保存当前的执行上下文(如程序计数器、局部变量等),并将控制权交还给调度器或其他协程。

      • 恢复​:当之前暂停的条件满足时(如I/O操作完成),调度器可以恢复该协程的执行,从之前暂停的地方继续执行。

      这种“挂起-恢复”机制使得协程在等待I/O等耗时操作时,可以主动让出CPU,让线程去执行其他就绪的协程,从而极大提高了CPU的利用率,避免了线程因阻塞而空转

      协程实现方式主要有两种:

      类型

      原理

      特点

      代表语言

      有栈协程 (Stackful)​

      为每个协程分配独立的调用栈。挂起时保存完整的栈信息。

      可以在任意函数调用深度挂起。切换开销相对无栈稍大。

      Go (goroutine), Lua

      无栈协程 (Stackless)​

      通过状态机闭包实现。挂起时仅保存必要状态(如局部变量)。

      通常只能在定义的语法点(如await)挂起。切换开销极低。

      Python (async/await), JavaScript, Kotlin

      Python 中可以使用 asyncio库来实现协程。下面是一个简单的示例,模拟两个协程并发执行网络请求(用 asyncio.sleep模拟网络延迟):

      import asyncio
      import timeasync def fetch_data(task_name, delay):"""模拟一个网络请求协程"""print(f"{task_name}: 开始请求数据,大约需要 {delay} 秒")start_time = time.time()await asyncio.sleep(delay)  # 关键!使用异步等待,模拟非阻塞I/Oend_time = time.time()print(f"{task_name}: 数据接收完成!耗时 {end_time - start_time:.2f} 秒")return f"{task_name} 的数据"async def main():"""主协程,负责调度其他协程"""# 使用 asyncio.gather 并发运行多个协程results = await asyncio.gather(fetch_data("任务A", 2),fetch_data("任务B", 3),fetch_data("任务C", 1))print("所有任务完成!结果:", results)# 运行主协程
      asyncio.run(main())

      输出结果可能是:

      任务A: 开始请求数据,大约需要 2 秒
      任务B: 开始请求数据,大约需要 3 秒
      任务C: 开始请求数据,大约需要 1 秒
      任务C: 数据接收完成!耗时 1.00 秒
      任务A: 数据接收完成!耗时 2.00 秒
      任务B: 数据接收完成!耗时 3.00 秒
      所有任务完成!结果: ['任务A 的数据', '任务B 的数据', '任务C 的数据']

      GIL对Python多线程、多进程的影响

      高并发I/O操作:高并发I/O操作指的是系统需要同时处理大量输入输出请求的场景,其性能瓶颈主要在于磁盘、网络等外部设备的读写速度,而非CPU的处理能力。任务执行过程中有大量时间花费在等待网络数据包(如用户HTTP请求)到达或磁盘读写完成上,此时CPU经常处于空闲状态。例如,当线程A发起一个读取文件的请求后,操作系统的内核会接手这个请求,并向磁盘控制器发出指令,此时,线程A的工作就暂时告一段落了,它必须等待磁盘驱动器找到数据、读取数据、并通过DMA(直接内存访问)将数据写入内存的整个物理过程完成,这个等待过程可能非常漫长(以CPU的速度来看)。如果没有多线程,CPU核心就只能空转,白白浪费巨大的计算能力。

      CPU密集型计算:CPU密集型计算(又称计算密集型)任务的特点是需要进行大量、复杂的运算,其性能瓶颈主要在于CPU的处理速度和核心数量。与I/O密集型任务相比,此类任务较少进行读写磁盘或网络通信等操作。CPU密集型计算的许多计算任务可以分解成多个子任务,在多个CPU核心上并行执行以加速计算​

      GIL(全局解释器锁):GIL(Global Interpreter Lock)是 ​CPython 解释器​(Python 编程语言的官方参考实现和最主流的解释器,由 Python 软件基金会维护,使用 C 语言编写,它负责将 Python 代码编译成字节码,然后在 Python 虚拟机 (PVM) 中解释执行,此外还有Jython、IronPython、MicroPython等多种Python解释器,需要注意的是,GIL 是 ​CPython​ 的特性。像 Jython(基于 Java)或 IronPython(基于 .NET)这样的解释器就没有 GIL)中的一个机制,它是一把全局互斥锁(mutex),核心作用是保证在同一时刻,只有一个线程可以执行 Python 字节码。在早期,GIL主要是为了简化内存管理而设计的,CPython 使用引用计数作为其主要的内存管理机制,每个 Python 对象都有一个引用计数(用来记录当前有多少个引用指向它自己),当一个对象的引用计数变为 ​0​ 时,意味着没有任何变量或数据结构再需要它,CPython 就会立即销毁该对象,并回收其占用的内存,如果在没有 GIL 的情况下,多个线程同时修改同一个对象的引用计数(例如,一个线程增加计数,另一个线程减少计数)会导致竞态条件,可能引发内存错误(如对象被错误释放)或数据损坏。GIL​ 是解释器层面的锁,用于保护 Python 解释器内部的状态(如引用计数),确保字节码执行的原子性。我们平常使用的线程锁​(threading)是应用程序层面的锁,由开发者显式地使用(如 threading.Lock()),用来保护业务逻辑中的共享数据,防止多个线程同时修改它们而导致数据不一致。

      关于Python字节码是什么,下图给出了字节码在Python程序执行过程中的位置和作用:字节码既不是你能直接编写的源代码,也不是计算机CPU直接执行的机器码,而是一种中间表示,它是一种平台无关的、低级的中间代码,它比源代码更接近机器码,但并非真正的机器指令。编译后的字节码通常会被缓存到 .pyc文件中,以便下次直接加载,加快启动速度。CPython 将你的 Python 源代码编译成的一种平台无关的、低级的指令集,它由 Python 虚拟机 (PVM) 解释执行。GIL 确保了一次只有一条线程可以向 PVM 送入字节码指令并执行。

      下面介绍GIL对python多线程和多进程的影响:

      根据上面的介绍,GIL的核心规则是一个Python进程中的多个线程不能同时执行Python字节码​,这意味着,​无论你的机器是单核还是多核,只要是在同一个Python进程内开辟多个线程执行Python代码,GIL的限制就存在,GIL对Python多线程影响如下:

      • 如果是单核CPU,多个线程本来也无法真正并行执行。操作系统会通过时间片轮转进行调度和切换。GIL的存在使得Python线程的切换不仅受操作系统调度,还增加了获取和释放GIL的环节。对于I/O密集型任务,线程在等待I/O时会释放GIL,其他线程可以获取GIL继续执行,因此多线程在单核上也能有效提升并发吞吐量。但对于CPU密集型任务,线程会一直占用CPU和GIL,直到时间片用完或主动释放(但纯计算很少主动释放)。线程切换和GIL争夺都会带来额外开销,甚至可能导致多线程比单线程更慢
      • 如果是多核CPU,操作系统的线程调度器会将一个python进程内的多个线程分配到多个CPU核心上​。然而,由于GIL的存在,这些被分配到不同核心上的线程并不能同时执行Python代码。它们会争抢同一个GIL,导致实际上仍然只有一个线程在某个核心上运行Python字节码,其他核心上的线程则处于等待GIL的状态。这造成了多核计算资源的浪费,使得多线程无法充分利用多核优势处理CPU密集型任务。但是对于I/O密集型任务,多线程还是要优于单线程,它的高效性完全来自于一个核心在执行计算时,其他线程的I/O操作可以在另一个核心上等待或完成,从而高效地重叠I/O等待时间。

      但是对于同时运行的多个python程序,它们是多个独立的进程,都有独立的Python解释器进程,每个有自己的GIL和内存空间,这时将它们运行在CPU的多个核上就不会产生影响。也就是说,​GIL 并不限制多进程。恰恰相反,​多进程是规避 GIL 对 CPU 密集型任务限制的首选方案​。对于一段计算密集型的Python程序,要利用多进程提升运行速度,关键在于将任务分解成多个可以独立计算的子任务,multiprocessing模块是Python标准库中用于实现多进程编程的主要工具。但是创建进程比创建线程的资源开销大得多,而且进程间不共享内存,数据交换需要通过 ​IPC(进程间通信)​,如 multiprocessing.QueuePipe或共享内存,这比线程间直接共享内存的通信方式速度慢且更复杂,不过对于 ​CPU 密集型任务,虽然多进程有额外开销,但因为它能真正实现并行计算,其带来的性能收益通常远远超过这些开销​

      多线程、多进程、多协程的选择

      特性

      多进程 (Multiprocessing)

      多线程 (Multithreading)

      多协程 (Coroutine)

      核心特点

      独立内存空间,由操作系统调度

      共享进程内存,由操作系统调度

      共享线程内存,由用户态事件循环调度

      并行能力

      ⭐⭐⭐⭐⭐
      真正并行,可充分利用多核CPU

      ⭐⭐
      受GIL限制,​并发但非真正并行


      单线程内并发,无法直接利用多核

      开销成本

      高(创建/切换慢,内存占用大)

      中(创建/切换较快,内存占用适中)

      极低​(创建/切换极快,内存占用小)

      数据共享

      复杂,需IPC(如队列、管道)

      简单,​直接共享内存​(需线程锁同步)

      简单,直接共享内存(无需锁同步

      编程复杂度

      中(需处理进程间通信)

      ​(需谨慎处理线程安全竞态条件

      中(需适应异步编程范式

      适用任务

      CPU密集型​(计算为主)

      I/O密集型​(阻塞等待为主)

      高并发I/O密集型​(大量网络连接)

      典型场景

      科学计算、图像处理、数据加密

      文件读写、数据库查询、Web服务器请求处理

      微服务、高性能网络应用、爬虫

      Python库

      multiprocessing, concurrent.futures

      threading, concurrent.futures

      asyncio, gevent

      并发编程与并行编程

      并发编程

      并发编程是一种编程范式,它允许程序中的多个任务在同一时间段内执行。这就像是让程序从一个“单线处理”模式切换到了“多线协同”模式,目的是为了更高效地利用计算资源,提升程序的执行效率和响应能力。但是并发并不是真正意义上的多个任务在同一时间段内都在执行,这些任务在CPU某个核上是通过时间片轮转“交替”运行,只是在宏观上看起来同时发生。这就像一位厨师同时照看几口锅炒菜,他无法真正同时翻炒所有锅,但通过快速地在每口锅前轮流翻炒一小会儿,最终所有菜都能炒熟,并且给顾客的感觉是“大厨在同时炒好几道菜”。并发本质上是“单个核心上交替执行多个任务”​​ 的技术

      下面这个表格汇总了时间片轮转调度中的核心角色和过程:

      角色/过程

      说明

      调度器 (Scheduler)​

      操作系统的核心组件,负责按照规则分配CPU时间,决定下一个运行的任务。

      就绪队列 (Ready Queue)​

      所有已准备好、等待CPU时间的任务排成的队列,通常是FIFO(先进先出)。

      时间片 (Time Quantum)​

      CPU分配给每个任务的固定执行时间单位,通常为几十毫秒。

      时钟中断 (Clock Interrupt)​

      硬件定时器发出的信号,是调度器收回CPU控制权、进行任务切换的关键机。

      上下文切换 (Context Switch)​

      在切换任务时,保存当前任务状态、加载下一个任务状态的过程。

      时间片轮转调度具体的工作流程可以分解为以下几个关键步骤:

      1. 任务就绪与排队​:所有准备好运行的任务(线程/进程)会被操作系统放入一个叫做“就绪队列”的队列中排队,通常是按照先来先服务(FIFO)的顺序
      2. 分配时间片​:调度器从就绪队列的队首取出任务,并赋予它一个间片​(例如100毫秒)的CPU执行时间,然后开始运行该任务
      3. 时间片耗尽或任务阻塞​:最常见的情况是任务在时间片内没有执行完,但时间片用尽了。这时,硬件会发出一个时钟中断,CPU会暂停当前任务,并通知操作系统。任务主动让出,任务在时间片结束前,因需要等待某些资源(如I/O操作完成)而主动阻塞自己(例如等待用户输入),也会立刻引发切换。
      4. 保存上下文​:操作系统介入,将当前正在运行任务的执行状态(称为上下文,主要包括CPU寄存器、程序计数器等的值)保存到该任务的进程控制块(PCB)中。这样,后续恢复时就能从精确的断点继续执行。
      5. 移至队尾并选择新任务​:刚才被中断的任务会被重新放回就绪队列的末尾,等待下一轮调度。然后,调度器从就绪队列的队首取出下一个任务​。
      6. ​加载上下文并执行​:操作系统将新任务的上下文从它的PCB中加载到CPU的寄存器和程序计数器,然后CPU开始执行这个新任务。​这个过程(步骤4和6)就是“上下文切换”​​。
      7. 循环往复​:上述过程不断重复,就绪队列中的每个任务都会轮流获得CPU时间片,直到所有任务都执行完毕。

      这个过程可以通过下图更直观地理解:

      时间片的设置是一个非常关键的权衡:

      • 时间片太短​:会导致非常频繁的上下文切换。保存和恢复状态本身就有开销,如果切换花费的时间占比过高,CPU真正用于执行任务的时间就少了,整体效率会下降。
      • 时间片太长​:又会使系统的响应速度变慢。如果队列末尾有一个交互式任务(比如等待用户输入),它可能需要等待很长时间才能轮到自己,用户会感觉到明显的卡顿。

      因此,时间片的大小通常是一个折中的值(例如几十到一百毫秒),需要在切换开销系统响应性之间取得平衡。

      单核CPU上的时间片轮转,其核心就是通过时钟中断上下文切换这两个机制,强制当前运行的任务让出CPU,让下一个任务有机会运行。通过这种快速的、循环的轮流执行方式,在宏观上模拟出了“多任务同时运行”的效果。

      并行编程

      ​并行​是指同一时刻真正同时执行多个任务,这必须依赖多核处理器或多个CPU等硬件支持。

      以多核CPU为例,多核CPU的每个核心都能独立执行一个线程任务,这是真正的同时执行。但这并不意味着多核CPU没有时间片轮转,当系统需要处理的任务数量远多于CPU核心数量时,每个核心上仍然需要通过时间片轮转机制在多个任务之间快速切换。

      操作系统的调度器会这样安排任务:​首先尽力将多个线程分配到不同的核心上并行执行,这是真正的物理并行。在每个核心上,如果被分配了多个线程(实际情况几乎总是如此),操作系统依然需要通过时间片轮转的方式,让这些线程轮流使用该核心的时间片。这就是逻辑上的并发。所以,在多核CPU上,​并行(Parallelism)和并发(Concurrency)是同时存在的​:并行是“多个核心同时工作”,而并发是“单个核心上交替执行多个任务”。​

      并行编程本质是将一个大的计算任务分解成多个可以同时执行的子任务,将这些子任务分配给多个计算单元(如CPU核心)去执行,最后将这些局部结果整合成最终结果。这个过程的关键在于任务分解、任务分配与同步通信。

      实现并行编程可以借助许多优秀的工具和库,它们能简化开发流程:

      工具/库名称

      主要特点与适用场景

      OpenMP​ (Open Multi-Processing)

      主要用于C/C++/Fortran,支持共享内存模型,通过编译器指令实现并行化,非常适合在单台多核机器上进行循环并行化

      MPI​ (Message Passing Interface)

      用于跨节点的分布式内存系统,进程间通过消息传递进行通信,适合大规模科学计算和超级计算机

      CUDA

      由NVIDIA推出的并行计算平台和编程模型,允许使用C++等语言开发在GPU上运行的并行程序,极大加速计算密集型任务

      Java Concurrent Package​ (java.util.concurrent)

      提供了丰富的并发工具,如线程池、阻塞队列、原子变量等,便于构建高效可靠的多线程应用程序

      Python multiprocessing

      通过创建多个进程规避GIL限制,充分利用多核处理CPU密集型任务

      Python concurrent.futures

      提供了高级的线程池和进程池接口,简化了异步任务执行


      文章转载自:

      http://CIckbTlz.fqkLt.cn
      http://Vgd3WpgV.fqkLt.cn
      http://csSePoHJ.fqkLt.cn
      http://YmfOhDaW.fqkLt.cn
      http://j9HCZScr.fqkLt.cn
      http://4SJVJaMY.fqkLt.cn
      http://YwYS4o0F.fqkLt.cn
      http://CR5c4fBI.fqkLt.cn
      http://53r6KusQ.fqkLt.cn
      http://RyGbV5h3.fqkLt.cn
      http://zsSxns3S.fqkLt.cn
      http://AxLkMhv9.fqkLt.cn
      http://XVg2p4Uj.fqkLt.cn
      http://rqMGLFnS.fqkLt.cn
      http://R6e9IpKG.fqkLt.cn
      http://OFNiTQ2S.fqkLt.cn
      http://uzZ8Lk7B.fqkLt.cn
      http://OukbtftA.fqkLt.cn
      http://krnzpGeo.fqkLt.cn
      http://lBHs4kOP.fqkLt.cn
      http://cp8tlxNI.fqkLt.cn
      http://U0Stq5D9.fqkLt.cn
      http://5Tv7UI0x.fqkLt.cn
      http://BKocugBh.fqkLt.cn
      http://jOmJtZZL.fqkLt.cn
      http://lIPB04UY.fqkLt.cn
      http://CrZCDn1l.fqkLt.cn
      http://Qdl5s54o.fqkLt.cn
      http://vGovpqs8.fqkLt.cn
      http://JyFZByHH.fqkLt.cn
      http://www.dtcms.com/a/388522.html

      相关文章:

    • Java/注解Annotation/反射/元数据
    • C++学习:哈希表的底层思路及其实现
    • 机器学习python库-Gradio
    • 创作一个简单的编程语言,首先生成custom_arc_lexer.g4文件
    • 湖北燃气瓶装送气工证考哪些科目?
    • MySQL死锁回滚导致数据丢失,如何用备份完美恢复?
    • Zustand入门及使用教程(二--更新状态)
    • Matplotlib统计图:绘制精美的直方图、条形图与箱线图
    • 在el-table-column上过滤数据,进行格式化处理
    • 记一次golang结合前端的axios、uniapp进行预签名分片上传遇到403签名错误踩坑
    • 十一章 无界面压测
    • 多色印刷机的高精度同步控制:EtherCAT与EtherNet/IP的集成应用
    • 【随笔】【蓝屏】DMA错误
    • Coze源码分析-资源库-创建工作流-后端源码-IDL/API/应用/领域层
    • 5 分钟将网站打包成 APP:高效实现方案
    • 物联网智能网关核心功能实现:解析西门子1500 PLC的MQTT通信配置全流程
    • 新国标电动自行车实施,BMS 静电浪涌风险与对策
    • 【Python】Python文件操作
    • C#如何使用ApiPost接口,将数据显示在unity面板
    • 零基础从头教学Linux(Day 36)
    • 深度学习(2)
    • 火山 17 声音回调
    • Flash芯片的VCC上电到可操作时间过长
    • CSP-S——各算法可以实现的问题
    • 第十七章 Arm C1-Premium性能监控单元(PMU)事件详解
    • vue锚点导航
    • 软件体系结构——后端三层架构
    • Nmap 端口扫描
    • 关于青春的沉浸式回忆录-《学生时代》评测
    • 深入理解虚拟 DOM(VDOM):原理、优势与应用