Python 学习-深入理解 Python 进程、线程与协程(下)

第三章 协程:用户态的轻量级并发单元

3.1 协程的本质与核心特性

协程(Coroutine)是用户态的轻量级执行单元,由程序(而非操作系统内核)控制调度,本质是通过 "协作式调度" 实现的状态机。与进程、线程的内核态调度不同,协程的切换完全在用户态完成,无需内核参与,因此具有极高的切换效率和极低的资源占用。

协程的核心特性可概括为 "三轻一协":

  • 轻量级资源:一个协程仅占用数 KB 内存(主要用于保存栈帧和状态),一台机器可同时运行数十万甚至数百万个协程(进程 / 线程仅能运行数千个);
  • 轻量级切换:协程切换仅需保存 / 恢复函数的上下文(如寄存器、程序计数器),无需内核态与用户态的切换(进程切换需千倍以上开销);
  • 轻量级调度:协程由用户程序通过事件循环(Event Loop)调度,无需操作系统内核干预,调度策略可自定义;
  • 协作式调度:协程必须主动放弃 CPU(如调用await挂起),其他协程才能获得执行机会,不存在抢占式调度(避免线程安全问题)。

从实现层面看,Python 协程经历了三个发展阶段:

  • 生成器协程(Python 2.5+):基于生成器(yield/yield from)实现,本质是 "生成器的扩展",需手动管理状态与调度逻辑,缺乏专门的协程语法标识,边界模糊;

  • 原生协程(Python 3.5+):引入 async def/await 语法糖,明确区分协程与生成器,协程成为独立的语法单元,调度逻辑更清晰,无需依赖生成器的迭代特性;

  • 异步生态成熟(Python 3.7+):asyncio.run () 简化事件循环启动,无需手动创建、管理循环生命周期,aiohttp/aiofiles/asyncpg 等异步库全面完善,协程成为 Python 处理高并发 I/O 任务的主流方案,生态工具链与最佳实践逐步标准化。

3.2 四个核心概念

要真正掌握 Python 协程的使用,必须先理解其运行体系中的四个核心概念:协程函数、协程对象、事件循环与可等待对象。这四个概念相互关联,共同构成了协程的执行基础。​

3.2.1 协程函数与协程对象​
  • 协程函数:以async def关键字定义的函数,是协程的 "模板"。与普通函数的本质区别在于:调用协程函数不会立即执行函数体,而是返回一个协程对象;​
  • 协程对象:协程函数的实例,内部存储了协程的执行状态(如当前执行到的代码行、局部变量值、栈帧信息)。协程对象本身无法直接执行,必须通过事件循环驱动。​

    import asyncio

    定义协程函数(async def 是关键标识)

    async def calculate_sum(a, b):
    print(f"协程开始计算 {a} + {b}")
    await asyncio.sleep(1) # 模拟耗时操作,主动挂起协程
    result = a + b
    print(f"协程计算完成,结果:{result}")
    return result

    调用协程函数:返回协程对象,函数体未执行

    coro = calculate_sum(3, 5)
    print("协程对象类型:", type(coro)) # <class 'coroutine'>
    print("协程对象状态(是否运行中):", coro.cr_running) # False

    错误用法:直接打印协程对象不会执行,且会触发警告

    print(coro) # 输出 object calculate_sum at 0x0000023F7A8D1C40>

    正确用法:通过事件循环驱动协程执行

    asyncio.run(coro) # 执行后会输出计算过程,返回结果8

关键注意点:​

  • 协程函数内部不能使用yield关键字(否则会被识别为生成器函数),必须使用await挂起操作;​
  • 若协程对象未被事件循环驱动执行,Python 解释器会在程序退出时抛出RuntimeWarning,提示 "协程未被等待"。​
3.2.2 事件循环:协程的 "调度中枢"​

事件循环(Event Loop)是协程的核心调度机制,相当于协程的 "操作系统",负责管理所有协程的生命周期(创建、执行、挂起、恢复、销毁)。其工作原理可概括为 "事件驱动的循环队列模型",具体流程如下:​

  1. 初始化阶段:创建事件循环实例,注册待执行的协程任务(通常通过asyncio.create_task()将协程对象包装为任务);​

  2. 事件检测阶段:循环检测是否有 "就绪状态" 的任务(如 I/O 操作完成、定时器到期的任务);​

  3. 任务执行阶段:取出就绪任务,执行其协程函数体,直到遇到await关键字(协程主动挂起);​

  4. 状态切换阶段:将挂起的任务加入 "等待队列",继续执行下一个就绪任务;​

  5. 循环终止阶段:当所有任务执行完成或被取消,事件循环退出,释放资源。​

Python asyncio模块提供了事件循环的完整实现,Python 3.7+ 引入的asyncio.run()函数大幅简化了事件循环的使用 ------ 无需手动创建、启动和关闭事件循环,该函数会自动完成所有操作。​

复制代码
import asyncio
import time

async def task1():
    print("任务1启动,耗时2秒")
    await asyncio.sleep(2)  # 挂起2秒,期间事件循环可执行其他任务
    print("任务1完成")
    return "任务1结果"

async def task2():
    print("任务2启动,耗时1秒")
    await asyncio.sleep(1)  # 挂起1秒
    print("任务2完成")
    return "任务2结果"

async def main():
    # 1. 方式一:使用asyncio.create_task()创建任务(立即加入事件循环)
    start_time = time.time()
    task_a = asyncio.create_task(task1())
    task_b = asyncio.create_task(task2())
    
    # 等待任务完成并获取结果(可单独等待,也可批量等待)
    result_a = await task_a
    result_b = await task_b
    print(f"任务1结果:{result_a},任务2结果:{result_b}")
    print(f"方式一总耗时:{time.time() - start_time:.2f}秒")  # 约2秒(并发执行)

    # 2. 方式二:使用asyncio.gather()批量管理任务(自动包装为任务)
    start_time = time.time()
    results = await asyncio.gather(task1(), task2())  # 接收协程对象,返回结果列表
    print(f"批量任务结果:{results}")  # ["任务1结果", "任务2结果"]
    print(f"方式二总耗时:{time.time() - start_time:.2f}秒")  # 约2秒

# 启动事件循环(asyncio.run() 自动管理事件循环生命周期)
asyncio.run(main())

两种任务管理方式的差异:​

|---------|-----------------------------|---------------------------------------------|
| 特性​ | asyncio.create_task()​ | asyncio.gather()​ |
| 输入参数​ | 单个协程对象​ | 多个协程对象(可变参数)​ |
| 返回值​ | Task对象(可单独管理状态)​ | 结果列表(按输入协程顺序排列)​ |
| 任务状态管理​ | 支持单独取消(task.cancel())、查询状态​ | 不支持单独管理,需整体取消(return_exceptions=True可捕获异常)​ |
| 适用场景​ | 动态添加任务、需单独控制的任务​ | 批量固定任务、需统一等待结果的场景​ |

3.2.3 可等待对象:await的 "合法操作数"​

await关键字是协程挂起的核心语法,但其操作数必须是可等待对象(Awaitable)------ 即实现了__await__()方法的对象。Python 中常见的可等待对象分为三类,具体关系如下:​

  1. 协程对象(Coroutine):最基础的可等待对象,由async def函数返回,直接支持await;​

  2. 任务(Task):asyncio.create_task()的返回值,是协程对象的 "包装器",继承自Future,支持状态管理(如done()判断是否完成、result()获取结果);​

  3. 未来对象(Future):低层级的异步结果容器,代表 "一个尚未完成的异步操作"(如 I/O 操作结果)。Task是Future的子类,协程的执行结果最终会存储在Future的结果属性中。​

    import asyncio

    async def use_future():
    # 1. 创建空的Future对象(初始状态为未完成)
    future = asyncio.Future()
    print("Future初始状态:", future.done()) # False

    复制代码
     # 2. 定义回调函数:当Future设置结果时触发
     def set_future_result():
         future.set_result("Future任务完成!")  # 设置结果,Future状态变为完成
         print("Future结果已设置")
    
     # 3. 用事件循环的call_soon()方法,立即执行回调函数
     loop = asyncio.get_running_loop()
     loop.call_soon(set_future_result)
    
     # 4. 等待Future完成,获取结果(Future是可等待对象,支持await)
     result = await future
     print("获取Future结果:", result)  # 输出 "Future任务完成!"
     print("Future最终状态:", future.done())  # True

    asyncio.run(use_future())

核心逻辑:Future相当于一个 "异步结果占位符",当异步操作(如 I/O)完成后,通过set_result()设置结果,等待该Future的协程会被自动唤醒并获取结果。​

3.3 Python 协程的实战场景与案例​

协程的核心优势是高效处理 I/O 密集型任务,因为在 I/O 等待期间(如网络请求、文件读写、数据库操作),协程会主动挂起并释放 CPU,让其他协程执行,从而最大化 CPU 利用率。以下通过三个典型场景,展示协程的实战用法。​

3.3.1 异步网络请求:用aiohttp高效爬取​

requests是 Python 常用的同步网络库,但在并发爬取时会因 I/O 等待阻塞线程;aiohttp是基于协程的异步 HTTP 客户端,配合asyncio可实现高并发网络请求,大幅提升爬取效率。​

复制代码
import asyncio
import aiohttp

# 异步爬取单个URL的页面长度
async def fetch_url(session, url):
    try:
        # async with 自动管理HTTP连接(类似同步的with,但支持await)
        async with session.get(url, timeout=5) as response:
            # 异步读取响应内容(避免I/O阻塞)
            content = await response.text()
            return {
                "url": url,
                "status": response.status,  # 响应状态码(如200、404)
                "content_length": len(content)  # 页面内容长度
            }
    except Exception as e:
        return {
            "url": url,
            "error": str(e)  # 捕获异常(如超时、连接失败)
        }

# 批量异步爬取多个URL
async def batch_fetch(urls):
    # 创建异步HTTP会话(复用连接池,提升效率)
    async with aiohttp.ClientSession() as session:
        # 创建任务列表:每个URL对应一个爬取任务
        tasks = [asyncio.create_task(fetch_url(session, url)) for url in urls]
        
        # 等待所有任务完成,获取结果(可通过asyncio.as_completed()实现"完成一个处理一个")
        results = await asyncio.gather(*tasks)
        return results

if __name__ == "__main__":
    # 待爬取的URL列表
    target_urls = [
        "https://www.baidu.com",
        "https://www.github.com",
        "https://www.python.org",
        "https://www.zhihu.com",
        "https://www.csdn.net"
    ]

    # 启动异步爬取
    start_time = asyncio.get_event_loop().time()
    results = asyncio.run(batch_fetch(target_urls))
    end_time = asyncio.get_event_loop().time()

    # 打印爬取结果
    print(f"爬取完成,总耗时:{end_time - start_time:.2f}秒")
    for result in results:
        if "error" in result:
            print(f"失败 {result['url']}: {result['error']}")
        else:
            print(f"成功 {result['url']}: 状态码{result['status']}, 内容长度{result['content_length']}")

效率对比:若用requests同步爬取 5 个 URL,总耗时约为 "每个 URL 的 I/O 时间之和"(约 5-10 秒);而用aiohttp协程爬取,总耗时约等于 "最长单个 URL 的 I/O 时间"(约 1-2 秒),效率提升 5-10 倍。​

3.3.2 异步文件读写:用aiofiles避免 I/O 阻塞​

open()函数是 Python 的同步文件操作接口,在读写大文件或大量文件时,I/O 等待会阻塞线程;aiofiles是基于协程的异步文件操作库,通过async with和await语法,实现文件读写的异步化。​

复制代码
import asyncio
import aiofiles

# 异步写入文件
async def async_write(filename, content):
    # async with 自动管理文件句柄(关闭文件)
    async with aiofiles.open(filename, 'w', encoding='utf-8') as f:
        await f.write(content)  # 异步写入,I/O等待时挂起
    print(f"已写入文件:{filename}")

# 异步读取文件
async def async_read(filename):
    async with aiofiles.open(filename, 'r', encoding='utf-8') as f:
        content = await f.read()  # 异步读取(支持readline()、readlines())
    print(f"读取文件 {filename},内容长度:{len(content)} 字节")
    return content

# 批量处理文件(先写后读)
async def batch_file_operation():
    # 1. 并发写入3个文件
    write_tasks = [
        async_write(f"test_{i}.txt", f"这是第{i}个测试文件,内容为:{i * 100}")
        for i in range(3)
    ]
    await asyncio.gather(*write_tasks)

    # 2. 并发读取3个文件
    read_tasks = [async_read(f"test_{i}.txt") for i in range(3)]
    contents = await asyncio.gather(*read_tasks)
    return contents

if __name__ == "__main__":
    start_time = asyncio.get_event_loop().time()
    contents = asyncio.run(batch_file_operation())
    print(f"总耗时:{asyncio.get_event_loop().time() - start_time:.2f}秒")
    # 可选:打印读取的内容
    # for i, content in enumerate(contents):
    #     print(f"test_{i}.txt 内容:{content}")
3.3.3 协程的高级特性:超时、取消与异常处理​

在实际开发中,协程可能遇到 "任务超时""需要主动取消""执行异常" 等场景,asyncio提供了对应的解决方案,确保协程的稳健运行。​

(1)超时控制:asyncio.wait_for()​

为协程设置超时时间,若协程在超时时间内未完成,会自动抛出asyncio.TimeoutError,避免无限等待。​

复制代码
import asyncio

async def slow_task():
    print("慢任务启动,预计耗时3秒")
    await asyncio.sleep(3)  # 模拟耗时3秒的任务
    print("慢任务完成")
    return "慢任务结果"

async def main():
    try:
        # 设置超时时间2秒,任务未完成则抛出TimeoutError
        result = await asyncio.wait_for(slow_task(), timeout=2)
        print("任务结果:", result)
    except asyncio.TimeoutError:
        print("任务超时!(2秒内未完成)")

asyncio.run(main())  # 输出:慢任务启动... 任务超时!
(2)任务取消:Task.cancel()​

通过Task对象的cancel()方法主动取消协程,被取消的协程会抛出asyncio.CancelledError,可在协程内部捕获该异常进行 "清理操作"(如释放资源)。​

复制代码
import asyncio

async def long_running_task():
    try:
        print("长时间任务启动,每1秒输出一次")
        while True:
            print("任务运行中...")
            await asyncio.sleep(1)  # 每1秒挂起一次
    except asyncio.CancelledError:
        # 捕获取消异常,执行清理逻辑
        print("任务被取消,执行清理操作(如关闭连接、保存数据)...")
        raise  # 可选:重新抛出异常,让调用方感知取消
    finally:
        print("任务最终清理(无论是否取消,都会执行)")

async def main():
    # 创建任务并启动
    task = asyncio.create_task(long_running_task())
    # 运行2秒后取消任务
    await asyncio.sleep(2)
    print("准备取消任务...")
    task.cancel()

    try:
        # 等待任务完成(必须await,否则会触发警告)
        await task
    except asyncio.CancelledError:
        print("主函数捕获到任务取消")

asyncio.run(main())
(3)异常处理:try-except与return_exceptions​

协程执行过程中若抛出异常,需通过try-except捕获;若使用asyncio.gather()批量管理任务,可设置return_exceptions=True,让异常作为结果返回(而非直接抛出),便于统一处理。​

复制代码
import asyncio

async def normal_task():
    await asyncio.sleep(1)
    return "正常任务结果"

async def error_task():
    await asyncio.sleep(0.5)
    raise ValueError("任务执行出错!")  # 模拟异常

async def main():
    # 方式一:单独捕获异常
    try:
        await error_task()
    except ValueError as e:
        print("单独捕获异常:", e)  # 输出 "任务执行出错!"

    # 方式二:批量任务捕获异常(return_exceptions=True)
    results = await asyncio.gather(
        normal_task(),
        error_task(),
        return_exceptions=True  # 异常会作为结果返回,不中断其他任务
    )
    print("批量任务结果:", results)
    # 输出:["正常任务结果", ValueError("任务执行出错!")]

    # 遍历结果,处理正常结果和异常
    for result in results:
        if isinstance(result, Exception):
            print(f"处理异常:{result}")
        else:
            print(f"处理正常结果:{result}")

asyncio.run(main())

3.4 Python 协程的局限性​

尽管协程在 I/O 密集型任务中表现优异,但也存在明显的局限性,需在实际开发中规避:​

  • 不适合 CPU 密集型任务​:协程是协作式调度,若一个协程执行 CPU 密集型任务(如大量数学计算、数据加密),会长期占用 CPU 且不主动挂起,导致其他协程无法执行(事件循环阻塞)。此时需结合多进程(如multiprocessing),让 CPU 密集型任务在独立进程中执行,协程负责调度。​

  • 依赖异步生态​:协程需配合异步库使用(如aiohttp、aiofiles、asyncpg),若使用同步库(如requests、open()、psycopg2),会阻塞事件循环,失去并发能力。目前 Python 异步生态已较完善,但仍有部分库仅支持同步操作。​

  • 调试难度较高​:协程的执行流程由事件循环调度,而非线性执行,传统的 "打印日志""断点调试" 方式难以追踪协程的切换过程。虽有asyncio.debug()等调试工具,但整体调试体验仍不如同步代码。​

  • 协作式调度的风险​:若协程忘记调用await挂起(如无限循环且无await),会导致事件循环 "卡死",所有协程无法执行。需在协程中确保有合理的await操作,或通过超时控制(asyncio.wait_for())避免此类问题。​

  • GIL 的间接影响​:协程运行在单个线程中,受 Python 全局解释器锁(GIL)限制,无法利用多核 CPU。若需充分利用多核,需通过 "多进程 + 协程" 的组合模式:每个进程运行一个事件循环,管理多个协程,进程间通过 IPC(如队列)通信。​

