【问题标题】:Run async task continuation on native function callback thread在本机函数回调线程上运行异步任务延续
【发布时间】:2015-10-26 18:53:15
【问题描述】:

我有一个 C 函数 FsReadStream,它执行一些异步工作并接受回调。完成后,它使用QueueUserWorkItem windows 函数调用回调。

我正在尝试使用 async/await 模式从托管代码 (c#) 调用此函数。所以我做了以下

  1. 构造一个Task 对象,向构造函数传递一个返回结果的lambda。
  2. 使用RunSynchronously 方法构造一个运行此任务的回调
  3. 调用异步原生函数,传入回调
  4. 将任务对象返回给调用者

我的代码看起来像这样

/// Reads into the buffer as many bytes as the buffer size
public Task<ReadResult> ReadAsync(byte[] buffer)
{
    GCHandle pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    IntPtr bytesToRead = Marshal.AllocHGlobal(sizeof(long));
    Marshal.WriteInt64(bytesToRead, buffer.Length);

    FsAsyncInfo asyncInfo = new FsAsyncInfo();
    ReadResult readResult = new ReadResult();

    Task<ReadResult> readCompletionTask = new Task<ReadResult>(() => { return readResult; });
    TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();

    asyncInfo.Callback = (int status) =>
    {
        readResult.ErrorCode = status;
        readResult.BytesRead = (int)Marshal.ReadInt64(bytesToRead);
        readCompletionTask.RunSynchronously(scheduler);
        pinnedBuffer.Free();
        Marshal.FreeHGlobal(bytesToRead);
    };

    // Call asynchronous native method    
    NativeMethods.FsReadStream(
                    pinnedBuffer.AddrOfPinnedObject(),
                    bytesToRead,
                    ref asyncInfo);

    return readCompletionTask;
}

我这样称呼它

ReadResult readResult = await ReadAsync(data);

我有两个问题

  1. 如何使调用await ReadAsync 后运行的代码与回调在同一线程上运行?目前,我看到它在不同的线程上运行,即使我正在调用readCompletionTask.RunSynchronously。我在 ASP.NET 和 IIS 下运行此代码。
  2. 本机QueueUserWorkItem 函数是否使用与托管ThreadPool.QueueUserWorkItem 方法相同的线程池?我的意见是应该这样做,因此托管 TaskScheduler 应该可以在本机回调线程上安排任务。

【问题讨论】:

    标签: c# windows async-await pinvoke


    【解决方案1】:

    您不应该在现代代码中使用 Task 构造函数。完全没有。曾经。它没有用例。

    在这种情况下,您应该使用TaskCompletionSource&lt;T&gt;

    如何使调用 await ReadAsync 之后运行的代码与回调在同一线程上运行?

    你不能保证; await 只是不能那样工作。如果代码绝对必须在同一个线程上执行,那么它应该直接从回调中调用。

    但是,如果只是首选在同一个线程上执行,那么您不必做任何特别的事情; await 已经使用了ExecuteSynchronously 标志:

    public Task<ReadResult> ReadAsync(byte[] buffer)
    {
      var tcs = new TaskCompletionSource<ReadResult>();
      GCHandle pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    
      IntPtr bytesToRead = Marshal.AllocHGlobal(sizeof(long));
      Marshal.WriteInt64(bytesToRead, buffer.Length);
    
      FsAsyncInfo asyncInfo = new FsAsyncInfo();
      asyncInfo.Callback = (int status) =>
      {
        tcs.TrySetResult(new ReadResult
        {
          ErrorCode = status;
          BytesRead = (int)Marshal.ReadInt64(bytesToRead);
        });
        pinnedBuffer.Free();
        Marshal.FreeHGlobal(bytesToRead);
      };
    
      NativeMethods.FsReadStream(pinnedBuffer.AddrOfPinnedObject(), bytesToRead, ref asyncInfo);
    
      return tcs.Task;
    }
    

    本机 QueueUserWorkItem 函数是否使用与托管 ThreadPool.QueueUserWorkItem 方法相同的线程池?

    没有。这是两个完全不同的线程池。

    【讨论】:

    • 有趣。是否有不鼓励使用 Task ctor 的 msdn。您实际上是在声称 Task ctor 和 Task.RunSynchronously 方法均已弃用。
    • @tcb:不。MSDN(和大多数 Microsoft 文档)是描述性的,而不是规定性的。我在我的博客上总结了我关于why the Task constructor is uselesswhy RunSynchronously is useless 的论点。
    • 值得注意的是,在 ASP.NET 和 IIS(@tcb 指定为运行时环境)下,await ReadAsync(data) 不会在同一个线程上继续,除非它是 await ReadAsync(data).ConfigureAwait(false)
    • @Noseratio:我相信它会在同一个线程上继续。不过,我还没有测试过。
    • @StephenCleary,不幸的是不会,至少在AspNetSynchronizationContext的当前实现中不会:stackoverflow.com/q/23062154/1768303。我仍然不知道他们为什么要走那条路。
    【解决方案2】:

    如何使调用 await ReadAsync 之后运行的代码与回调在同一线程上运行?

    这是不可能的。 ExecuteSynchronously 不是保证。 RunSynchronously 也不保证。您当然可以传入一个回调并同步调用该回调。

    另外,FromCurrentSynchronizationContext 返回什么?我的蜘蛛感告诉我这是基于一个误解......

    本机 QueueUserWorkItem 函数是否使用与托管 ThreadPool.QueueUserWorkItem 方法相同的线程池?

    我不这么认为,即使是这种情况,您也无法针对特定线程。您只能针对特定的池。

    为什么需要在同一个线程上执行?通常,提出这个问题的人确实想要并且需要其他东西。


    您创建和返回任务的方式很奇怪。为什么不使用基于TaskCompletionSource 的标准模式?


    我认为你有一个 GC 漏洞,因为没有什么能让 asyncInfo.Callback 活着。可以在本地调用进行时将其收集起来。在回调中使用GC.KeepAlive

    【讨论】:

    • 我使用了Task ctor 而不是TaskCompletionSource,因为这允许我在运行任务时指定调度程序。调度程序的类型为System.Threading.Tasks.SynchronizationContextTaskScheduler。感谢您指出 GC 漏洞。
    • 在回调线程上尝试执行await后的代码,避免线程间的上下文切换
    • 您可以为任务指定调度程序,但不能为延续指定调度程序。在任何特定线程上运行 return readResult; 是没有意义的。 avoid context switching 好的,所以指定 ExecuteSynchronously 继续。这在 99% 的时间里都有效。然后,扔掉时髦的 TCS 仿真并使用 TCS。这能回答问题吗?
    • 谢谢,这说明了一点。我仍在寻找一种在回调线程上运行延续的方法,即使这只在 99% 的时间里有效。
    • 使用 TaskContinuationOptions.ExecuteSynchronously。它就是这样做的。这样做有什么问题吗?
    猜你喜欢
    • 2023-03-16
    • 1970-01-01
    • 2016-10-05
    • 2020-10-15
    • 1970-01-01
    • 2020-06-08
    • 1970-01-01
    • 2022-12-07
    • 2021-12-13
    相关资源
    最近更新 更多