第6章 异步原理

第6章 异步原理

6.1 生成代码的结构

异步模式的实现原理是基于 状态机 的,它负责追踪 async 方法当前的执行进度。从逻辑上讲,可以分为以下 4 种状态:

  • 未启动
  • 正在执行
  • 暂停
  • 完成(成功或 faulted)

Eureka

这里的“暂停”,指程序运行至 await 处,任务未完成时,当前方法在此处挂起(暂停)。

async 方法中的每个 await 表达式是单独的状态,每次返回后都会触发后续代码的执行。只有当状态机需要进入 暂停 时,才需要记录状态(记录状态旨在从当前执行位置恢复执行)。下图演示了不同状态之间的转换关系:

image

下面两段代码演示了原代码和编译器转化后的代码(有删改):

static async Task PrintAndWait(TimeSpan delay)
{Console.WriteLine("Before first delay");await Task.Delay(delay);Console.WriteLine("Between delays");await Task.Delay(delay);Console.WriteLine("After second delay");
}
// 桩方法
[AsyncStateMachine(typeof(PrintAndWaitStateMachine))]
[DebuggerStepThrough]
private static unsafe Task PrintAndWait(TimeSpan delay)
{var machine = new PrintAndWaitStateMachine{                                               //delay = delay,                              // 初始化状态机,builder = AsyncTaskMethodBuilder.Create(),  // 包括方法参数state = -1                                  //};machine.builder.Start(ref machine);     // 运行状态机,直到需要等待为止return machine.builder.Task;    // 返回代表异步操作的 task
}// 状态机的私有结构体
[CompilerGenerated]
private struct PrintAndWaitStateMachine : IAsyncStateMachine
{public int state;                       // 状态机的状态public AsyncTaskMethodBuilder builder;  // 异步基础架构类型所关联的 builderprivate TaskAwaiter awaiter;            // 恢复执行时用于获取结果的 awaiterpublic TimeSpan delay;                  // 原始方法参数void IAsyncStateMachine.MoveNext(){// 状态机主要的工作代码,此处已省略}[DebuggerHidden]void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine){// 连接 builder 和 装箱后的状态机this.builder.SetStateMachine(stateMachine);}
}

6.1.1 桩方法:准备和开始第一步

延续前文内容,以如下代码为例,编译器创建的 PrintAndWait()​ 便是桩方法:

// 桩方法
[AsyncStateMachine(typeof(PrintAndWaitStateMachine))]
[DebuggerStepThrough]
private static unsafe Task PrintAndWait(TimeSpan delay)
{var machine = new PrintAndWaitStateMachine{                                               //delay = delay,                              // 初始化状态机,builder = AsyncTaskMethodBuilder.Create(),  // 包括方法参数state = -1                                  //};machine.builder.Start(ref machine);     // 运行状态机,直到需要等待为止return machine.builder.Task;    // 返回代表异步操作的 task
}

状态机 在桩方法中创建,主要有以下 3 点信息:

  • 形参:每个形参在状态机中都是独立的 字段
  • builder :该对象随着 async 方法 返回类型 的不同而不同。

该对象始终是 类型,桩方法通过它的 Start() ​ 方法启动状态机,并将状态机自身以 引用 方式传入(状态机也是 类型,引用传递用于避免值拷贝)

因值类型的特点,如下重构方式不可行:

var builder = machine.builder;
builder.Start(ref machine);
return builder.Task;
  • 初始状态:永远是 -1

6.1.2 状态机的结构

延续前文内容,以如下代码为例,编译器创建的 PrintAndWaitStateMachine​ 结构体便是状态机:

// 状态机的私有结构体
[CompilerGenerated]
private struct PrintAndWaitStateMachine : IAsyncStateMachine
{public int state;                       // 状态机的状态public AsyncTaskMethodBuilder builder;  // 异步基础架构类型所关联的 builderprivate TaskAwaiter awaiter;            // 恢复执行时用于获取结果的 awaiterpublic TimeSpan delay;                  // 原始方法参数void IAsyncStateMachine.MoveNext(){// 状态机主要的工作代码,此处已省略}[DebuggerHidden]void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine){// 连接 builder 和 装箱后的状态机this.builder.SetStateMachine(stateMachine);}
}

该类型有如下特点:

  • 它实现了 IAsyncStateMachine​ 接口,该接口用于异步基础架构。
  • 字段由状态机在 步进MoveNext() ​ 方法调用时)时使用。
  • MoveNext()​ 方法在状态机 启动 后或 暂停恢复 后被调用。
  • SetStateMachine()​ 方法的实现总是保持不变(在 release 模式下)。

其中的字段大致可分为以下 5 类

  • 当前状态(例如未启动、暂停等待某个 await 表达式等);

    有以下几种可能值:

    • -1:尚未启动或正在执行
    • -2:执行完成(成功或 faulted)
    • 其他值:正在某个 await 表达式处暂停
  • 方法 builder,用于和异步基础架构交互,并且提供返回的 Task;

    该对象类型可以是:

    • AsyncVoidMethodBuilder
    • AsyncTaskMethodBuilder
    • AsyncTaskMethodBuilder<T>
    • 自定义 task 类型的 builder
  • awaiter;

    该字段的数量取决于当前异步方法等待几类 Task 类型。以如下代码为例,编译器将创建 TaskAwaiter​、TaskAwaiter<int>​、TaskAwaiter<string>​ 三个 awaiter,分别用于 获取它们的结果

    static async Task PrintAndWait(TimeSpan delay)
    {await Task.Delay(delay);Console.WriteLine("delayed");string value = await GetString();Console.WriteLine(value);int num = await GetNumber();Console.WriteLine(num);
    }static async Task<int> GetNumber()
    {await Task.Delay(TimeSpan.FromSeconds(1));return 1;
    }static async Task<string> GetString()
    {await Task.Delay(TimeSpan.FromSeconds(1));return "1";
    }
    
  • 形参和局部变量;

  • 临时栈变量。

    当 await 表达式用作其他表达式的一部分,会用到临时栈变量。如下代码便涉及该情况:

    public async Task TemporaryStackDemoAsync()
    {Task<int> task = Task.FromResult(10);DateTime now = DateTime.UtcNow;int result = now.Second + now.Hours * await task;
    }
    

6.1.3 MoveNext()​ 方法

MoveNext()​ 方法用于适时恢复、暂停状态机。它的执行逻辑如下:

  1. 每次 MoveNext()​ 方法被调用,状态机都 向前执行一步
  2. 每次执行到 await 表达式,如果 await 的值已经完成, 继续执行 ,否则状态机 暂停

MoveNext()​ 会在如下几种情况返回:

  • 需要暂停等待一个未完成的值
  • 执行流程到达了 方法末尾 或者遇到 return 语句
  • 在 async 方法中有异常抛出并且 异常没有被捕获

下图是一个简化后的 MoveNext()​ 方法流程图(不含异常处理):

image

6.1.4 SetStateMachine()​ 方法以及状态机的装箱事宜

在状态机装箱后,该方法用于把状态机的 引用 传递给 builder,以保证后续 MoveNext()​ 操作在同一个状态机上执行。它的实现代码非常简单:

void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{this.builder.SetStateMachine(stateMachine);
}

Eureka

装箱是为了保证后续在同一实例上操作,装箱也让我们可以获得该实例的引用。

Tips

该方法的调用在底层进行,其调用代码大致如下:

void BoxAndRemember<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IStateMachine
{IStateMachine boxed = stateMachine;boxed.SetStateMachine(boxed);
}

6.2 一个简单的 MoveNext()​ 实现

6.2.1 一个完整的具体示例

以如下代码为例,它会生成形如第二段的 MoveNext()​ 方法代码:

static async Task PrintAndWait(TimeSpan delay)
{Console.WriteLine("Before first delay");await Task.Delay(delay);Console.WriteLine("Between delays");await Task.Delay(delay);Console.WriteLine("After second delay");
}
void IAsyncStateMachine.MoveNext()
{int num = this.state;try{TaskAwaiter awaiter1;switch (num){default:goto MethodStart;case 0:goto FirstAwaitContinuation;case 1:goto SecondAwaitContinuation;}MethodStart:Console.WriteLine("Before first delay");awaiter1 = Task.Delay(this.delay).GetAwaiter();if (awaiter1.IsCompleted){goto GetFirstAwaitResult;}this.state = num = 0;this.awaiter = awaiter1;this.builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this);return;FirstAwaitContinuation:awaiter1 = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;GetFirstAwaitResult:awaiter1.GetResult();Console.WriteLine("Between delays");TaskAwaiter awaiter2 = Task.Delay(this.delay).GetAwaiter();if (awaiter2.IsCompleted){goto GetSecondAwaitResult;}this.state = num = 1;this.awaiter = awaiter2;this.builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);return;SecondAwaitContinuation:awaiter2 = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;GetSecondAwaitResult:awaiter2.GetResult();Console.WriteLine("After second delay");}catch (Exception exception){this.state = -2;this.builder.SetException(exception);return;}this.state = -2;this.builder.SetResult();
}

6.2.2 MoveNext()​ 方法的通用结构

Info

以下内容会涉及如下术语:

  • 快速路径:await 时已经完成,状态机将继续执行
  • 慢速路径:await 时尚未完成,状态机会安排一个续延并暂停

MoveNext()​ 方法主要工作逻辑如下:

  • 从正确的位置开始执行;

    无论是在原异步代码的起始位置或者中间位置。

  • 当需要暂停时,保存状态;

    包括局部变量和代码中的位置。

  • 当需要暂停时安排一个续延。

  • awaiter 获得返回值。

  • 通过 builder 生成异常;

    不是让 MoveNext()​ 自己抛出异常。

  • 通过 builder 生成返回值或者完成方法。

void IAsyncStateMachine.MoveNext()
{try{switch (this.state){default: goto MethodStart;// case 的数量和 await 表达式的数量相等case 0: goto Label0A;case 1: goto Label1A;case 2: goto Label2A;}MethodStart:// 第一个 await 表达式之前的代码// 此处设置第一个 awaiterLabel0A:// 从延续中恢复执行的代码Label0B:// 快速路径和慢速路径汇合处// 剩余代码(其他标签及 awaiter 等)}catch (Exception e)             //{                               //this.state = -2;            // 通过 builder 填充builder.SetException(e);    // 所有异常信息return;                     //}                               //this.state = -2;        // 通过 builder 填充builder.SetResult();    // 方法完成的信息
}

Tips

虽然 MoveNext()​ 中抛出的异常多数会传递给 builder,不过一些特殊的异常(如 ThreadAbortException​、StackOverflowException​)会直接从 MoveNext()​ 中抛出。

Notice

状态机中的 return 语句,与原始方法中的 return 语句含义不同:

  • 状态机中的 return:状态机为 awaiter 安排延续暂停后,调用该语句
  • 原方法的 return:表示方法完成,对应 try/catch 块外部的 return 语句。

6.2.3 详探 await 表达式

  1. 通过调用 GetAwaiter()​ 来获取 awaiter ,并将其保存到 上。

  2. 检查 awaiter 是否已经完成。如果完成,则可以直接跳转到结果获取(第 9 步)。

    这是 快速 路径。

  3. 如果是慢速路径,通过状态字段来记录 当前执行位置

  4. 使用一个字段记录 awaiter。

  5. 使用 awaiter 来安排一个 续延 ,保证当续延执行时,能够回到正确的状态(根据需要执行装箱操作)。

  6. MoveNext()​ 方法返回到 原始调用方 (如果是第一次暂停),或者返回到 续延安排者中

  7. 当续延调起时,把状态设为正在执行(−1)。

  8. 把 awaiter 从字段中复制到 中,清理字段(帮助回收垃圾)。

  9. 从 awaiter 从获取结果,该结果位于 上。

    这一过程与快速路径或慢速路径无关。即便没有结果值,也需要调用 GetResult()​,以便在必要时 awaiter 可以填充错误信息。

  10. 执行剩余原始代码。

    可以使用异步操作所返回的值。

至此我们回头看上一节的代码(已截取):

MethodStart:awaiter1 = Task.Delay(this.delay).GetAwaiter();if (awaiter1.IsCompleted){goto GetFirstAwaitResult;}this.state = num = 0;this.awaiter = awaiter1;this.builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this);return;
FirstAwaitContinuation:awaiter1 = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;
GetFirstAwaitResult:awaiter1.GetResult();

这些步骤有包含了若干细节:

  • builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this)​ 的调用是 装箱 操作的一部分,它有一个回调方法 SetStateMachine()

    某些时候调用的是 AwaitOnCompleted()​,而非 AwaitUnsafeOnCompleted()​,具体细节见6.5 再探自定义 task 类型

  • num​ 局部变量的存在只是出于优化的目的,该变量的读取都可以视作 this.state ​ 的读取。

6.3 控制流如何影响 MoveNext()

6.3.1 await表达式之间的控制流

本节的讲解将以如下代码为例,它增加了一个循环控制流程:

static async Task PrintAndWaitWithSimpleLoop(TimeSpan delay)
{Console.WriteLine("Before first delay");await Task.Delay(delay);for (int i = 0; i < 3; i++){Console.WriteLine("Between delays");}await Task.Delay(delay);Console.WriteLine("After second delay");
}

相较6.2.1 一个完整的具体示例中的无循环代码,编译器生成的代码对比如下:

GetFirstAwaitResult:awaiter1.GetResult();Console.WriteLine("Between delays");TaskAwaiter awaiter2 = Task.Delay(this.delay).GetAwaiter();
GetFirstAwaitResult:awaiter1.GetResult();for (int i = 0; i < 3; i++){Console.WriteLine("Between delays");}TaskAwaiter awaiter2 = Task.Delay(this.delay).GetAwaiter();

可以看到二者的差别仅是多了一个普通循环。

6.3.2 在循环中使用 await

本节的讲解将以如下代码为例,它仅有一个 await,放在了循环中:

static async Task AwaitInLoop(TimeSpan delay)
{Console.WriteLine("Before loop");for (int i = 0; i < 3; i++){Console.WriteLine("Before await in loop");await Task.Delay(delay);Console.WriteLine("After await in loop");}Console.WriteLine("After loop delay");
}

编译器利用 goto 语句将 for 循环进行拆解,新增的标签涵盖了如下 4 个功能:

  • 循环初始化
  • 循环条件判断
  • 循环体
  • 自增运算

如下是对应的代码示例:

    switch (num){default:goto MethodStart;case 0:goto AwaitContinuation;}
MethodStart:Console.WriteLine("Before loop");this.i = 0;                 // for 循环初始化goto ForLoopCondition;      // 跳转至 for 循环的条件检查
ForLoopBody:                    // for 的循环体Console.WriteLine("Before await in loop");TaskAwaiter awaiter = Task.Delay(this.delay).GetAwaiter();if (awaiter.IsCompleted){goto GetAwaitResult;}this.state = num = 0;this.awaiter = awaiter;this.builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);return;
AwaitContinuation:          // 状态机恢复时的跳转位置awaiter = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;
GetAwaitResult:awaiter.GetResult();Console.WriteLine("After await in loop");this.i++;                   // for 循环中的自增运算
ForLoopCondition:               // for 循环的条件检查if (this.i < 3){goto ForLoopBody;}Console.WriteLine("After loop delay");

6.3.3 在 try/finally 块中使用 await 表达式

Info

C#5 只支持在 try 块中使用 await,C#6 解除了这一限制,支持在 catch、finally 块中使用。

本节的讲解将以如下代码为例,有一个 await 语句位于 try 块中:

static async Task AwaitInTryFinally(TimeSpan delay)
{Console.WriteLine("Before try block");await Task.Delay(delay);try{Console.WriteLine("Before await");await Task.Delay(delay);Console.WriteLine("After await");}finally{Console.WriteLine("In finally block");}Console.WriteLine("After finally block");
}

下面是编译器生成的代码,它有如下特点:

  • try 块前有一个 标签 ,try 块内包含另一个 switch 语句

    在 C# 和 IL 中,都不允许从外部直接跳转到 try 块内部,因此编译器通过额外的 标签switch 语句,实现从外部跳入 try 块内部的功能。作者将该技巧称为“蹦床”

  • finally 块添加了一个 if 判断

    MoveNext()​ 的返回≠原 async 方法执行完毕。此处借助 num 值判断 是否为原 async 方法执行结束

    switch (num){default:goto MethodStart;case 0:goto AwaitContinuationTrampoline;   // 跳转至蹦床之前,以便跳转到正确位置}
MethodStart:Console.WriteLine("Before try");
AwaitContinuationTrampoline:
try
{switch (num)                    //{                               //default:                    //goto TryBlockStart;     // try 块中的蹦床case 0:                     //goto AwaitContinuation; //}                               //
TryBlockStart:Console.WriteLine("Before await");TaskAwaiter awaiter = Task.Delay(this.delay).GetAwaiter();if (awaiter.IsCompleted){goto GetAwaitResult;}this.state = num = 0;this.awaiter = awaiter;this.builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);return;
AwaitContinuation:              // 真正的延续目标awaiter = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;
GetAwaitResult:awaiter.GetResult();Console.WriteLine("After await");
}
finally
{if (num < 0)        // 该判断用于暂停期间忽略 finally 块{Console.WriteLine("In finally block");}
}
Console.WriteLine("After finally block");

6.4 执行上下文和执行流程

​#suspend#​看不懂,略

6.5 再探自定义 task 类型

现在,我们回头看5.8.2 剩下 0.1% 的情况:创建自定义 task 类型中的自定义 Task,每个方法的作用我们都做了讲解:

  • Create() ​ 方法:由桩方法调用,用于创建 builder 实例。
  • AwaitOnCompleted()​、AwaitUnsafeCompleted()​ 方法:状态机内部会对每个 await 表达式创建一个该方法的调用(二者选其一)。方法内部会调用 IAsyncStateMachine.SetStateMachine()​ 方法,进而调用 builder 的 SetStateMachine()​ 方法,完成 装箱
  • SetException() ​、 SetResult() ​ 方法:指示异步操作完成。
public class CustomTaskBuilder<T>
{public static CustomTaskBuilder<T> Create();public void Start<TStateMachine>(ref TStateMachine stateMachine)where TStateMachine : IAsyncStateMachine;public CustomTask<T> Task { get; }public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)where TAwaiter : INotifyCompletionwhere TStateMachine : IAsyncStateMachine;public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)where TAwaiter : INotifyCompletionwhere TStateMachine : IAsyncStateMachine;public void SetStateMachine(IAsyncStateMachine stateMachine);public void SetException(Exception exception);public void SetResult(T result);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/909362.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用Arduino开发STM32

参考文档: https://blog.csdn.net/weixin_42880082/article/details/121619101 qq:505645074

【重点】文件摆渡系统如何简化跨平台跨网络的文件传输?

在当今数字化时代,企业数据的流动性和安全性变得愈发重要。随着业务规模的扩大和全球化协作的深入,跨平台、跨网络的文件传输需求日益增长。然而,传统的文件传输方式往往面临效率低下、安全性不足、兼容性差等问题。文件摆渡系统作为一种专门设计用于简化跨平台、跨网络文件…

想让你多爱自己一些的开源计时器

我用过 Pomotroid、Reminders MenuBar、Timer 等众多优秀、开源的番茄时钟和计时工具,它们帮我更高效地管理时间,让我更专注地投入工作。 但有一个问题始终困扰着我:每当计时结束时,我总是习惯性地忽略休息提醒,继续沉浸在手头的事情中,直到第二天身体不适才追悔莫及。 最…

VMware ESXi 8.0U3d macOS Unlocker OEM BIOS 标准版和厂商定制版,已适配主流品牌服务器

VMware ESXi 8.0U3d macOS Unlocker & OEM BIOS 标准版和厂商定制版,已适配主流品牌服务器VMware ESXi 8.0U3d macOS Unlocker & OEM BIOS 标准版和厂商定制版 ESXi 8.0U3d 标准版,Dell (戴尔)、HPE (慧与)、Lenovo (联想)、Inspur (浪潮)、Cisco (思科)、Hitachi (日…

AMS1117-LDO(线性稳压器)稳压电路

AMS1117-LDO(线性稳压器)稳压电路 原理图引脚说明编号 名称 功能描述1 GND GND2 VOUT 输出3 VIN 输入4 VOUT 输出拓展C8和C7为输出滤波电容,用于抑制自激振荡。如果这两个电容不接,线性稳压器的输出通常会是一个振荡波形。 电容C5和C6是输入电容。对于交流电压整流输入,它…

Avalonia 界面效果 滚动的渐变矩形边框

本文将和大家介绍一个 Avalonia 界面效果,制作一个滚动的渐变矩形边框本文代码基于 Avalonia 11.2.x 版本实现,预期在其他 Avalonia 版本也能正常使用 本文效果由 晓嗔戈 提供,我只是记录此实现方法的工具人 界面效果如下图所示,录制的gif中颜色存在一些偏差,动画有些卡顿…

Avalonia 界面效果 三个圆实现模糊界面动效背景

本文将和大家介绍一个 Avalonia 动效界面效果,由三个圆带模糊效果实现的模糊界面动效背景,适合用在各种 AIGC 主题的应用里面本文代码基于 Avalonia 11.2.x 版本实现,预期在其他 Avalonia 版本也能正常使用 本文效果由 晓嗔戈 提供,我只是记录此实现方法的工具人 界面效果如…

儿子的画

昨天儿子在幼儿园学习了自制小册子,并在上面画画。 晚上睡觉前,心血来潮想要再展示一下他在学校是怎么弄的,于是又一顿操作起来,动作还算麻利,只是完成之后都已过十点了,非要我们帮他配上文字,我一开始不明就理,以为写个标题就好了..., 但最后终于搞懂他是要我帮忙下一…

团队协作管理:贝尔宾团队角色模型学习

“没有完美的个人,只有完美的团队。” 任何企业的领导者要想使自己的企业能够快速地发展和成长,就必须对团队建设的重要性有正确的认识。团队角色理论 团队角色理论是管理学中用于分析和优化团队协作的重要理论,可以帮助管理者认识人才、选拔人才,组建高效率合作团队。也可…

钉钉 + AI 网关给 DeepSeek 办入职

通过 Open-WebUI 在企业内部部署一套 DeepSeek 只是第一步,给 DeepSeek 办理入职,在钉钉等企业通讯工具上和 DeepSeek 对话才是真时尚。通过 Open-WebUI 在企业内部部署一套 DeepSeek 只是第一步,给 DeepSeek 办理入职,在钉钉等企业通讯工具上和 DeepSeek 对话才是真时尚。…

网络工程师修仙指北---STP(Spanning Tree Protocol)

网络工程师修仙指北---STP(Spanning Tree Protocol) Hello哇,欢迎来到《网络工程是修仙指北系列》,今天我们接着上一篇VLAN的内容,继续为大家介绍网络交换二层技术中另一个重要的内容---STP 一口小酒🍸,一首歌📻,阿轩带你修成仙! 上一篇中我们讲到,通过VLAN的技术…

「通义灵码+X」公开课开讲啦!和赛博同桌一起完成开发任务 有奖励

在AI技术重塑未来的今天,阿里云通义灵码团队携手高校开发者,推出「通义灵码+X系列公开课」暨赛博同桌计划,为编程学习注入全新活力!活动将于2025年3月12日至4月30日火热进行,无论你是技术小白还是代码达人,都能在这里找到与AI并肩学习的乐趣,赢取限定好礼!让AI成为你的…