亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現(xiàn)在的位置是:首頁 > 技術閱讀 >  微信終端自研 C++協(xié)程框架的設計與實現(xiàn)

微信終端自研 C++協(xié)程框架的設計與實現(xiàn)

時間:2024-02-11

作者:peterfan,騰訊 WXG 客戶端開發(fā)工程師

來源:公眾號騰訊技術工程


背景

基于跨平臺考慮,微信終端很多基礎組件使用 C++ 編寫,隨著業(yè)務越來越復雜,傳統(tǒng)異步編程模型已經(jīng)無法滿足業(yè)務需要。Modern C++ 雖然一直在改進,但一直沒有統(tǒng)一編程模型,為了提升開發(fā)效率,改善代碼質量,我們自研了一套 C++ 協(xié)程框架 owl,用于為所有基礎組件提供統(tǒng)一的編程模型。

owl 協(xié)程框架目前主要應用于 C++ 跨平臺微信客戶端內核(Alita),Alita 的業(yè)務邏輯部分全部用協(xié)程實現(xiàn),相比傳統(tǒng)異步編程模型,至少減少了 50% 代碼量。Alita 目前已經(jīng)應用于兒童手表微信、Linux 車機微信、Android 車機微信等多個業(yè)務,其中 Linux 車機微信的所有 UI 邏輯也全部用協(xié)程實現(xiàn)

為什么要造輪子?

那么問題來了,既然 C++20 已經(jīng)支持了協(xié)程,業(yè)界也有不少開源方案(如 libco、libgo 等),為什么不直接使用?

原因:

  • owl 基礎庫需要支持盡量多的操作系統(tǒng)和架構,操作系統(tǒng)包括:Android、iOS、macOS、Windows、Linux、RTOS;架構包括:x86、x86_64、arm、arm64、loongarch64,目前并沒有任何一個方案能直接支持。
  • owl 協(xié)程自 2019 年初就推出了,而當時 C++20 還未成熟,實際上到目前為止 C++20 普及程度依然不高,公司內部和外部合作伙伴的編譯器版本普遍較低,導致目前 owl 最多只能用到 C++14 的特性
  • 業(yè)界已有方案有不少缺點:
    1. 大多為后臺開發(fā)設計,不適用終端開發(fā)場景
    2. 基本只支持 Linux 系統(tǒng)和 x86/x86_64 架構
    3. 封裝層次較低,大多是玩具或 API 級別,并沒有達到框架級別
    4. 在 C++ 終端開發(fā)沒有看到大規(guī)模應用案例

Show me the code

那么協(xié)程比傳統(tǒng)異步編程到底好在哪里?下面我們結合代碼來展示一下協(xié)程的優(yōu)勢,同時也回顧一下異步編程模型的演化過程:

假設有一個異步方法 AsyncAddOne,用于將一個 int 值加 1,為了簡單起見,這里直接開一個線程 sleep 100ms 后再回調新的值:

void AsyncAddOne(int value, std::function<void (int)> callback) {
    std::thread t([value, callback = std::move(callback)] {
        std::this_thread::sleep_for(100ms);
        callback(value + 1);
    })
;
    t.detach();
}

要調用 AsyncAddOne 將一個 int 值加 3,有三種主流寫法:

1、Callback

傳統(tǒng)回調方式,代碼寫起來是這樣:

AsyncAddOne(100, [] (int result) {
    AsyncAddOne(result, [] (int result) {
        AsyncAddOne(result, [] (int result) {
            printf("result %d\n", result);
        });
    });
});

回調有一些眾所周知的痛點,如回調地獄、信任問題、錯誤處理困難、生命周期管理困難等,在此不再贅述。

2、Promise

Promise 解決了 Callback 的痛點,使用 owl::promise 庫的代碼寫起來是這樣:

// 將回調風格的 AsyncAddOne 轉成 Promise 風格
owl::promise AsyncAddOnePromise(int value) {
    return owl::make_promise([=] (auto d) {
        AsyncAddOne(value, [=] (int result) {
            d.resolve(result);
        });
    });
}

// Promise 方式
AsyncAddOnePromise(100)
.then([] (int result) {
    return AsyncAddOnePromise(result);
})
.then([] (int result) {
    return AsyncAddOnePromise(result);
})
.then([] (int result) {
    printf("result %d\n", result);
});

很顯然,由于消除了回調地獄,代碼漂亮多了。實際上 owl::promise 解決了 Callback 的所有痛點,通過使用模版元編程和類型擦除技術,甚至連語法都接近 JavaScript Promise。

但實踐發(fā)現(xiàn),Promise 只適合線性異步邏輯,復雜一點的異步邏輯用 Promise 寫起來也很亂(如循環(huán)調用某個異步接口),因此我們廢棄了 owl::promise,最終將方案轉向了協(xié)程。

3、Coroutine

使用 owl 協(xié)程寫起來是這樣:

// 將回調風格的 AsyncAddOne 轉成 Promise 風格
// 注:
// owl::promise 擦除了類型,owl::promise2 是類型安全版本
// owl 協(xié)程需要配合 owl::promise2 使用
owl::promise2<intAsyncAddOnePromise2(int value) {
    return owl::make_promise2<int>([=] (auto d) {
        AsyncAddOne(value, [=] (int result) {
            d.resolve(result);
        });
    });
}

// Coroutine 方式
// 使用 co_launch 啟動一個協(xié)程
// 在協(xié)程中即可使用 co_await 將異步調用轉成同步方式
owl::co_launch([] {
    auto value = 100;
    for (auto i = 0; i < 3; i++) {
        value = co_await AsyncAddOnePromise2(value);
    }
    printf("result %d\n", value);
});

使用協(xié)程可以用同步方式寫異步代碼,大大減輕了異步編程的心智負擔。co_await 語法糖讓 owl 協(xié)程寫起來跟很多語言內置的協(xié)程并無差別。

回調轉協(xié)程

要在實際業(yè)務中使用協(xié)程,必須通過某種方式讓回調代碼轉換為協(xié)程支持的形式。通過上面的例子可以看出,回調風格接口要支持在協(xié)程中同步調用非常簡單,只需短短幾行代碼將回調接口先轉成 Promise 接口,在協(xié)程中即可直接通過 co_await 調用:

// 回調接口
void AsyncAddOne(int value, std::function<void (int)> callback);

// Promise 接口
owl::promise2<intAsyncAddOnePromise2(int value);

// 協(xié)程中調用
auto value = co_await AsyncAddOnePromise2(100);

實際項目中通常會省略掉上述中間步驟,直接一步到位:

// 在協(xié)程中可以像調用普通函數(shù)一樣調用此函數(shù)
int AsyncAddOneCoroutine(int value) {
    return co_await owl::make_promise2<int>([=] (auto d) {
        AsyncAddOne(value, [=] (int result) {
            d.resolve(result);
        });
    });
}

后臺開發(fā)使用協(xié)程,通常會 hook socket 相關的 I/O API,而終端開發(fā)很少需要在協(xié)程中使用底層 I/O 能力,通常已經(jīng)封裝好了高層次的異步 I/O 接口,因此 owl 協(xié)程并沒有 hook I/O API,而是提供一種方便的將回調轉協(xié)程的方式。

一個完整的例子

上述代碼片段很難體現(xiàn)出協(xié)程的實際用法,這個例子使用協(xié)程實現(xiàn)了一個 tcp-echo-server,只有 40 多行代碼:

int main(int argc, char* argv[]) {
    // 使用 co_thread_scope() 創(chuàng)建一個協(xié)程作用域,并啟動一個線程作為協(xié)程調度器
    co_thread_scope() {
        owl::tcp_server server;
        int error = server.listen(3090);
        if (error < 0) {
            zerror("tcp server listen failed!");
            return;
        }
        zinfo("tcp server listen OK, local %_", server.local_address());

        while (true) {
            auto client = server.accept();
            if (!client) {
                zerror("tcp server accept failed!");
                break;
            }

            zinfo("accept OK, local %_, peer %_", client->local_address(), client->peer_address());

            // 當有新 client 連接時,使用 co_launch 啟動一個協(xié)程專門處理
            owl::co_launch([client] {
                char buf[1024] = { 0 };
                while (true) {
                    auto num_recv = client->recv_some(buf, sizeof(buf), 0);
                    if (num_recv <= 0) {
                        break;
                    }
                    buf[num_recv] = '\0';
                    zinfo("[fd=%_] RECV %_ bytes: %_", client->fd(), num_recv, buf);

                    if (strcmp(buf, "exit") == 0) {
                        break;
                    }
                    auto num_send = client->send(buf, num_recv, 0);
                    if (num_send < 0) {
                        break;
                    }
                    zinfo("[fd=%_] SENT %_ bytes back", client->fd(), num_send);
                }
            });
        }
    };
    return 0;
}

框架分層

為了便于擴展和復用,owl 協(xié)程采用分層設計,開發(fā)者可以直接使用最上層的 API,也可以基于 Context API 或 Core API 搭建自己的協(xié)程框架。

協(xié)程設計

協(xié)程棧

協(xié)程按有無調用棧分為兩類:

  • 有棧協(xié)程(stackful):每個協(xié)程都有自己的調用棧,類似于線程的調用棧
  • 無棧協(xié)程(stackless):協(xié)程沒有調用棧,協(xié)程的狀態(tài)通過狀態(tài)機或閉包來實現(xiàn)

很顯然,無棧協(xié)程比有棧協(xié)程占用更少的內存,但無棧協(xié)程通常需要手動管理狀態(tài),如果自研協(xié)程采用無棧方式會非常難用。因此語言級別的協(xié)程通常使用無棧協(xié)程,將復雜的狀態(tài)管理交給編譯器處理;自研方案通常使用有棧協(xié)程,owl 也不例外是有棧協(xié)程

有棧協(xié)程按棧的管理方式又可以分為兩類:

  • 獨立棧:每個協(xié)程都有獨立的調用棧
  • 共享棧:每個協(xié)程都有獨立的狀態(tài)棧,一個線程中的多個協(xié)程共享一個調用棧。由于這些協(xié)程中同時只會有一個協(xié)程處于活躍狀態(tài),當前活躍的協(xié)程可以臨時使用調用棧。當此協(xié)程被掛起時,將調用棧中的狀態(tài)保存到自身的狀態(tài)棧;當協(xié)程恢復運行時,將狀態(tài)棧再拷貝到調用棧。實踐中通常設置較大的調用棧和較小的狀態(tài)棧,來達到節(jié)省內存的目的。

共享棧本質上是一種時間換空間的做法,但共享棧有一個比較明顯的缺點,看代碼:

owl::co_launch("co1", [] {
    char buf[1024] = { 0 };
    auto job = owl::co_launch("co2", [&buf] {
        // oops!!!
        buf[0] = 'a';
    });
    job->join();
});

上面的代碼在共享棧模式下會出問題,協(xié)程 co1 在棧上分配的 buf,在協(xié)程 co2 訪問的時候已經(jīng)失效了。要規(guī)避共享棧的這個缺點,可能需要對協(xié)程的使用做一些限制或檢查,無疑會加重使用者的負擔。

對于終端開發(fā),由于同時運行的協(xié)程數(shù)量并不多,性能問題并不明顯,為了使用上的便捷性,owl 協(xié)程使用獨立棧

選擇獨立棧之后,協(xié)程棧應該如何分配又是另外的問題,有如下幾種方案:

  • Split Stacks:簡單來說是一個支持自動增長的非連續(xù)棧,由于只有 gcc 支持且有兼容性問題,實踐中比較少用
  • malloc/mmap:直接使用 malloc 或 mmap 分配內存,業(yè)界主流方案
  • Thread Stack:在線程中預先分配一大段棧內存作為協(xié)程棧,業(yè)界比較少用

后兩種方案通常還會采用內存池來優(yōu)化性能,采用 mprotect 來進行棧保護

owl 協(xié)程同時使用了后兩種方案,那么什么場景下會使用到 Thread Stack 方案呢?因為 Android JNI 和部分 RTOS 系統(tǒng)調用 會檢查 sp 寄存器是否在線程棧空間內,如果不在則認為棧被破壞,程序會直接掛掉。獨立棧協(xié)程在執(zhí)行時 sp 寄存器會被修改為指向協(xié)程棧,而通過 malloc/mmap 分配的協(xié)程棧空間不屬于任何線程棧,一定無法通過 sp 檢查。為了解決這個問題,我們在 Android 和部分 RTOS 上默認使用 Thread Stack

協(xié)程調度

