首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 協程 深度解析:從 Iterator 到狀態機的演進之路

     編輯:關於C語言

C# 2.0 引入的 yield return 不只是語法糖,更是編譯器自動生成有限狀態機,在單執行緒上實現協同多任務。本文深度剖析協程本質、C# 實作機制、跨語言對比(Ruby、C++ STL、Boost.Lambda、Win32 Fiber),並展望現代 async/await 的繼承關係。

什麼是協程?

協程 = 行為 + 控制(邏輯+狀態)

  • 行為:處理目標物件(如列印元素)
  • 控制邏輯:如何遍歷(順序、反向、過濾)
  • 控制狀態:當前執行到哪裡(索引、游標、堆疊幀)

foreach 與 yield return 在同一執行緒交替執行,介於「順序執行」與「搶佔式執行緒」之間:調度由程式碼邏輯隱式協同完成,而非 OS 強制切換。

C# 2.0 Iterator 實作機制:編譯器生成狀態機

使用者代碼


public class Tokens : IEnumerable<string>
{
    private readonly string[] _elements = { "a", "b", "c" };

    public IEnumerator<string> GetEnumerator()
    {
        for (int i = 0; i < _elements.Length; i++)
            yield return _elements[i]; // 編譯器介入點
    }
}

// 消費端
foreach (var item in new Tokens())
    Console.WriteLine(item);

編譯器生成的狀態機 (簡化版)


private sealed class <GetEnumerator>d__0 : IEnumerator<string>, IEnumerator, IDisposable
{
    private int <>1__state;      // 狀態:-2=結束, -1=執行中, 0=初始, 1=yield return 後
    private string <>2__current; // 當前 yield 的值
    public Tokens <>4__this;     // 外部 this 參考
    private int <i>5__1;         // 區域變數 i 被提升為欄位

    public string Current => <>2__current;

    public bool MoveNext()
    {
        switch (<>1__state)
        {
            case 0:
                <>1__state = -1;
                <i>5__1 = 0;
                goto case 1;
            case 1:
                if (<i>5__1 < <>4__this._elements.Length)
                {
                    <>2__current = <>4__this._elements[<i>5__1];
                    <>1__state = 1;
                    <i>5__1++;
                    return true; // yield return
                }
                <>1__state = -2;
                return false;
        }
        return false;
    }

    public void Reset() => throw new NotSupportedException();
    public void Dispose() { }
    // IEnumerator.Current 顯式實作...
}

關鍵洞察

  • 區域變數提升為欄位:i 變成 <i>5__1,跨 MoveNext 呼叫保持狀態
  • 狀態編碼:<>1__state 記錄執行位置,switch 跳轉
  • 零執行緒切換開銷:純使用者模式狀態機,效率極高
  • 不修改 CLR:純編譯器技巧,IL 層面仍是普通類別

跨語言協程實作對比

Ruby:原生 yield 關鍵字


def textfiles(dir)
  Dir.chdir(dir)
  Dir["*"].each do |entry|
    yield dir + "\\" + entry if /\.txt$/.match?(entry)
    textfiles(entry) { |file| yield dir + "\\" + file } if File.directory?(entry)
  end
  Dir.chdir("..")
end

textfiles("C:\\") { |file| puts file }

Ruby 的 yield 直接將控制權交給區塊,語言層面內建協程語義。

C++ STL:迭代器 + 演算法 + 謂詞


vector<int> v = {1,2,3};
for_each(v.begin(), v.end(), [](int& x){ x *= -2; }); // C++11 Lambda
  • 迭代器保存控制狀態 (位置)
  • 演算法 (for_each) 表達控制邏輯
  • Lambda/函數物件表達行為
  • 缺點:語義分散、樣板代碼多、需手動管理迭代器生命週期

Boost.Lambda:語法糖緩解


for_each(v.begin(), v.end(), _1 *= -2); // 運算子重載構建表達式樹

仍需顯式傳遞迭代器範圍,控制邏輯/狀態未內建。

C# 1.0:手寫 IEnumerator (痛苦)


public class Tokens : IEnumerable
{
    public IEnumerator GetEnumerator() => new TokenEnumerator(this);

    private class TokenEnumerator : IEnumerator
    {
        private int _pos = -1;
        private readonly Tokens _t;
        public TokenEnumerator(Tokens t) => _t = t;
        public bool MoveNext() => ++_pos < _t._elements.Length;
        public void Reset() => _pos = -1;
        public object Current => _t._elements[_pos];
    }
}

控制邏輯鎖死在 MoveNext,擴展性極差。

Win32 Fiber:使用者模式執行緒


// 偽代碼流程
ConvertThreadToFiber();
fiber = CreateFiber(FiberProc, mainFiber);
while (cur >= 0) { Print(cur); SwitchToFiber(fiber); }
DeleteFiber(fiber);
  • 單執行緒內手動切換堆疊幀
  • .NET 1.x 需自行管理 Managed 執行環境切換,極其複雜
  • Ajai Shankar (MSDN) 曾實作完整 Coroutine 庫

C# 2.0 匿名方法:將行為內嵌至控制流


// C# 2.0 匿名方法 (閉包捕獲區域變數)
list.ForEach(delegate(int x) { Console.WriteLine(x * 2); });

// 相當於 Boost.Lambda,但編譯器自動生成閉包類別
// 為後來的 Lambda (C# 3.0) 與 async/await (C# 5.0) 鋪路

從 Iterator 到 async/await 的演進脈絡

版本特性本質應用場景
C# 2.0yield return同步協程/序列生成器IEnumerable、資料管線
C# 2.0匿名方法閉包/行為內嵌回調、事件處理
C# 3.0Lambda 運算式匿名方法語法糖LINQ、表達式樹
C# 5.0async/await非同步協程/狀態機I/O 非同步、UI 響應

async/await 可視為 yield return 的非同步泛化:

  • yield return:同步產出值,呼叫端 MoveNext() 驅動
  • await:非同步暫停,Task 完成時由 SynchronizationContext 排程續體
  • 同樣由編譯器生成狀態機,同樣將區域變數提升為欄位

最佳實踐與效能建議

  • ✅ 使用 yield return 實現延遲執行、記憶體效率高的序列
  • ✅ 避免在 yield 迭代器中混入副作用(I/O、鎖),導致行為不可預測
  • ✅ 大量資料流處理優先考慮 IAsyncEnumerable<T> (C# 8.0) + await foreach
  • ⚠️ 迭代器不可序列化、不可跨 AppDomain、Reset() 通常拋異常
  • ⚠️ yield 在 try/finally 中需注意 Dispose() 呼叫時機

結語:C# 的協程之路,是從編譯器生成同步狀態機 (yield) 起步,經由閉包捕獲語義 (匿名方法/Lambda),最終演化為運行時排程非同步狀態機 (async/await)。理解這條脈絡,不僅能寫出更高效的迭代器,更能深刻掌握現代 C# 非同步程式設計的底層邏輯。下一步,嘗試手寫一個簡易的 Task 排程器,親自體驗狀態機如何驅動非同步流程。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved