【问题标题】:Calling a PInvoke in an async doesn't return to the main thread after execution在异步中调用 PInvoke 执行后不会返回到主线程
【发布时间】:2020-05-11 16:27:11
【问题描述】:

我正在使用一个非托管库,该库要求对其 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)

【问题讨论】:

  • 有什么理由不使用 System.Threading 命名空间(例如使用自定义任务调度程序)? stackoverflow.com/questions/30719366/…
  • @SimonMourier 是的,首先它比创建事件循环要多得多:)。然后,我实际上一开始就尝试过,但我想不出一个好的解决方案来让它与我用来轮询其中一些 API 调用的 RX Observables 一起工作,这个解决方案更适合我正在做的其他事情。而且,对我来说更重要的是,即使我最终以不同的方式做事,我也想了解为什么这不起作用。它做了一些非常反直觉的事情,打破了我对 C# 中 async/await 的理解
  • 控制台应用之所以特别,是因为它没有设置 SynchronizationContext:devblogs.microsoft.com/pfxteam/…。但是,我已经对 EventLoopScheduler 特定的测试做了一些测试,但它仍然无法正常工作。 pastebin.com/raw/Kh2Yw9pn 闻起来很糟糕。它适用于“常规”任务/TaskScheduler。
  • 有趣。这是netfx还是netcore?我没有对 EventLoopScheduler 做任何事情,但我认为这个想法是它安装了一个自定义同步上下文,队列在初始线程上工作?
  • 如果我有时间的话,我明天将不得不玩 Rx,但我的第一个猜测是 EventLoopScheduler 本身并没有设置同步上下文,而只是在 observable 内部使用。当您执行await <observable> 时,将使用当前同步上下文,这是控制台应用程序的默认上下文,与 EventLoopScheduler 无关,这意味着您将获得默认的“从线程池中选择线程”。那里有一些优化,很容易让你回到同一个线程进行同步代码。

标签: c# multithreading async-await pinvoke system.reactive


【解决方案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,但概念是一样的。因此,当任务完成时,它会在同一个线程上下文中调用状态机 += 1 的状态机。只有在使用任务调度程序时才能看到任务池线程。

抽象中的泄漏

您看到这种特殊行为的原因的解释:

After managed call on thread "" (1)

相当复杂。它与预定的 thunk 是否立即完成有关。如果您在第一个托管调用中添加Thread.Sleep,您会注意到继续在事件循环线程上运行。

这是由于调度优化更倾向于仅对if something is currently running 进行排队。当您调用ToTask() 时,您使用的是默认调度程序,即当前线程调度程序。

当前线程调度器是这样工作的:

免费?立即运行。

忙吗?排队工作。

立即运行行为是您看到日志在主线程上运行的原因。 如果你只是添加一个

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

一开始,您使事件循环忙碌,导致所有内容都进入队列,因此您可以看到所有内容都记录在事件循环线程中。所以这与 P/Invoke 无关。

需要明确的是,这不是指定用于观察的调度程序,而是订阅。当您将 Observable 转换为其他抽象,如任务、枚举、阻塞连接等时,可能会泄漏一些内部复杂性。

【讨论】:

  • 这种“只在当前线程上立即排队”的行为是一个很好的优化,但可能会产生很大的问题。我记得读过一篇来自 Stephens 的文章(我认为)关于他们如何在某些框架代码中解决这个问题以确保堆栈不会被炸毁(因为如果它立即执行,堆栈会不断增长,与如果它被发布到线程池)。
  • 我想我记得。 queue-if-required 行为是导致lotsubtle bugs 的原因。但它是高性能的必要条件。
  • 非常感谢@Asti 的精彩解释,那么您将如何确保只有Run 方法中的代码在特定线程上运行,而不是await 之后的继续运行?
  • 如果你真的希望它在主线程上运行,你必须使用Task.Wait 而不是await(不推荐)。只需将另一个调度程序传递给 ToTask 方法。
  • 有一个ObserveOn 重载,它接受同步上下文。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多