本文將示範如何在 C# 中利用 IOCP(I/O Completion Ports)實現線程間的高效數據傳遞,並深入剖析關鍵 API 的使用細節與最佳實踐,幫助開發者避開非托管資源管理的常見陷阱。
IOCP 機制主要依賴三個 Windows 核心 API:
完成端口是一個核心物件,創建時涉及用戶態到內核態的切換,且必須顯式釋放句柄。最佳實踐採用 Dispose 模式搭配 SafeHandle 衍生類別(如 SafeFileHandle 或 SafeWaitHandle)。
為何必須用 SafeHandle?
CriticalFinalizerObject,CLR 保證其 Finalizer 在極端情況(如 ThreadAbortException、AppDomain 卸載)下仍能執行,確保非托管資源釋放。[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern SafeFileHandle CreateIoCompletionPort(
IntPtr FileHandle,
IntPtr ExistingCompletionPort,
IntPtr CompletionKey,
uint NumberOfConcurrentThreads);
new IntPtr(-1)(即 INVALID_HANDLE_VALUE)。IntPtr.Zero;後續綁定 Socket 時傳入首次創建返回的端口句柄。FileHandle 流動。Socket 程式常傳 Socket 句柄或狀態物件指標,工作線程取出後知悉屬於哪個連接。[DllImport("Kernel32", CharSet = CharSet.Auto)]
private static extern bool PostQueuedCompletionStatus(
SafeFileHandle CompletionPort,
uint dwNumberOfBytesTransferred,
IntPtr dwCompletionKey,
IntPtr lpOverlapped);
OVERLAPPED 結構指標,此處作為攜帶任意用戶數據的指標(單 I/O 數據)。關鍵點:數據經非托管邊界傳遞,不能直接傳遞托管物件引用,必須使用 GCHandle.Alloc(obj, GCHandleType.Normal) 將物件固定在 GC 句柄表中,傳遞其 IntPtr 表示形式((IntPtr)gch)。工作線程收到後用 GCHandle.FromIntPtr 還原並 gch.Free() 釋放。GCHandleType.Normal:防止 GC 回收,但允許移動物件(適用托管-非托管-托管往返)。GCHandleType.Pinned:禁止移動(適用傳遞 byte[] 給非托管填充)。[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; }
}
部分開發者習慣將端口句柄作 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. 重疊結構、線程數配置策略、非托管資源確定性釋放 三大核心,即可構建穩健的高併發網絡服務。