Linux C/C++ 学习日记(30):协程(一):同步和异步、协程的简要介绍、用户态CPU调度的实现

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

一、同步

同步:阻塞时仍等待,等IO执行完之后再去干其他事情

核心:操作的发起和结果处理在同一个执行流中顺序进行

优点:逻辑简单,符合人脑的线性思维

缺点:当IO等待时间过长时,影响性能

cpp 复制代码
int main
{
    read(); 

    // 阻塞等待

    parse_data(); // 处理数据

    write(); // 存储数据

    other(); // 其他操作
    
}

二、异步

异步:阻塞时不等待,去干别的事情,等IO执行完之后再返回来去执行(非阻塞模型)

核心:"非阻塞的操作执行与结果通知机制"

优点:性能优越

缺点:传统方式的异步实现大都依赖回调、业务复杂时容易回调地狱,同时线程数量有限制(Linux中1线程占8MB),不适合高并发

Linux中提供异步io函数 (aio库):

aio_read()为例子:

  1. 非阻塞的操作执行:文件的读取由内核直接调度 I/O 操作,无需用户态线程(即不会阻塞)

  2. 结果通知机制:读取完毕后,通过信号(需循环判断是否执行完毕)或回调通知(无需判断是否执行完毕,设置回调函数,系统会单开线程执行该回调函数)。

cpp 复制代码
handle()
{
    parse_data(buf);
    write(buf);
}


int main()
{
    aio_read(fd, buf, handle); //不阻塞等待
  
    other(); // 其他操作
}

三、同步和异步的对比

维度 同步 异步
执行流 阻塞等待,单执行流 非阻塞,多执行流 / 事件驱动
结果处理 立即在当前执行流中获取 后续通过回调、事件等异步通知
适用场景 逻辑简单、操作耗时短的场景 高并发、耗时操作(I/O、网络)

四、协程

核心:同步的编程方式,异步的性能

本质:轻量级线程(用户态实现的CPU调度),形式表现为一个可以暂停和恢复的函数。

遇到阻塞(wait、sleep)时,让出CPU给调度器,调度器分配给非阻塞的协程(ready),常搭配epoll使用

1. 协程有四种状态:

需要CPU:

ready:协程处于非阻塞状态

不需要CPU:

wait:等待IO,IO完毕时,回复ready

sleep: 协程主动休眠(内部调用sleep),超过休眠时间,变回ready

正在使用CPU:

running: 协程执行中

2. 原语操作:

create:协程创建,进入ready

yield: 协程:ready -> wait or sleep 。 让出CPU给调度器

resume: 协程: ready -> running。 调度器将CPU分配给协程

exit:摧毁协程

3. 优势:

  • 避免回调地狱:用同步式的代码结构实现异步逻辑,可读性远高于传统异步(如回调、Promise)。
  • 轻量级调度:协程的创建和切换开销极低(比线程小几个数量级),支持大规模并发(如百万级协程)。
  • 资源高效:无需为每个任务创建线程,减少内存占用和上下文切换开销。

4. 与传统同步、异步的对比

特性 传统同步(线程) 传统异步(回调 / 事件) 协程
编码复杂度 低(线性逻辑) 高(回调嵌套 / 事件注册) 低(线性逻辑)
性能开销 高(线程切换) 低(无阻塞) 极低(用户态调度)
并发规模 有限(线程数受限) 高(无阻塞) 极高(百万级协程)

5. 优缺点

维度 详细说明
优点 1. 极致轻量,资源消耗低协程在用户态创建和切换,无需操作系统内核介入,内存占用极小(通常几 KB 栈空间),可创建数十万甚至数百万个协程(而线程通常只能创建数千个,受内核限制)。 2. 调度灵活,无内核切换开销 协程的切换由程序显式控制(如通过yieldawaitswapcontext),无需陷入内核态,切换成本仅为保存 / 恢复少量寄存器和栈指针(微秒级,线程切换通常是毫秒级),适合高频切换场景。 3. 避免多线程锁竞争协程通常运行在单线程内("单线程多协程" 模型),共享数据无需加锁(同一时间只有一个协程执行),减少了锁带来的复杂性和性能损耗。 4. 天然支持 "暂停 - 恢复" 模式协程可在执行中主动暂停(如等待 I/O 时),保存当前状态,后续再从暂停点恢复,适合处理需要 "等待 - 继续" 的逻辑(如网络请求、文件读写)。
缺点 1. 无法直接利用多核 CPU单线程内的协程只能在一个 CPU 核心上运行,若要利用多核,需配合多进程(每个进程运行多个协程),增加了设计复杂度。 2. 依赖语言 / 库支持,调试难度高 协程需要语言原生支持(如 Python 的async/await、Go 的goroutine)或第三方库(如 C++ 的libco),不同实现的语法和机制差异大;且执行流是非线性的(频繁暂停 / 恢复),调试时难以跟踪调用栈。 3. 需手动处理阻塞操作 若协程中存在阻塞操作(如同步 I/O)且未主动切换,会阻塞整个线程内的所有协程("一损俱损"),需配合异步 I/O(如epollkqueue)才能发挥优势。 4. 不适合 CPU 密集型任务协程在 CPU 密集型任务中无法并行计算(单线程限制),性能不如多线程(可利用多核)。

6. 应用场景

场景类型 具体说明 示例
I/O 密集型任务 适合包含大量等待时间(如网络请求、文件读写、数据库操作)的场景。协程在等待 I/O 时可切换到其他任务,避免 CPU 空闲,大幅提升并发效率。 - 网络爬虫:同时发起数千个 HTTP 请求,等待响应时切换到其他请求。- Web 服务器:处理数万并发连接,每个连接用协程管理,等待数据时不阻塞。
异步编程框架 作为异步编程的底层执行单元,简化异步逻辑的编写(无需回调嵌套 "回调地狱")。 - Python 的asyncio:用协程实现异步网络操作,代码线性编写,可读性高。- Node.js 的async/await(基于协程思想):替代回调函数,简化异步逻辑。
游戏开发 游戏中角色 AI、动画帧、事件触发等逻辑需要 "暂停 - 恢复"(如技能冷却、对话等待),协程可自然表达这种时序逻辑。 - 游戏角色释放技能:协程先执行施法动画(1 秒),暂停后等待冷却(5 秒),再恢复执行后续逻辑。
数据流 / 管道处理 多步骤的数据处理流程(如解析→过滤→转换→输出),每个步骤用协程实现,数据传递时切换执行,提高流水线效率。 - 日志处理系统:用协程分别处理日志读取、格式解析、关键词过滤,数据流式传递,步骤间无缝切换。
嵌入式 / 资源受限场景 嵌入式设备内存和 CPU 资源有限,无法创建大量线程,协程的轻量级特性适合实现多任务调度。 - 物联网设备:用协程同时处理传感器数据采集、网络上报、本地交互,低资源消耗下实现多任务。

总结

协程的核心价值是用极低的开销实现高并发,尤其适合 I/O 密集型、需要频繁切换且逻辑存在 "等待 - 恢复" 的场景。但它无法直接利用多核,需配合异步 I/O 和多进程才能发挥最大优势,且不适合 CPU 密集型任务。现代编程语言(如 Go、Python、C++20)已普遍支持协程,使其成为高并发场景的重要解决方案。

五、用户态实现上下文切换的方式(CPU的调度):

协程是一个函数,所谓的CPU调度,可以简单的理解为如何在一个函数执行到一半时跳到另一个函数,同时保存当前函数的执行状态。

实现方式

1. setjmp.h

名称 类型 / 性质 参数 核心作用 返回值 / 特殊说明
jmp_buf 结构体(特殊类型) 无(自身为保存载体) 保存程序的 "执行环境",包括程序计数器、栈指针、寄存器状态等,即当前执行位置和上下文。 无(仅作为setjmplongjmp的操作对象)
setjmp 函数 jmp_buf env 将当前执行环境保存到env中,作为后续longjmp的 "跳转目标点"。 首次调用返回0;被longjmp跳回时,返回longjmp指定的非 0 值(用于区分首次执行与跳转)。
longjmp 函数 jmp_buf envint val 跳转到setjmp保存的env环境,恢复当时的执行状态(回到setjmp调用位置)。 无返回值;val必须为非 0 值,否则行为未定义(val会作为setjmp的返回值)。

使用方式:

jmp_buf env; // 定义保存当前状态的变量

setjump(env) // 设定锚点

