簡體   English   中英

在異步中調用 PInvoke 執行后不會返回到主線程

[英]Calling a PInvoke in an async doesn't return to the main thread after execution

我正在使用一個非托管庫,該庫要求對其 API 的所有調用都在同一線程上運行。 我們想使用響應式擴展的EventLoopScheduler來促進這一點,因為我們將使用 Observable 來處理其他事情。

我正在使用類似於下面代碼示例中的Run方法的方法來執行調度程序中的代碼,該調度程序將始終在同一線程上運行。 當我使用托管代碼時,它按預期工作,並且所有調用都在事件循環管理的線程上運行,並且異步調用之前/之后是主線程。

但是,當我調用 P/Invoke 時(代碼示例中的那個只是一個示例,我並沒有在我的代碼中真正調用這個,但行為是相同的),線程確實在事件循環線程上運行,但之后的一切都是如此!

我嘗試添加ConfigureAwait(true) (和false ),但它沒有改變任何東西。 我真的對這種行為感到困惑,為什么調用 P/Invoke 會改變等待后繼續的線程?!?

這是要重現的代碼:

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

public static Task Run(Action action, IScheduler scheduler)
{
    return Observable.Start(action, scheduler).SingleAsync().ToTask();
}

public static string ThreadInfo() =>
    $"\"{Thread.CurrentThread.Name}\" ({Thread.CurrentThread.ManagedThreadId})";

private static async Task Main(string[] args)
{
    var scheduler = new EventLoopScheduler();

    Console.WriteLine($"Before managed call on thread {ThreadInfo()}");

    await Run(() => Console.WriteLine($"Managed call on thread {ThreadInfo()}"), scheduler);

    Console.WriteLine($"After managed call on thread {ThreadInfo()}");

    Console.WriteLine($"Before PInvoke on thread {ThreadInfo()}");

    await Run(() => MessageBox(IntPtr.Zero, $"Running on thread {ThreadInfo()}", "Attention", 0), scheduler);

    Console.WriteLine($"After PInvoke on thread {ThreadInfo()}");
}

執行返回如下內容:

Before managed call on thread "" (1)
Managed call on thread "Event Loop 1" (6)
After managed call on thread "" (1)
Before PInvoke on thread "" (1)
Message box displayed with text: Running on thread "Event Loop 1" (6)
After PInvoke on thread "Event Loop 1" (6)

我期望的地方

Before managed call on thread "" (1)
Managed call on thread "Event Loop 1" (6)
After managed call on thread "" (1)
Before PInvoke on thread "" (1)
Message box displayed with text: Running on thread "Event Loop 1" (6)
After PInvoke on thread "" (1)

任務

Task或 promise 只是回調的抽象。 而 async/await 只是任務的語法糖。

因為它是一個回調抽象,所以await不會阻塞線程。 為什么它看起來像阻塞? 這是因為await將您的代碼重寫為狀態機,當正在等待的任務完成時,該狀態機會前進。

它大致被重寫如下:

switch (state)
{
    case 0:
        Console.WriteLine($"Before managed call on thread {ThreadInfo()}");
        Await(Run(() => Console.WriteLine($"Managed call on thread {ThreadInfo()}"), scheduler));
        return;
    case 1:

        Console.WriteLine($"After managed call on thread {ThreadInfo()}");
        Console.WriteLine($"Before PInvoke on thread {ThreadInfo()}");
        Await(Run(() => MessageBox(IntPtr.Zero, $"Running on thread {ThreadInfo()}", "Attention", 0), scheduler));
        return;
    case 2:
        Console.WriteLine($"After PInvoke on thread {ThreadInfo()}");
        return;
}

實際的重寫使用goto而不是switch ,但概念是相同的。 因此,當任務完成時,它會在同一線程上下文中使用 state += 1 調用此狀態機。 當您使用任務調度程序時,您只會看到任務池線程。

抽象中的泄漏

為什么您會看到這種特殊行為的解釋:

After managed call on thread "" (1)

相當復雜。 它與預定的 thunk 是否立即完成有關。 如果您在第一個托管調用中添加Thread.Sleep ,您會注意到繼續在事件循環線程上運行。

這是由於調度優化傾向於僅在當前正在運行的情況下排隊。 當您調用ToTask()時,您使用的是默認調度程序,即當前線程調度程序。

當前的線程調度程序是這樣工作的:

自由的? 立即運行。

忙碌的? 排隊工作。

立即運行行為是您看到日志在主線程上運行的原因。 如果你只是添加一個

var scheduler = new EventLoopScheduler();
scheduler.Schedule(() => Thread.Sleep(1000));

一開始,您使事件循環忙碌,導致 go 的所有內容都進入隊列,因此您隨后會在事件循環線程中看到所有日志。 所以這與 P/Invoke 無關。

需要明確的是,這不是關於指定用於觀察的調度程序,而是訂閱。 當您將 Observable 轉換為其他抽象,如任務、枚舉、阻塞連接等時,可能會泄漏一些內部復雜性。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM