3

I have a C function FsReadStream that does some asynchronous work and takes a callback. When done, it calls the callback using the QueueUserWorkItem windows function.

I am trying to call this function from managed code (c#) using the async/await pattern. So I do the following

  1. Construct a Task object passing the constructor a lambda that returns the result.
  2. Construct a callback that runs this task using the RunSynchronously method
  3. Call the asynchronous native function, passing in the callback
  4. Return the task object to the caller

My code looks something like this

/// 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;
}

and I call it like this

ReadResult readResult = await ReadAsync(data);

I have two questions

  1. How to make the code that runs after the call to await ReadAsync run on the same thread as the callback? Currently, I see it runs on a different thread even though I am calling readCompletionTask.RunSynchronously. I am running this code under ASP.NET and IIS.
  2. Does the native QueueUserWorkItem function use the same threadpool as the managed ThreadPool.QueueUserWorkItem method? My opinion was that it should, and therefore it should be possible for the managed TaskScheduler to schedule tasks on the native callback thread.
tcb
  • 4,408
  • 5
  • 34
  • 51

2 Answers2

3

You shouldn't use the Task constructor in modern code. At all. Ever. There is no use case for it.

In this case, you should use TaskCompletionSource<T>.

How to make the code that runs after the call to await ReadAsync run on the same thread as the callback?

You can't guarantee it; await just doesn't work that way. If the code absolutely must execute on the same thread, then it should be called directly from the callback.

However, if it's just preferred to execute on the same thread, then you don't have to do anything special; await already uses the ExecuteSynchronously flag:

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;
}

Does the native QueueUserWorkItem function use the same threadpool as the managed ThreadPool.QueueUserWorkItem method?

No. Those are two completely different thread pools.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Interesting. Is there an msdn that discourages the use of the `Task` ctor. You are essentially claiming both the `Task` ctor and `Task.RunSynchronously` methods are deprecated. – tcb Oct 26 '15 at 21:50
  • @tcb: No. MSDN (and most Microsoft documentation) is descriptive, not prescriptive. I summarize my arguments on my blog about [why the `Task` constructor is useless](http://blog.stephencleary.com/2014/05/a-tour-of-task-part-1-constructors.html) and [why `RunSynchronously` is useless](http://blog.stephencleary.com/2015/02/a-tour-of-task-part-8-starting.html). – Stephen Cleary Oct 26 '15 at 22:35
  • 2
    Worth noting though that under ASP.NET and IIS (which @tcb specified as runtime environment) `await ReadAsync(data)` won't continue on the same thread, unless it's `await ReadAsync(data).ConfigureAwait(false)`. – noseratio Oct 26 '15 at 22:50
  • @Noseratio: I believe it will continue on the same thread. I haven't tested it, though. – Stephen Cleary Oct 26 '15 at 22:54
  • 1
    @StephenCleary, it won't unfortunately, at least not in the current implementation of `AspNetSynchronizationContext`: http://stackoverflow.com/q/23062154/1768303. I still don't know why they took that route. – noseratio Oct 26 '15 at 22:57
2

How to make the code that runs after the call to await ReadAsync run on the same thread as the callback?

This is not possible in a reliable way. ExecuteSynchronously is not a guarantee. RunSynchronously does not guarantee it either. You can of course pass in a callback and call that callback synchronously.

Also, what does FromCurrentSynchronizationContext return? My spider sense is telling me that this is based on a misunderstanding...

Does the native QueueUserWorkItem function use the same threadpool as the managed ThreadPool.QueueUserWorkItem method?

I don't think so and even if this was the case you could not target a particular thread. You only could target a particular pool.

Why do you need to execute on the same thread? Usually, people who ask about that really do want and need something else.


Your way to create and return a task is very odd. Why are you not using the standard pattern based on TaskCompletionSource?


I think you have a GC hole because nothing keeps asyncInfo.Callback alive. It can be collected away while the native call is in progress. Use GC.KeepAlive in the callback.

usr
  • 168,620
  • 35
  • 240
  • 369
  • I used the `Task` ctor instead of `TaskCompletionSource` because that allowed me to specify the scheduler when running the task. The scheduler is of type `System.Threading.Tasks.SynchronizationContextTaskScheduler`. Thanks for pointing out the GC hole. – tcb Oct 26 '15 at 20:30
  • Trying to execute the code after await on the callback thread so as to avoid context switching between threads – tcb Oct 26 '15 at 20:33
  • You can specify the scheduler for the task but not for the continuations. There is no point in running `return readResult;` on any particular thread.; `avoid context switching` OK, so specify `ExecuteSynchronously` for the continuations. This works 99% of the time. Then, throw out the funky TCS emulation and use TCS. Does this answer the question? – usr Oct 26 '15 at 20:36
  • Thanks, that clarifies a bit. I am still looking for a way to run the continuation on the callback thread, even if that works only 99% of the time. – tcb Oct 26 '15 at 20:54
  • Use TaskContinuationOptions.ExecuteSynchronously. It does just that. Any problems doing that? – usr Oct 26 '15 at 21:11