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

C# 實作 IOCP(I/O 完成端口)完整範例與核心細節解析

     編輯:關於C#

本文將示範如何在 C# 中利用 IOCP(I/O Completion Ports)實現線程間的高效數據傳遞,並深入剖析關鍵 API 的使用細節與最佳實踐,幫助開發者避開非托管資源管理的常見陷阱。

核心 API 概覽

IOCP 機制主要依賴三個 Windows 核心 API:

  • CreateIoCompletionPort:建立或關聯完成端口物件。
  • PostQueuedCompletionStatus:向完成端口佇列投遞自定義完成狀態(模擬 I/O 完成或發送用戶數據)。
  • GetQueuedCompletionStatus:工作線程調用,從完成端口佇列中取出完成狀態,無消息時高效等待。

關鍵細節:非托管資源的安全管理

完成端口是一個核心物件,創建時涉及用戶態到內核態的切換,且必須顯式釋放句柄。最佳實踐採用 Dispose 模式搭配 SafeHandle 衍生類別(如 SafeFileHandle 或 SafeWaitHandle)。

為何必須用 SafeHandle?

  • 管理引用計數,防止句柄洩漏。
  • 繼承自 CriticalFinalizerObject,CLR 保證其 Finalizer 在極端情況(如 ThreadAbortException、AppDomain 卸載)下仍能執行,確保非托管資源釋放。
  • 避免在指派句柄給指標的瞬間發生異常導致洩漏。

API 簽名與參數深度解析

CreateIoCompletionPort

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern SafeFileHandle CreateIoCompletionPort(
    IntPtr FileHandle,
    IntPtr ExistingCompletionPort,
    IntPtr CompletionKey,
    uint NumberOfConcurrentThreads);
  • FileHandle:要綁定的句柄(如 Socket)。首次創建端口時傳 new IntPtr(-1)(即 INVALID_HANDLE_VALUE)。
  • ExistingCompletionPort:現有端口句柄。首次創建傳 IntPtr.Zero;後續綁定 Socket 時傳入首次創建返回的端口句柄。
  • CompletionKey:單句柄數據,隨 FileHandle 流動。Socket 程式常傳 Socket 句柄或狀態物件指標,工作線程取出後知悉屬於哪個連接。
  • NumberOfConcurrentThreads:允許同時在端口上運行的線程數。傳 0 表示等於 CPU 核心數。注意:工作線程數通常應大於此值,因線程可能阻塞;若線程數僅等於 CPU 核心數且有線程阻塞,CPU 會閒置。

PostQueuedCompletionStatus

[DllImport("Kernel32", CharSet = CharSet.Auto)]
private static extern bool PostQueuedCompletionStatus(
    SafeFileHandle CompletionPort,
    uint dwNumberOfBytesTransferred,
    IntPtr dwCompletionKey,
    IntPtr lpOverlapped);
  • 用於手動向工作線程投遞消息(如關閉通知、業務指令)。
  • lpOverlapped:原意是 OVERLAPPED 結構指標,此處作為攜帶任意用戶數據的指標(單 I/O 數據)。關鍵點:數據經非托管邊界傳遞,不能直接傳遞托管物件引用,必須使用 GCHandle.Alloc(obj, GCHandleType.Normal) 將物件固定在 GC 句柄表中,傳遞其 IntPtr 表示形式((IntPtr)gch)。工作線程收到後用 GCHandle.FromIntPtr 還原並 gch.Free() 釋放。
  • GCHandleType.Normal:防止 GC 回收,但允許移動物件(適用托管-非托管-托管往返)。GCHandleType.Pinned:禁止移動(適用傳遞 byte[] 給非托管填充)。

GetQueuedCompletionStatus

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool GetQueuedCompletionStatus(
    SafeFileHandle CompletionPort,
    out uint lpNumberOfBytesTransferred,
    out IntPtr lpCompletionKey,
    out IntPtr lpOverlapped,
    uint dwMilliseconds);
  • 工作線程循環調用,dwMilliseconds 傳 0xFFFFFFFF(INFINITE)無限等待。
  • 返回值:true 表示成功取出完成狀態(含 I/O 完成或 PostQueuedCompletionStatus 投遞);false 表示超時或錯誤(需檢查 Marshal.GetLastWin32Error())。
  • lpOverlapped 為 IntPtr.Zero 且 lpCompletionKey 非零時,通常表示端口關閉或特殊信號(視協議而定)。