longjump(env,eventi_d) // 跳回到锚点,返回event_id (不能为0),锚点根据event_id 的值执行下一步操作

注意:env必须保证longjump时有效。简单点可以定义为全局变量,当然也可以定义为局部变量作为参数传给需要使用的函数(需确保主函数参数未被销毁)。

基本格式(伪代码)

cpp 复制代码
jmp_buf env;


void fun()
{
    ....
    longjmp(env,eventid);
}



int main()
{
    int ret = set_jump(env) // 初始返回0
    if (ret == 0)
    {
        fun();
    }
    else if (ret == eventid)
    {
        ....
    }

}

1. 例子:

1. env作为参数传入
cpp 复制代码
#include <stdio.h>
#include <setjmp.h>

void fun(jmp_buf env)
{
    printf("come to fun\n");
    longjmp(env, 1);
}

int main()
{
    jmp_buf env;
    int ret = setjmp(env);
    if (ret == 0)
    {
        fun(env);
    }
    else
    {
        printf("come back main\n");
    }
}

易错:如果fun中也设置了锚点env1,那么env1是不会对env进行保存的,也就是说下一次main跳转到env1的话,fun是使用不了env的!!!!

错误示例:
cpp 复制代码
#include <stdio.h>
#include <setjmp.h>

jmp_buf env1;
void fun(jmp_buf env)
{
    int ret = setjmp(env1);
    if (ret == 0)
    {
        printf("come to fun firstly\n");
        longjmp(env, 1);
    }
    else
    {
        printf("come to fun secondly\n");
        longjmp(env, 1);
    }
}

int main()
{
    jmp_buf env;
    int ret = setjmp(env);
    if (ret == 0)
    {
        fun(env);
    }
    else if (ret == 1)
    {
        printf("come back main firstly\n");
        longjmp(env1, 1);
    }

    else
    {
        printf("come back to main secondly\n");
    }
}

fun返回不去main,因为env没有被保存。所以说,稳妥一点,把env变量都定义在全局变量当中。

2. 多个env(锚点)、通过main来分配CPU
cpp 复制代码
#include <stdio.h>
#include <setjmp.h>

jmp_buf env0; // 锚定 main 中的点
jmp_buf env1; // 锚定 func1 中的点
jmp_buf env2; // 锚定 func2 中的点

enum SIG
{
    FUNC1_ONE = 100,
    FUNC1_TWO,
    FUNC2_ONE,
    FUNC2_TWO,
};

void func2()
{
    int ret = setjmp(env2);
    if (ret == 0)
    {
        printf("好");
        longjmp(env0, FUNC2_ONE);
    }
    else
    {
        printf("界");
        longjmp(env0, FUNC2_TWO);
    }
}

void func1()
{

    int ret = setjmp(env1);
    if (ret == 0)
    {
        printf("你");
        longjmp(env0, FUNC1_ONE);
    }

    else
    {
        printf("世");
        longjmp(env0, FUNC1_TWO); 
    }
}

int main()
{
    int ret = setjmp(env0);

    switch (ret)
    {
    case 0:
    {
        func1();
        break;
    }

    case FUNC1_ONE:
    {
        func2();
        break;
    }

    case FUNC1_TWO:
     {
        longjmp(env2, 2);
        break;
    }

    case FUNC2_ONE:
     {
        longjmp(env1, 2);
        break;
    }

    case FUNC2_TWO:
     {
        printf("!\n");
        break;
    }

    default:
        break;
    }

    return 0;
}

运行结果如上,通过eventid实现了CPU的定向分配

2. 优缺点、应用场景:

维度 内容
优点 - 提供非局部跳转能力,可跨多层函数直接跳转,简化深层错误处理流程 - 实现轻量级控制流切换 (如协程、状态机),无需操作系统级线程开销 - 错误处理时避免多层函数返回的繁琐,直接跳回顶层处理逻辑
缺点 - 严重破坏程序控制流和栈结构,代码逻辑难以理解和维护 - 易引发资源泄漏(如局部变量未销毁、文件句柄未关闭等) - 存在未定义行为风险(如跳回已返回函数的环境,导致栈访问越界) - 调试困难,控制流跳跃使得程序执行轨迹难以跟踪
应用场景 - 底层错误处理框架:C 语言早期用于跨函数错误恢复(如文件操作、网络通信的深层错误直接跳回顶层) - 协程 / 状态机实现:早期无 ucontext 时,用于保存 / 恢复执行上下文,实现简单协程或有限状态机 - 遗留系统维护:一些老旧 C 代码中仍依赖其处理复杂控制流,如嵌入式系统、特定领域的底层工具

