Boost.Asio學習(5):c++的協程

協程是什么?

協程就是可以“暫停”和“繼續”的函數,像在函數里打個斷點,然后以后可以從斷點繼續運行,而不是重新開始。

線程 vs 協程:類比

想象你在寫小說:

  • 線程

    • 你開了 3 個作者(線程),每個人都在寫書。

    • 系統(OS)來回切換誰在寫。

    • 切換的時候要收拾桌子、換椅子、拿走稿紙(上下文切換),開銷大。

  • 協程

    • 你只有 1 個作者,但可以寫一會兒暫停,去做別的,再回來接著寫。

    • 暫停的時候,只需在稿紙上插個書簽(掛起點),下次從書簽繼續,開銷極小。

特性線程協程
切換方式搶占式(OS決定)協作式(程序決定)
切換成本(要保存寄存器、堆棧)(只是保存協程狀態)
并發多線程可以真正并行協程必須在同一個線程輪流跑
控制權OS 控制程序員控制(通過 co_await
  • 協程 ≠ 線程,它只是單線程內的“任務切換”機制。

  • 協程可以跑在一個線程上,也可以結合線程池,讓多個線程各自跑多個協程 → 混合使用

  • 協程不提供并行(不會用多個 CPU 核心),但可以輕松實現異步(不會阻塞線程)。

協程解決了什么問題?

傳統異步寫法(回調):

async_read(socket, buf, [](auto ec, auto size) {async_write(socket, buf, [](auto ec, auto size) {// 回調地獄});
});

協程寫法:

co_await async_read(socket, buf);
co_await async_write(socket, buf);

為什么開銷小?

  • 線程切換:需要保存 CPU 寄存器、切換內核態、切換棧,可能上百納秒甚至微秒級。

  • 協程切換:只保存協程狀態(程序計數器 + 局部變量),在用戶態完成,納秒級。

使用

  • C++20 正式引入協程機制,底層通過 編譯器轉換 + 棧幀對象(promise) 實現。

  • 三個關鍵字:

    • co_await —— 掛起并等待某個任務。

    • co_yield —— 掛起并返回一個值(類似生成器)。

    • co_return —— 返回最終值并結束協程。

協程的運行方式

協程不是線程,也不是普通函數,而是 編譯器把協程函數轉換成狀態機,并返回一個“協程句柄”。

流程:

  1. 編譯器檢測函數是否包含 co_await/co_yield/co_return

  2. 如果包含,編譯器會生成:

    • 協程狀態對象(包含局部變量、掛起點、promise)。

    • 協程句柄std::coroutine_handle<>)控制 resume/destroy。

  3. 第一次調用協程函數

    • 返回一個 awaitable 對象,不會立刻執行全部邏輯。

  4. 調用 resume()

    • 協程運行到 co_await 掛起。

  5. 再次 resume()

    • 從上次掛起點繼續。

關鍵字作用
co_return返回最終結果并結束協程
co_yield產出一個值并掛起(生成器)
co_await掛起并等待某個操作完成

常見協程返回類型

  • std::generator<T>(C++23 提供)

  • 自定義 task<T>

  • 第三方庫:

    • cppcoro(最常用)

    • Boost.Coroutine

?協程常見寫法

(1)最簡單的生成器(C++23)

#include <coroutine>
#include <iostream>
#include <generator>std::generator<int> numbers() {for (int i = 0; i < 5; ++i)co_yield i;
}int main() {for (auto v : numbers())std::cout << v << "\n";
}

(2)異步任務(自定義 Task)

#include <coroutine>
#include <iostream>// 1. 定義返回類型 Task
struct Task {// 2. promise_type:編譯器需要它來生成協程對象struct promise_type {// 編譯器調用:創建協程返回值Task get_return_object() {return Task{std::coroutine_handle<promise_type>::from_promise(*this)};}// 協程開始前是否掛起?std::suspend_never initial_suspend() noexcept { return {}; }// 協程結束后是否掛起?std::suspend_never final_suspend() noexcept { return {}; }// 處理 co_returnvoid return_void() {}void unhandled_exception() {} // 異常處理};std::coroutine_handle<promise_type> handle;Task(std::coroutine_handle<promise_type> h): handle(h) {}~Task() { if(handle) handle.destroy(); }
};// 3. 協程函數:編譯器自動用 Task::promise_type 來管理
Task myCoroutine() {std::cout << "Hello\n";co_await std::suspend_always{}; // 掛起協程std::cout << "World\n";
}int main() {auto t = myCoroutine(); // 創建協程,執行到 co_await 掛起std::cout << "Resume...\n";t.handle.resume(); // 恢復協程,繼續執行
}

自定義 Task詳細解析

為什么要定義 promise_type

  • C++20 協程和普通函數完全不同,編譯器在看到協程時不會直接返回值,而是生成一個狀態機對象(Frame)

  • 協程的返回類型必須定義一個 嵌套類型 promise_type

  • 編譯器規則

    • 當你寫:

Task myCoroutine() {co_await something;co_return;
}

編譯器會查找 Task::promise_type,并生成代碼去:

  1. 創建一個 promise_type 對象。

  2. 調用它的 get_return_object(),把結果作為協程的返回值。

  3. 調用 initial_suspend() / final_suspend() 控制是否立即掛起。

  4. 調用 return_void()return_value() 處理 co_return

所以:

  • promise_type 就是協程的“大腦”,控制協程生命周期。

  • 它必須提供一組固定接口(編譯器調用)。

initial_suspend和final_suspend? ? ? ?

  • initial_suspend():協程剛創建時是否掛起?
    • 返回 std::suspend_always掛起,調用者必須手動 resume()

    • 返回 std::suspend_never不掛起,協程立即執行。

  • final_suspend():協程結束后是否掛起?
    • 一般返回 std::suspend_always,讓調用者決定何時銷毀協程。

  • 為什么要控制掛起?

    • 這關系到執行模型(立即運行 or 手動恢復)。

std::coroutine_handle是干啥的

  • 它是一個 輕量級句柄,指向協程幀(frame)。

  • 協程幀包含:

    • 狀態機(狀態編號)

    • 局部變量

    • 一個 promise_type 對象(由返回類型的 promise_type 定義)

  • std::coroutine_handle 提供操作協程生命周期的方法

    • 恢復協程(resume()

    • 銷毀協程(destroy()

    • 判斷是否執行完(done()

本質:它類似一個 void*,但知道協程幀布局,可以安全操作 promise 對象。

重點:協程不是線程,編譯器不會自動跑完,你必須顯式調用?resume(),所以要有句柄。

為什么模板參數是 promise_type

  • 協程幀里有一個 promise_type 成員。

  • coroutine_handle<promise_type> 是一個類型安全的 handle,可以直接訪問 promise 對象:

handle.promise()  // 返回對 promise 對象的引用

?這就是為什么你的 Task 返回類型必須定義 promise_type,編譯器才能生成 coroutine_handle<promise_type>

如果不關心 promise,可以用 無模板版本

std::coroutine_handle<> // 泛型 handle,不提供 promise()。

類成員函數?

1. resume()

  • 恢復協程執行。

  • 如果協程在掛起狀態,繼續執行,直到下一個掛起點或結束。

  • 所以resume()的同步地、阻塞的

2. done()

  • 判斷協程是否執行完畢(到達 final_suspend() 之后)。done() == true 表示協程執行到 final_suspend()

3. destroy()

  • 銷毀協程幀(釋放內存),必須在協程完全結束后調用,否則 UB。

4. promise()

  • 返回 promise_type&,可以訪問自定義邏輯,比如取值、狀態。

舉例按流程詳細解釋

代碼流程:

#include <coroutine>
#include <iostream>// 1. 協程返回類型 Task
struct Task {struct promise_type {// 編譯器會調用此函數獲取協程返回對象Task get_return_object() {std::cout << "[promise_type] get_return_object()\n";return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}std::suspend_always initial_suspend() noexcept {std::cout << "[promise_type] initial_suspend()\n";return {}; // 創建后立即掛起}std::suspend_always final_suspend() noexcept {std::cout << "[promise_type] final_suspend()\n";return {}; // 協程結束后掛起,等外部銷毀}void return_void() { std::cout << "[promise_type] return_void()\n"; }void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> handle;explicit Task(std::coroutine_handle<promise_type> h): handle(h) {}~Task() {if (handle) {std::cout << "[Task] destroy coroutine\n";handle.destroy(); // 銷毀協程 frame}}void resume() {std::cout << "[Task] resume()\n";handle.resume(); // 恢復協程}
};// 2. 協程函數:執行順序受 suspend 控制
Task myCoroutine() {std::cout << "[Coroutine] Start\n";co_await std::suspend_always{}; // 掛起std::cout << "[Coroutine] After co_await\n";co_return;
}int main() {auto t = myCoroutine();  // 創建協程(不會立刻跑完整個函數)std::cout << "[main] first resume\n";t.resume();              // 執行到 co_await 掛起std::cout << "[main] second resume\n";t.resume();              // 執行到 co_return,final_suspend 掛起
}

輸出:

[promise_type] get_return_object()
[promise_type] initial_suspend()
[main] first resume
[Task] resume()
[Coroutine] Start
[Coroutine] After co_await
[promise_type] return_void()
[promise_type] final_suspend()
[main] second resume
[Task] destroy coroutine

執行過程詳細解讀:

Step 1: 調用 myCoroutine()
  • 編譯器看到 co_awaitmyCoroutine 改造成協程,不直接返回 Task,而是:

    1. 編譯器硬編碼規則

      1. 如果協程返回類型是 R,就去找 R::promise_type

      2. 通過 R::promise_type 生成一個 promise_type?類的對象promise。

    2. 調用 get_return_object() → 返回 Task,并綁定 coroutine_handle

      • std::coroutine_handle<promise_type>::from_promise(*this):把 promise 和 handle 關聯。

    3. 調用 initial_suspend() → 返回 suspend_always,協程掛起,不執行函數體。

  • 此時:協程 frame(狀態機)在堆上創建,但沒開始執行

對象狀態

  • Task:持有 coroutine_handle

  • promise_type:存儲協程狀態。

  • handle:指向協程 frame,能控制 resume()

Step 2: t.resume()
  • 調用 handle.resume()

  • 協程開始執行,打印 [Coroutine] Start

  • 遇到 co_await std::suspend_always{} → 立即掛起。

    • 編譯器調用 await_suspend()(內部由 suspend_always 實現)。

  • 控制權返回 main,協程停在 co_await 這一行。

Step 3: 第二次 t.resume()
  • 協程從 co_await 后恢復。

  • 打印 [Coroutine] After co_await

  • 執行 co_return → 編譯器調用 promise_type.return_void()

  • 協程執行到尾,進入 final_suspend() → 返回 suspend_always,協程再次掛起,等待銷毀。

Step 4: 退出 main()Task 析構
  • 調用 handle.destroy() → 銷毀協程 frame 和 promise_type

  • 協程徹底結束

關鍵點匯總

  • promise_type 是協程的“控制器”,編譯器強制要求它有:

    • get_return_object()(返回 Task)

    • initial_suspend() / final_suspend()

    • return_void() / return_value()

  • coroutine_handle 是“遙控器”,用來 resume()destroy()

  • co_await 觸發掛起點

  • co_return 結束協程

  • 狀態機 + Frame 在堆上管理生命周期

co_yield使用詳細解釋

編譯器在看到 co_yield expr; 時,會將它轉換成 promise_type.yield_value(expr) 的調用,并把返回值當作 co_await 的對象。

也就是說:

co_yield value;

等價于:

co_await promise.yield_value(value);

所以:

  • co_yield 會調用 promise_type::yield_value(T value)

  • 這個函數必須返回一個 Awaitable 對象(通常是 std::suspend_alwaysstd::suspend_never)。

  • 編譯器會像 co_await 一樣調用:

    • await_ready() → 決定是否掛起。

    • await_suspend() → 掛起時的動作。

    • await_resume() → 恢復后取值。

舉個完整例子:生成器

#include <coroutine>
#include <iostream>struct Generator {struct promise_type {int current_value;Generator get_return_object() {return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void return_void() {}void unhandled_exception() { std::terminate(); }// **核心:co_yield 調用這個**std::suspend_always yield_value(int value) noexcept {std::cout << "[promise_type] yield_value(" << value << ")\n";current_value = value;return {}; // 掛起}};std::coroutine_handle<promise_type> handle;explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}~Generator() { if (handle) handle.destroy(); }bool move_next() {if (!handle.done()) {handle.resume();}return !handle.done();}int current_value() { return handle.promise().current_value; }
};// 協程函數
Generator numbers() {for (int i = 1; i <= 3; ++i) {co_yield i; // 調用 yield_value(i)}
}int main() {auto gen = numbers();while (gen.move_next()) {std::cout << "[main] got: " << gen.current_value() << "\n";}
}

執行流程

[promise_type] yield_value(1)
[main] got: 1
[promise_type] yield_value(2)
[main] got: 2
[promise_type] yield_value(3)
[main] got: 3

執行到 co_yield 時發生了什么?

  • 編譯器調用 promise_type.yield_value(i)

  • 這個函數保存值(current_value = i),返回 std::suspend_always

  • 協程掛起(await_suspend())。

  • 控制權返回 main(),用戶取值。

  • 下一次 resume(),協程從 co_yield 之后繼續。

return_value用法

co_return 在 C++20 協程中不僅可以 co_return;,還可以 co_return value;,對應 promise_typereturn_value() 方法(或 return_void())。

  • co_return 用于在協程函數里 返回結果結束協程

  • 兩種形式:

    1. co_return;:無返回值(void 協程)。

    2. co_return expr;:返回一個值。

編譯器會把 co_return 翻譯成 調用 promise 對象的方法

  • co_return;promise.return_void();

  • co_return expr;promise.return_value(expr);

例子:

#include <coroutine>
#include <iostream>
#include <optional>struct Task {struct promise_type {std::optional<int> value;Task get_return_object() {return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void return_value(int v) { value = v; }  // 支持 co_return v;void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> h;explicit Task(std::coroutine_handle<promise_type> h) : h(h) {}~Task() { if (h) h.destroy(); }int get() {h.resume();return *h.promise().value;}
};Task compute() {std::cout << "Computing...\n";co_return 42;  // 等價于 promise.return_value(42);
}int main() {Task t = compute();std::cout << "Result = " << t.get() << "\n";
}

協程函數不會像普通函數那樣直接“返回值”給調用者,而是:

  • 立即返回一個包裝了 coroutine_handle 的對象(例如 Task

  • 真正的“結果”要么通過:

    • promise_type 存儲(由 co_return 設置值),

    • 或由外部代碼通過 handle 或包裝類(如 Task::get())訪問。

這個std::optional<T>又是個啥?

std::optional<T> 是 C++17 引入的一個標準庫模板類,用來表示 “一個可能有值,也可能沒有值的對象”

在協程里,它常用來延遲存儲返回值,因為:

  • 協程一開始不會立即有返回值(因為可能掛起)。

  • 但最終會 co_return value;,所以我們需要一個容器,能先處于“空”狀態,等有值再存進去。

std::optional<T> 基本概念

  • 它是一個模板類,可以包含類型 T 的值,或者為空。

  • 類似于 T*(指針),但更安全,因為不會出現懸空指針,也不需要動態分配。

  • 常用接口:

std::optional<int> opt;    // 默認無值
opt.has_value();           // 判斷是否有值
opt = 42;                  // 賦值
int x = *opt;              // 取值(必須有值,否則未定義)

為什么用 optional 而不是直接 int?

  • 因為協程返回 Task<int>,在協程掛起時我們沒有值。

  • 如果直接用 int value;,我們沒法區分“值是 0”還是“值還沒準備好”。

  • std::optional<int> 可以:

    • 初始狀態 nullopt(空)。

    • return_value(42) 時賦值。

    • 之后通過 *optionaloptional.value() 取值。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/94219.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/94219.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/94219.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux 中,命令查看系統版本和內核信息

在 Linux 中&#xff0c;可以通過以下命令查看系統版本和內核信息&#xff1a;1. 查看內核版本uname -a或精簡顯示&#xff1a;uname -r # 只顯示內核版本示例輸出&#xff1a;Linux ubuntu 5.4.0-135-generic #152-Ubuntu SMP Tue Nov 15 08:12:21 UTC 2022 x86_64 x86_64 x8…

數據結構總綱以及單向鏈表詳解:

以下是基于筆記更詳細的知識梳理&#xff0c;從概念到細節逐層拆解&#xff0c;幫你吃透數據結構核心要點&#xff1a; 數據結構部分的重點內容&#xff1a;一、數據結構基礎框架 &#xff08;一&#xff09;邏輯結構&#xff08;關注元素間“邏輯關系”&#xff09; 筆記里提到…

模型學習系列之參數

背景 “GLM-4.5擁有 3550 億總參數量&#xff0c;其中 320 億活躍參數&#xff1b;GLM-4.5-Air 采用更緊湊的設計&#xff0c;擁有 1060 億總參數量&#xff0c;其中 120 億活躍參數。” 定義與關系 總參數量&#xff1a;模型中所有可訓練參數的總和&#xff08;包括嵌入層、注…

[創業之路-535]:軟件需要原型驗證、產品需要原型驗證、商業模式也需要原型驗證

原型驗證在軟件、產品開發以及商業模式探索中均扮演著至關重要的角色&#xff0c;它通過低成本、快速迭代的方式&#xff0c;幫助團隊驗證核心假設、降低風險并優化方案。以下是針對這三個領域的具體分析&#xff1a;一、軟件原型驗證&#xff1a;從概念到可交互的模型核心目的…

sublime text2配置

sublime text2配置背景配置其他背景 之前下載了就把它當記事本在使用。但是&#xff0c;在使用過程中&#xff0c;有些場景很痛苦。如果說找一個字符串中的某一部分&#xff0c;雖然它通過了這個功能&#xff0c;但是不夠明顯&#xff0c;看瞎了。。。 配置 下面是我改的一些選…

本地通信的選擇:為什么組播比廣播更適合多進程協作?

零、深入解析Linux本地通信機制,對比廣播與組播的核心差異 本地組播能讓多進程收到消息,而本地廣播不行,核心原因在于兩者的設計目標、網絡協議處理邏輯以及內核轉發機制存在本質差異。具體可以從以下幾個角度理解: 1. 通信模式與目標地址的本質區別 組播(Multicast):…

7-Django項目實戰[user]-發送郵件激活賬號

1.前期準備&#xff08;以QQ郵箱為例&#xff09; 登錄QQ郵箱 獲取授權碼 2.settings.py文件配置 1&#xff09;緩存配置 # 配置緩存 CACHES {# 郵件激活隨機數"default": {"BACKEND": "django_redis.cache.RedisCache","LOCATION&q…

社群團購市場選擇與開源技術賦能下的下沉市場開拓策略研究——以開源AI智能名片、鏈動2+1模式與S2B2C商城小程序為例

摘要&#xff1a;在社群團購行業面臨流量成本攀升與同質化競爭的背景下&#xff0c;下沉市場因其龐大用戶基數與未被充分滿足的消費需求&#xff0c;成為創業者突破增長瓶頸的關鍵賽道。本文以拼多多成功開拓小城鎮與農村市場的案例為切入點&#xff0c;結合開源AI智能名片、鏈…

Ollama前端:open-webui

github&#xff1a;https://github.com/open-webui/open-webui 官網&#xff1a;&#x1f3e1; Home | Open WebUI 1、docker安裝&#xff08;GPU&#xff09;&#xff1a; docker run -d -p 3000:8080 --gpusall -v ollama:/root/.ollama -v open-webui:/app/backend/data …

LeetCode513:找樹最左下角的值(bfs+dfs)

文章目錄一、 題目描述解法一&#xff1a;層序遍歷 (BFS) - 最直觀的解法核心思路代碼實現優缺點分析解法二&#xff1a;遞歸 (DFS) - 更深度的思考核心思路代碼實現優缺點分析四、 總結與對比LeetCode 513 - 尋找樹的最后一行的最左側的值&#xff0c;【難度&#xff1a;中等&…

把“評論”菜單從WordPress后臺移除的3種方法

在WordPress后臺移除“評論”菜單&#xff0c;可以通過以下幾種方法實現。以下是詳細步驟&#xff1a; 方法1&#xff1a;通過代碼移除(推薦) 將以下代碼添加到主題的functions.php文件中(或使用CodeSnippets插件)&#xff1a; // 移除后臺左側菜單的“評論” add_action(ad…

大語言模型 LLM 通過 Excel 知識庫 增強日志分析,根因分析能力的技術方案(4):只要過一遍LLM的簡約版本

文章大綱 只要過一遍LLM的簡約版本 1 設計原理(一句話) 2 極簡數據流 3 最小依賴實現(本地 SQLite + OpenAI 兼容端點) 3.1 一次性準備:Excel → SQLite 3.2 關鍵詞提取 + 查表(正則 / SQL) 3.3 單次 LLM 調用 4 運行結果示例 5 性能 & Token 對比 6 可擴展點 7 參考…

(轉)mybatis和hibernate的 緩存區別?

MyBatis 和 Hibernate 都是流行的 Java 持久化框架&#xff0c;它們都提供了自己的緩存機制來優化數據庫操作&#xff0c;減少數據庫的訪問次數&#xff0c;提高應用程序的性能。盡管兩者都支持緩存&#xff0c;但是它們的緩存實現方式和配置有所不同。1. 緩存機制的基本區別My…

【linux內核系列】:萬字詳解進程間通信:消息隊列

&#x1f525; 本文專欄&#xff1a;Linux &#x1f338;作者主頁&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客勵志語錄&#xff1a; 你討厭的現在&#xff0c;是未來的你拼命想回去修正的戰場。 ★★★ 本文前置知識&#xff1a; 匿名管道 命名管道 共享內存 前…

React 19 革命性升級:編譯器自動優化,告別手動性能調優時代

概述 React 19 是 React 框架的一個重要里程碑版本&#xff0c;帶來了眾多突破性的改進和新特性。本文檔將詳細介紹 React 19 的主要變化&#xff0c;幫助開發者了解并遷移到新版本。 &#x1f680; 主要新特性 React Compiler (編譯器) React 19 引入了全新的 React Compi…

UE5的渲染Debug技巧

ShaderPrint UE5相對UE4使用的ComputeShader(GPU Driven)的地方多很多。因為UE5為了方便查看ComputeShader的某些值&#xff0c;開發了“ShaderPrint”&#xff0c;方便直接在Shader 打印信息到屏幕&#xff0c;而不用采用CPUReadback在print的方式。 比如r.nanite.ShowStats…

【2025/08/03】GitHub 今日熱門項目

GitHub 今日熱門項目 &#x1f680; 每日精選優質開源項目 | 發現優質開源項目&#xff0c;跟上技術發展趨勢 &#x1f4cb; 報告概覽 &#x1f4ca; 統計項&#x1f4c8; 數值&#x1f4dd; 說明&#x1f4c5; 報告日期2025-08-03 (周日)GitHub Trending 每日快照&#x1f55…

Android系統模塊編譯調試與Ninja使用指南

模塊編譯調試方法 (此處舉例framework、installd、SystemUI等模塊的編譯調試&#xff0c;其他類似) 1. Framework模塊編譯 Android系統代碼的framework目錄內&#xff0c;一共有3個模塊單獨編譯&#xff1a;framework、services、framework-res.apk。 注意&#xff1a;偶爾會有…

【硬件-筆試面試題】硬件/電子工程師,筆試面試題-51,(知識點:stm32,GPIO基礎知識)

目錄 1、題目 2、解答 3、相關知識點 一、GPIO 基本結構與特性 1. GPIO 硬件結構 2. 主要特性 二、GPIO 工作模式 1. 輸入模式 2. 輸出模式 3. 復用功能模式 4. 特殊模式 三、GPIO 配置步驟&#xff08;以 STM32Cube HAL 庫為例&#xff09; 1. 初始化 GPIO 時鐘 …

小智服務器Java安裝編譯(xinnan-tech)版

github&#xff1a;https://github.com/xinnan-tech/xiaozhi-esp32-server 一、JDK 1、JDK21下載&#xff1a; https://www.oracle.com/cn/java/technologies/downloads/#jdk21-windows RPM安裝&#xff1a; rpm -ivh jdk-21_linux-x64_bin.rpm 2、IDEA設置JDK File → P…