完整可運行範例代碼

需開啟專案屬性 → Build → Allow unsafe code。

using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

[StructLayout(LayoutKind.Sequential)]
class PerIoData
{
    public string Data;
}

public class IocpDemo
{
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern SafeFileHandle CreateIoCompletionPort(
        IntPtr FileHandle, IntPtr ExistingCompletionPort,
        IntPtr CompletionKey, uint NumberOfConcurrentThreads);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool GetQueuedCompletionStatus(
        SafeFileHandle CompletionPort,
        out uint lpNumberOfBytesTransferred,
        out IntPtr lpCompletionKey,
        out IntPtr lpOverlapped,
        uint dwMilliseconds);

    [DllImport("Kernel32", CharSet = CharSet.Auto)]
    private static extern bool PostQueuedCompletionStatus(
        SafeFileHandle CompletionPort,
        uint dwNumberOfBytesTransferred,
        IntPtr dwCompletionKey,
        IntPtr lpOverlapped);

    public static void RunDemo()
    {
        // 1. 創建完成端口,不綁定任何句柄,併發線程數=1
        var port = CreateIoCompletionPort(new IntPtr(-1), IntPtr.Zero, IntPtr.Zero, 1);
        if (port.IsInvalid)
        {
            Console.WriteLine($"CreateIoCompletionPort 失敗: {Marshal.GetLastWin32Error()}");
            return;
        }

        // 2. 啟動工作線程
        var worker = new Thread(WorkerProc);
        worker.Start(port);

        // 3. 主線程投遞兩條自定義消息
        SendMessage(port, "hi, 我是主線程發送的第一條數據");
        SendMessage(port, "關閉工作線程吧"); // 退出指令

        Console.WriteLine("主線程執行完畢,按任意鍵退出...");
        Console.ReadKey();
    }

    static void SendMessage(SafeFileHandle port, string text)
    {
        var data = new PerIoData { Data = text };
        var gch = GCHandle.Alloc(data, GCHandleType.Normal);
        Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 主線程發送: {text}");
        // dwNumberOfBytesTransferred 傳 sizeof(IntPtr) 即可
        PostQueuedCompletionStatus(port, (uint)IntPtr.Size, IntPtr.Zero, (IntPtr)gch);
    }

    static void WorkerProc(object state)
    {
        var port = (SafeFileHandle)state;
        while (true)
        {
            Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 工作線程等待消息...");
            bool ok = GetQueuedCompletionStatus(port,
                out uint bytes, out IntPtr key, out IntPtr overlapped, 0xFFFFFFFF);

            if (!ok || bytes == 0) continue; // 錯誤或特殊信號

            var gch = GCHandle.FromIntPtr(overlapped);
            var data = (PerIoData)gch.Target;
            Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 收到: {data.Data}");
            gch.Free();

            if (data.Data == "關閉工作線程吧")
            {
                Console.WriteLine("收到退出指令,工作線程結束。");
                port.Dispose();
                break;
            }
        }
    }

    public static int Main() { RunDemo(); return 0; }
}

補充:替代 P/Invoke 簽名

部分開發者習慣將端口句柄作 uint 傳遞,並直接用 GCHandle 的 IntPtr 形式作為 lpOverlapped:

[DllImport("Kernel32")]
private static extern bool PostQueuedCompletionStatus(
    uint completionPort, int bytesTransferred,
    IntPtr completionKey, IntPtr overlapped);

[DllImport("Kernel32")]
private static extern bool GetQueuedCompletionStatus(
    uint completionPort, ref int bytesTransferred,
    ref IntPtr completionKey, ref IntPtr overlapped,
    uint milliseconds);

調用時:PostQueuedCompletionStatus((uint)port.DangerousGetHandle(), 4, (IntPtr)gchKey, (IntPtr)gchValue); 原理相同,僅參數類型不同。

總結

IOCP 是 Windows 高效能非同步 I/O 的基石。C# 透過 P/Invoke 結合 SafeHandle 與 GCHandle,能安全、高效地驅動此機制。掌握 完成鍵 vs. 重疊結構、線程數配置策略、非托管資源確定性釋放 三大核心,即可構建穩健的高併發網絡服務。

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