第四章 进程、线程与协程的对比总结与选型策略​

进程、线程、协程是 Python 并发编程的三大核心技术,三者在底层原理、资源占用、适用场景上存在本质差异。掌握三者的对比关系,是实现高效并发的关键。​

4.1 核心维度对比​

|---------|------------------------|----------------------|----------------------|
| 对比维度​ | 进程(Process)​ | 线程(Thread)​ | 协程(Coroutine)​ |
| 调度层级​ | 内核态(操作系统内核调度)​ | 内核态(操作系统内核调度)​ | 用户态(事件循环调度)​ |
| 资源占用​ | 高(独立地址空间,MB 级内存)​ | 中(共享地址空间,栈空间 1-8MB)​ | 极低(仅保存状态,KB 级内存)​ |
| 切换开销​ | 高(千倍级,需切换地址空间)​ | 中(十倍级,仅切换线程上下文)​ | 极低(微秒级,仅切换函数上下文)​ |
| 并发能力​ | 低(支持数千个)​ | 中(支持数万个)​ | 极高(支持数百万个)​ |
| 数据共享​ | 独立地址空间,需 IPC(队列 / 管道)​ | 共享地址空间,需锁保证线程安全​ | 共享线程内存,无抢占,无需锁​ |
| 线程安全​ | 安全(进程隔离)​ | 不安全(需手动加锁)​ | 安全(协作式调度,无竞态条件)​ |
| GIL 影响​ | 无(每个进程有独立 GIL)​ | 有(同一进程内线程竞争 GIL)​ | 有(运行在单个线程,受 GIL 限制)​ |
| 适用场景​ | CPU 密集型任务、进程隔离需求​ | I/O 密集型任务(中小型并发)​ | I/O 密集型任务(超大规模并发)​ |
| 实现复杂度​ | 高(IPC 机制复杂)​ | 中(需处理线程安全)​ | 中(需熟悉异步语法和生态)​ |

4.2 典型场景选型策略​

根据任务类型和并发需求,选择合适的技术方案,可最大化程序效率:​

(1)CPU 密集型任务(如数据计算、机器学习、加密解密)​
  • 首选方案:多进程(multiprocessing、concurrent.futures.ProcessPoolExecutor)

原因:多进程避开 GIL,可利用多核 CPU,每个进程独立执行 CPU 密集型任务,无资源竞争。​

  • 补充方案:多进程 + 协程

若任务中包含少量 I/O 操作(如读取输入数据、保存结果),可在每个进程中运行协程,让 CPU 密集型任务在进程中执行,协程负责 I/O 调度,进一步提升效率。​

(2)I/O 密集型任务(如网络请求、文件读写、数据库操作)​
  • 中小规模并发(千级以内):多线程(threading、concurrent.futures.ThreadPoolExecutor)​

原因:实现简单,无需修改现有同步代码(如requests),I/O 等待期间线程释放 GIL,可提升并发效率。​

  • 超大规模并发(万级以上):协程(asyncio+ 异步库)​

原因:资源占用极低,切换效率高,支持百万级并发,适合高并发服务器(如 API 网关、爬虫)。​

(3)混合任务(既有 CPU 密集型,也有 I/O 密集型)​
  • 推荐方案:多进程 + 协程

架构设计:​

  • 主进程负责任务分发和结果汇总;​

  • 多个子进程(数量等于 CPU 核心数)负责执行 CPU 密集型任务;​

  • 每个子进程内运行一个事件循环,管理协程,处理 I/O 密集型任务(如获取任务数据、返回结果);​

  • 进程间通过队列(multiprocessing.Queue)通信,传递任务和结果。​

4.3 实战组合案例:多进程 + 协程处理混合任务​

以下案例展示 "多进程 + 协程" 的组合模式,处理 "CPU 密集型计算 + 异步网络请求" 的混合任务:​

复制代码
import asyncio
import multiprocessing
import aiohttp

# CPU密集型任务:计算大数字的平方(在独立进程中执行)
def cpu_intensive_task(num):
    print(f"进程 {multiprocessing.current_process().pid} 计算 {num} 的平方")
    result = num ** 2  # 模拟CPU密集型计算
    return result

# 协程任务:异步获取数据,调用CPU密集型任务,返回结果
async def worker_coroutine(session, num, result_queue):
    try:
        # 1. 异步网络请求(I/O密集型)
        async with session.get(f"https://httpbin.org/get?num={num}", timeout=3) as response:
            data = await response.json()
            print(f"协程获取数据:{data['args']}")

        # 2. 调用CPU密集型任务(通过多进程执行)
        # 此处简化:直接调用函数,实际中可通过进程池执行
        cpu_result = cpu_intensive_task(num)

        # 3. 保存结果到队列(进程间通信)
        result_queue.put({
            "input_num": num,
            "cpu_result": cpu_result,
            "network_data": data['args']
        })
    except Exception as e:
        result_queue.put({"input_num": num, "error": str(e)})

# 子进程:运行事件循环,管理协程
def subprocess_worker(num_list, result_queue):
    async def main():
        async with aiohttp.ClientSession() as session:
            # 创建协程任务列表
            tasks = [worker_coroutine(session, num, result_queue) for num in num_list]
            await asyncio.gather(*tasks)

    # 子进程中启动事件循环
    asyncio.run(main())

# 主进程:分发任务,启动子进程,汇总结果
def main():
    # 1. 待处理的数字列表
    num_list = [100000, 200000, 300000, 400000, 500000]
    # 2. 进程间通信队列(用于传递结果)
    result_queue = multiprocessing.Queue()
    # 3. 拆分任务(按CPU核心数拆分,此处简化为2个进程)
    cpu_count = multiprocessing.cpu_count()
    task_chunks = [num_list[i::cpu_count] for i in range(cpu_count)]
    # 4. 启动子进程
    processes = []
    for chunk in task_chunks:
        if chunk:  # 避免空任务
            p = multiprocessing.Process(
                target=subprocess_worker,
                args=(chunk, result_queue)
            )
            p.start()
            processes.append(p)
    # 5. 等待子进程完成
    for p in processes:
        p.join()
    # 6. 汇总结果
    results = []
    while not result_queue.empty():
        results.append(result_queue.get())
    print("\n最终结果汇总:")
    for res in results:
        print(res)

if __name__ == "__main__":
    main()

总结​

Python 并发编程的核心是 "根据任务类型选择合适的技术":​

  • 若需利用多核 CPU 处理 CPU 密集型任务,优先选择多进程;​
  • 若需处理中小规模 I/O 密集型任务,优先选择多线程(实现简单);​
  • 若需处理超大规模 I/O 密集型任务,优先选择协程(效率最高);​
  • 若任务混合 CPU 和 I/O 操作,优先选择多进程 + 协程的组合模式,兼顾多核利用和高并发。​

掌握进程、线程、协程的底层原理和适用场景,不仅能提升程序的并发效率,还能帮助开发者构建更稳健、更易维护的并发系统。在实际开发中,需结合具体需求,灵活选择技术方案,避免 "过度设计" 或 "技术滥用"。

相关推荐
小猪写代码1 小时前
C语言系统函数-(新增)
c语言·开发语言
WXG10111 小时前
【matlab】matlab点云处理
开发语言·matlab
♛识尔如昼♛1 小时前
C 基础(3-2) - 数据和C
c语言·开发语言
可触的未来,发芽的智生1 小时前
微论-自成长系统引发的NLP新生
javascript·人工智能·python·程序人生·自然语言处理
活跃的煤矿打工人1 小时前
【星海随笔】标准学习
学习
liulilittle1 小时前
C++判断wchar_t空白字符
开发语言·c++
1***35772 小时前
SQL之CASE WHEN用法详解
数据库·python·sql
花阴偷移2 小时前
kotlin语法(上)
android·java·开发语言·kotlin
XuanRanDev2 小时前
【编程语言】Kotlin快速入门 - 泛型
开发语言·kotlin