2. ucontext

ucontext.h 是 C 语言中用于用户态上下文管理 的头文件,提供了一组函数和结构体,用于保存、恢复和切换程序的执行上下文(包括寄存器状态、栈指针、程序计数器等),是实现用户级线程(协程) 或复杂状态机的底层工具。

1. 关键结构体:ucontext_t

用于存储程序的 "执行上下文",包含以下核心信息:

  • 程序计数器(下一条要执行的指令地址)
  • 栈指针(当前栈的位置)
  • 通用寄存器状态
  • 信号掩码(阻塞的信号集合)
  • 关联的栈内存(uc_stack,用于上下文切换时的栈空间,需要用户配置)
  • 后继上下文(uc_link,当前上下文结束后自动切换到的上下文,需要用户配置)

2. 核心函数

函数原型 作用
int getcontext(ucontext_t *ucp); 保存当前执行上下文到 ucp 中(成功返回 0,失败返回 -1)。
void setcontext(const ucontext_t *ucp); 恢复 ucp 中保存的上下文,程序从该上下文的状态继续执行(不会返回,除非上下文切换失败)。
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); 修改已初始化的 ucp 上下文:指定新的入口函数 func、参数,以及栈空间(需先通过 getcontext 初始化并设置 uc_stack)。
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); 保存当前上下文到 oucp,同时切换到 ucp 上下文执行(成功返回 0,失败返回 -1)。 作用等价于: -先调用 getcontext(oucp)(保存当前上下文到 oucp); -再调用 setcontext(ucp)(恢复 ucp 上下文并执行)。

流程:

ucontext_t main_ctx, coro_ctx;

getcontext(&main_ctx);

getcontext(&coro_ctx);

coro_ctx.uc_stack.ss_sp = coro_stack; // 设置协程栈

coro_ctx.uc_stack.ss_size = sizeof(coro_stack);

coro_ctx.uc_link = &main_ctx; // 协程结束后自动切回 main_ctx

makecontext(&coro_ctx, coro_func, 0); // 绑定协程入口函数

swapcontext(&main_ctx, &coro_ctx); // 切换到 coro_ctx

注意:必须先调用 getcontext 初始化 ucontext_t,之后才能配置栈、后继上下文,最后通过 makecontext 设置入口函数,这是 ucontext 机制的强制要求:

  • getcontext 提供基础上下文:它会填充 ucontext_t 的所有必要字段(如程序计数器、栈指针、寄存器快照等),这些是 makecontext 能够正确修改上下文的前提。

  • makecontext 依赖已有上下文结构:它需要基于 getcontext 保存的 "原始状态",替换其中的程序计数器(指向新的入口函数 func2)和栈信息(确保切换后使用新栈),否则无法正确创建可执行的上下文。

3. 协程中栈的作用

作用分类 具体说明 示例
存储局部变量和临时数据 函数执行时的局部变量(如int a=10char buf[100])及运算临时数据,均存储在栈上,避免与其他上下文数据冲突。 协程函数coro_funcprintf调用产生的临时数据,存储在协程专属栈coro_stack中,不干扰主程序栈。
保存函数调用链信息 上下文执行中嵌套调用函数时,栈会记录每次调用的返回地址、参数列表等,形成 "调用链",确保函数执行后能正确返回上层。 coro_func调用sub_funcsub_func再调用sub_sub_func,三次调用的返回地址依次压入协程栈,保证最终能回到coro_func继续执行。
支持上下文独立执行 不同上下文(如主程序、协程)作为独立执行流,需各自的栈存储自身函数状态,避免共用栈导致数据覆盖。 主程序栈存储main及相关函数状态,协程栈coro_stack存储coro_func及相关函数状态,二者互不干扰。
确保切换后正确恢复状态 上下文切换时,通过恢复栈指针(指向该上下文上次执行的栈位置),可读取之前的局部变量和调用链信息,继续未完成的逻辑。 协程切换回主程序后,主程序从自身栈中读取临时数据,继续执行printf("主程序:从协程返回\n")

总结:栈是上下文的 "专属内存空间",通过存储临时状态(局部变量、调用链等),保障多个独立执行流(如主程序与协程)的切换互不干扰且能正确恢复,是上下文切换的核心保障。

栈大小选哟"按需分配":

  • 先根据场景估算(参考经验值),再通过测试验证(逐步调整至稳定运行)。
  • 简单场景:16KB~64KB;中等场景:128KB~512KB;复杂场景:1MB~8MB。
  • 优先保证不溢出,再优化内存占用。

4. main_ctx需要分配栈吗?

当然需要,只不过是由操作系统自动分配的

上下文 栈来源 是否需要手动分配 / 设置
coro_ctx 用户手动定义的栈(如 coro_stack 是(需设置 uc_stack.ss_spss_size
main_ctx 操作系统自动分配的进程主栈 否(getcontext 会自动记录主栈信息)

5. 简单示例:

cpp 复制代码
#include <stdio.h>
#include <ucontext.h>
#include <stdlib.h>

ucontext_t main_ctx, coro_ctx;
char coro_stack[1024 * 16]; // 协程栈

// 协程函数
void coro_func() {
    printf("协程:开始执行\n");
    printf("协程:切换回主程序\n");
    swapcontext(&coro_ctx, &main_ctx); // 切换到 main_ctx
    printf("协程:再次执行\n");
}

int main() {
    // 初始化主程序上下文
    getcontext(&main_ctx);

    // 初始化协程上下文
    getcontext(&coro_ctx);
    coro_ctx.uc_stack.ss_sp = coro_stack; // 设置协程栈
    coro_ctx.uc_stack.ss_size = sizeof(coro_stack);
    coro_ctx.uc_link = &main_ctx; // 协程结束后自动切回 main_ctx
    makecontext(&coro_ctx, coro_func, 0); // 绑定协程入口函数

    printf("主程序:切换到协程\n");
    swapcontext(&main_ctx, &coro_ctx); // 切换到 coro_ctx
    printf("主程序:从协程返回\n");
    swapcontext(&main_ctx, &coro_ctx); // 再次切换到协程
    printf("主程序:结束\n");

    return 0;
}

6. 优缺点、应用场景:

维度 详细说明
优点 1. 用户态轻量切换:上下文切换在用户态完成,无需陷入内核,开销远低于操作系统线程切换(无内核态 / 用户态切换成本),适合高频切换场景。 2. 完整上下文管理 :不仅保存寄存器和程序计数器,还显式管理栈空间(uc_stack),支持独立执行流(如协程)的完整生命周期(创建、切换、销毁),比 setjmp/longjmp 更灵活。 3. 控制流灵活:可实现复杂的非局部跳转(如跨多层函数切换),支持 "暂停 - 恢复" 模式,适合状态机、协程等需要保存中间状态的场景。
缺点 1. 兼容性差:在 POSIX.1-2008 中被标记为 "过时"(obsolescent),部分现代系统(如 macOS)不支持,依赖它的代码可移植性低。 2. 栈管理复杂 :需手动分配和管理栈空间(uc_stack),需预估栈大小(过小导致溢出,过大浪费内存),且栈溢出无安全保护(直接引发段错误)。 3. 调试困难 :上下文切换会打乱线性执行流,调试时难以跟踪调用栈和执行轨迹,增加问题定位难度。4. API 底层且繁琐 :需按固定流程调用 getcontext→配置栈→makecontextswapcontext,使用门槛高,易因步骤错误导致未定义行为(如未初始化上下文直接切换)。
应用场景 1. 用户级协程 / 轻量级线程 :作为早期协程库的底层实现(如 Python 早期 greenlet),通过 ucontext 实现多协程间的快速切换,适合 I/O 密集型任务(如网络爬虫、服务器)。 2. 复杂状态机 :在解析器(如语法解析、协议解析)或事件驱动程序中,用 ucontext 保存不同状态的执行上下文,实现状态间的高效切换。 3. 嵌入式系统轻量调度 :资源受限的嵌入式环境中,用 ucontext 实现用户态任务调度(无需操作系统内核支持),降低内存和 CPU 开销。 4. 遗留系统维护 :一些老旧 C 代码(如特定领域工具、早期服务器框架)仍依赖 ucontext 实现控制流,需基于它进行维护或迁移。

总结

ucontext 是用户态上下文管理的经典工具,核心价值在于轻量级切换和完整上下文控制 ,但受限于兼容性和使用复杂度,现代开发中已逐渐被语言原生协程(如 C++20 coroutine)或更高层库替代。其主要适用场景集中在对性能敏感的轻量级并发、复杂状态机,或依赖旧接口的遗留系统。

六、上下文切换是什么

上下文切换 指的是暂停当前程序的执行状态,保存其 "上下文"(执行时的全部状态信息),然后恢复另一个程序的上下文,使其继续执行的过程。简单来说,就是在多个独立的执行流之间 "切换工作",让它们轮流使用 CPU,从而实现并发或多任务的效果。

Tips:"上下文" 是程序执行时的完整状态快照,包含:

  • 寄存器值(如通用寄存器、程序计数器 PC,记录下一条要执行的指令地址);
  • 栈指针(记录当前栈的位置,用于存储局部变量、函数调用信息);
  • 内存中的临时数据(如局部变量、函数参数);
  • 程序的执行位置(当前执行到哪一行代码)。

1. 为什么需要上下文切换?

其核心目的是实现 "并发" 或 "多任务",让多个执行流(如进程、线程、协程)看起来 "同时" 执行,提升 CPU 利用率:

  • 操作系统层面:比如同时运行浏览器和音乐播放器,操作系统会在它们之间切换,让 CPU 轮流为两个程序服务。
  • 用户态层面:比如在一个程序中实现多个协程(轻量级线程),通过上下文切换让协程轮流执行,模拟并发效果。

2. 上下文切换的过程(以 "协程切换" 为例)

以用户态协程为例,切换步骤如下:

  1. 保存当前上下文 :当需要切换时,先把当前协程的寄存器值、栈指针、程序计数器等状态保存到内存中 (比如ucontext_t结构体)。
  2. 恢复目标上下文 :从内存中读取另一个协程的上下文,恢复其寄存器、栈指针、程序计数器,让 CPU 从该协程的 "暂停点" 继续执行。

3. 不同层面的上下文切换区别

场景 切换主体 切换位置 开销大小 典型例子
操作系统级 进程 / 线程 内核态 大(需陷入内核) 进程切换、线程调度
用户级 协程、用户线程 用户态 小(用户态完成) 协程切换(如 ucontext)

4. 通俗类比

可以把 "上下文切换" 想象成:

  • 你正在写论文(程序 A),突然要去接电话(程序 B)。
  • 你先保存论文的 "当前状态"(比如记下笔停在第几页、当前思路),然后去接电话。
  • 接完电话后,恢复论文的状态(回到之前的页数、继续之前的思路),继续写论文。

5. 总结

上下文切换是实现 "多任务并发" 的核心机制,通过保存和恢复程序的执行状态,让多个执行流轮流使用 CPU。它在操作系统(进程 / 线程调度)、用户态编程(协程、状态机)中广泛应用,是现代计算机 "并发能力" 的基础。

相关推荐
hello kitty w7 小时前
Python学习(11) ----- Python的泛型
windows·python·学习
讽刺人生Yan7 小时前
RFSOC学习记录(五)带通采样定理
学习·fpga·rfsoc
报错小能手8 小时前
linux学习笔记(49)Redis详解(1)
linux·笔记·学习
QT 小鲜肉8 小时前
【个人成长笔记】在本地Windows系统中如何正确使用adb pull命令,把Linux系统中的文件或文件夹复制到本地中(亲测有效)
linux·windows·笔记·学习·adb
_李小白10 小时前
【OPENGL ES 3.0 学习笔记】第九天:缓存、顶点和顶点数组
笔记·学习·elasticsearch
洛白白11 小时前
Word文档中打勾和打叉的三种方法
经验分享·学习·word·生活·学习方法
楼田莉子13 小时前
C++学习:C++11关于类型的处理
开发语言·c++·后端·学习
酷讯网络_24087016013 小时前
PHP双轨直销企业会员管理系统/购物直推系统/支持人脉网络分销系统源码
学习·开源
xwz小王子13 小时前
面向机器人学习的低成本、高效且拟人化手部的设计与制作
人工智能·学习·机器人