協(xié)程按控制傳遞機制分為兩類:

  • 對稱協(xié)程(Symmetric Coroutine):和線程類似,協(xié)程之間是對等關系,多個協(xié)程之間可以任意跳轉
  • 非對稱協(xié)程(Asymmetric Coroutine):協(xié)程之間存在調用和被調用關系,如協(xié)程 A 調用/恢復協(xié)程 B,協(xié)程 B 掛起/返回時只能回到協(xié)程 A

非對稱協(xié)程與函數(shù)調用類似,比較容易理解,主流編程語言對協(xié)程的支持大都是非對稱協(xié)程。從實現(xiàn)的角度,非對稱協(xié)程的實現(xiàn)也比較簡單,實際上我們很容易用非對稱協(xié)程實現(xiàn)對稱協(xié)程。owl 協(xié)程使用非對稱協(xié)程

上圖展示了非對稱協(xié)程調用和函數(shù)調用的相似性,詳細的時序如下:

  1. 調用者調用 co_create() 創(chuàng)建協(xié)程,這一步會分配一個單獨的協(xié)程棧,并為 func 設置好執(zhí)行環(huán)境
  2. 調用者調用 co_resume() 啟動協(xié)程,func 函數(shù)開始運行
  3. 協(xié)程運行到 co_yield(),協(xié)程掛起自己并返回到調用者
  4. 調用者調用 co_resume() 恢復協(xié)程,協(xié)程從 co_yield() 后續(xù)代碼繼續(xù)執(zhí)行
  5. 協(xié)程執(zhí)行完畢,返回到調用者

如上圖所示,有意思的是,如果一個協(xié)程沒用調用 co_yield(),這個協(xié)程的調用流程其實跟函數(shù)一模一樣,因此我們經(jīng)常會說:函數(shù)就是協(xié)程的一種特例

單線程調度器

協(xié)程和線程很像,不同的是線程多是搶占式調度,而協(xié)程多是協(xié)作式調度。多個線程之間共享資源時通常需要鎖和信號量等同步原語,而協(xié)程可以不需要。

通過上面的示例可以看出,使用 co_create() 創(chuàng)建協(xié)程后,可以通過不斷調用 co_resume() 來驅動協(xié)程的運行,而協(xié)程函數(shù)可以隨時調用 co_yield() 來掛起自己并將控制權轉移給調用者。

很顯然,當協(xié)程數(shù)量較多時,通過手工調用 co_resume() 來驅動協(xié)程不太現(xiàn)實,因此需要實現(xiàn)協(xié)程調度器。

協(xié)程調度器分為兩類:

  • 1:N 調度(單線程調度):使用 1 個線程調度 N 個協(xié)程,由于多個協(xié)程都在同一個線程中運行,因此協(xié)程之間訪問共享資源無需加鎖
  • M:N 調度(多線程調度):使用 M 個線程調度 N 個協(xié)程,由于多個協(xié)程可能不在同一個線程運行,甚至同一個協(xié)程每次調度都有可能運行在不同線程,因此協(xié)程之間訪問共享資源需要加鎖,且協(xié)程中使用 TLS(Thread Local Storage) 會有問題

單線程調度通常使用 RunLoop 之類的消息循環(huán)來作為調度器,雖然調度性能低于多線程調度,但單線程調度器可以免加鎖的特性,能極大降低編碼復雜度,因此 owl 協(xié)程使用單線程調度

使用 RunLoop 作為調度器的原理其實很簡單,將所有 co_resume() 調用都 Post 到 RunLoop 中執(zhí)行即可。原理如圖所示,要想象一個協(xié)程是如何在 RunLoop 中執(zhí)行的,大概可以認為是:協(xié)程函數(shù)中的代碼被 co_yield() 分隔成多個部分,每一部分代碼都被 Post 到 RunLoop 中執(zhí)行

使用 RunLoop 作為調度器除了協(xié)程不用加鎖,還有一些額外的好處:

  • 協(xié)程中的代碼可以和 RunLoop 中的傳統(tǒng)異步代碼和諧共處
  • 若使用 UI 框架的 RunLoop 作為調度器,從協(xié)程中可以直接訪問 UI

為了方便擴展,owl 協(xié)程將調度器抽象成一個單獨的接口類,開發(fā)者可以很容易實現(xiàn)自己的調度器,或和項目已有的 RunLoop 機制結合:

class executor {
public:
    virtual ~executor() {}
    virtual uint64_t post(std::function<void ()> closure) 0;
    virtual uint64_t post_delayed(unsigned delay, std::function<void ()> closure) 0;
    virtual void cancel(uint64_t id) {}
};

在 Linux 車機微信客戶端,我們通過實現(xiàn)自定義調度器讓協(xié)程運行在 UI 框架的消息循環(huán)中,得以方便地在協(xié)程中訪問 UI。

協(xié)程間通信

通過使用單線程調度器,多個協(xié)程之間訪問共享資源不再需要多線程的鎖機制了。

那么用協(xié)程寫代碼是否就完全不需要加鎖呢?看代碼:

static int value = 0;
for (auto i = 0; i < 4; ++i) {
    owl::co_launch([] {
        value++;
        owl::co_delay(1000);
        value--;
        printf("value %d\n", value);
    });
}

假設協(xié)程中要先將 value++,做完一些事情,再將 value--,我們期望最終 4 個協(xié)程的輸出都是 0。但由于 owl::co_delay(1000) 這一行導致了協(xié)程調度,最終輸出結果必然不符合預期。

一些協(xié)程庫為了解決這種問題,提供了和多線程鎖類似的協(xié)程鎖機制。好不容易避免了線程鎖,又要引入?yún)f(xié)程鎖,難道沒有更好的辦法了嗎?

實際上目前主流的并發(fā)模型除了共享內存模型,還有 Actor 模型與 CSP(Communicating Sequential Processes)模型,對比如下:

Do not communicate by sharing memory; instead, share memory by communicating. 不要通過共享內存來通信,而應該通過通信來共享內存

相信這句 Go 語言的哲學大家已經(jīng)不陌生了,如何理解這句話?本質上看,多個線程或協(xié)程之間同步信息最終都是通過共享內存來進行的,因為無論是用哪種通信模型,最終都是從內存中獲取數(shù)據(jù),因此這句話我們可以理解為 盡量使用消息來通信,而不要直接共享內存

Actor 模型和 CSP 模型采用的都是消息機制,區(qū)別在于 Actor 模型里協(xié)程與消息隊列(mailbox)是綁定關系;而 CSP 模型里協(xié)程與消息隊列(channel)是獨立的。從耦合性的角度,CSP 模型比 Actor 模型更松耦合,因此 owl 協(xié)程使用 channel 作為協(xié)程間通信機制

由于我們在實際業(yè)務開發(fā)中并沒有遇到一定需要協(xié)程鎖的場景,因此 owl 協(xié)程暫沒有提供協(xié)程鎖機制。

結構化并發(fā)

想象這樣一個場景:我們寫一個 UI 界面,在這個界面會啟動若干協(xié)程通過網(wǎng)絡去拉取和更新數(shù)據(jù),當用戶退出 UI 時,為了不泄露資源,我們希望協(xié)程以及協(xié)程發(fā)起的異步操作都能取消。當然,我們可以通過手動保存每一個協(xié)程的句柄,在 UI 退出時通知每一個協(xié)程退出,并等待所有協(xié)程都結束后再退出 UI。然而,手動進行上述操作非常繁瑣,而且很難保證正確性。

不止是使用協(xié)程才會遇到上述問題,把協(xié)程換成線程,問題依然存在。傳統(tǒng)并發(fā)主要有兩類問題:

  • 生命周期問題:如何保證協(xié)程引用的資源不被突然釋放?
  • 協(xié)程取消問題:1)如何打斷正在掛起的協(xié)程?2)結束協(xié)程時,如何同時結束協(xié)程中創(chuàng)建的子協(xié)程?3)如何等待所有子協(xié)程都結束后再結束父協(xié)程?

這里的主要矛盾在于:協(xié)程是獨立的,但業(yè)務是結構化的

為了解決這個問題,owl 協(xié)程引入了結構化并發(fā)

結構化并發(fā)的概念是:

  • 作用域中的并發(fā)操作,必須在作用域退出前結束
  • 作用域可以嵌套

作用域是一個抽象概念,有明確生命周期的實體都是作用域,如:

  • 一個代碼塊
  • 一個對象
  • 一個 UI 頁面

如上圖所示,代碼由上而下執(zhí)行,在進入外部 scope 后,從 scope 中啟動了兩個協(xié)程,并進入了內部 scope,當執(zhí)行流最終從外部 scope 出來時,結構化并發(fā)機制必須保證這兩個協(xié)程已經(jīng)結束。同樣的,若內部 scope 中啟動了協(xié)程,執(zhí)行流從內部 scope 出來時,也必須保證其中的協(xié)程全部結束。

結構化并發(fā)在 owl 協(xié)程的實現(xiàn)其實并不復雜,本質上是一個樹形結構:

核心理念是:

  • 協(xié)程也是一個作用域
  • 協(xié)程有父子關系
  • 父協(xié)程取消,子協(xié)程也自動取消
  • 父協(xié)程結束前,必須等待子協(xié)程結束

光說概念有點抽象,最后來看一個 owl 協(xié)程結構化并發(fā)的例子:

class SimpleActivity {
public:
    SimpleActivity() {
        // 為 scope_ 設置調度器,后續(xù)通過 scope_ 啟動的協(xié)程
        // 默認使用 UI 的消息循環(huán)作為調度器
        scope_.set_exec(GetUiExecutor());
    }

    ~SimpleActivity() {
        // UI 銷毀的時候取消所有子協(xié)程
        scope_.cancel();
        // scope_ 析構時會等待所有子協(xié)程結束
    }

    void OnButtonClicked() {
        // 在 UI 事件中通過 scope_ 啟動協(xié)程
        scope_.co_launch([=] {
            // 啟動子協(xié)程下載圖片
            auto p1 = owl::co_async([] { return DownloadImage(...); });
            auto p2 = owl::co_async([] { return DownloadImage(...); });

            // 等待圖片下載完畢
            auto image1 = co_await p1;
            auto image2 = co_await p2;

            // 合并圖片
            auto new_image = co_await AsyncCombineImage(image1, image2);

            // 更新圖片,由于協(xié)程運行在消息循環(huán)中,可以直接訪問 UI
            image_->SetImage(new_image);
        });

        // 可以通過 scope_ 啟動任意多個協(xié)程
        scope_.co_launch([=] {
            ...
        });
    }

private:
    owl::co_scope scope_;
    ImageLabel* image_;
};

性能測試

說明:

  • 上下文切換:使用 Context API 進行上下文切換的性能,耗時在 20~30ns 級別
  • 協(xié)程切換:使用單線程調度器進行協(xié)程切換的性能,耗時在 0.5~3us 級別
  • 線程切換:pthread 線程切換的性能,耗時在 2~8us 級別

owl 協(xié)程受限于單線程調度器性能,切換速度和上下文切換比并不算快,但在終端使用也足夠了。

總結

總的來說,自 owl 協(xié)程在實際項目中應用以來,開發(fā)效率和代碼質量都有很大提升。owl 協(xié)程雖然已經(jīng)得到廣泛應用,但還存在很多不完善的地方,未來會繼續(xù)迭代打磨。owl 現(xiàn)階段在騰訊內部開源,待框架更完善且 API 穩(wěn)定后,再進行對外開源。



往期推薦



圖解|30張圖,帶你深入理解CPU流水線和分支預測的那些事兒

多線程異步【日志系統(tǒng)】,高效、強悍的實現(xiàn)方式:雙緩沖!

如何閱讀開源項目代碼

new[]和delete[]一定要配對使用嗎?

分享一個編程設計小技巧(沒有兩三年工作經(jīng)驗估計看不懂)

C++20新特性的小細節(jié)



RECOMMEND

- 點個在看你最好看 -


主站蜘蛛池模板: 上饶县| 东乌珠穆沁旗| 措勤县| 台南县| 定襄县| 桐柏县| 佛坪县| 桂林市| 沁源县| 临沧市| 长阳| 古交市| 全椒县| 临桂县| 福贡县| 萍乡市| 全椒县| 竹山县| 来宾市| 长岭县| 乌鲁木齐县| 专栏| 通山县| 乳山市| 临海市| 五家渠市| 高阳县| 芜湖县| 红原县| 鹿泉市| 长乐市| 大化| 龙口市| 崇阳县| 泽州县| 双辽市| 奉新县| 彭阳县| 东丽区| 方山县| 镇远县|