2015-10-26 46 views
1

我有一個C函數FsReadStream,它執行一些異步工作並進行回調。完成後,它使用QueueUserWorkItem窗口函數調用回調。在本機函數回調線程上運行異步任務延續

我想從託管代碼(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應該可以在本地回調線程上安排任務。

回答

2

如何使調用後運行的代碼等待ReadAsync在與回調相同的線程上運行?

這是不可能的一種可靠的方式。 ExecuteSynchronously不是保證。 RunSynchronously也不能保證。您當然可以傳入回調並同步調用該回調。

另外,FromCurrentSynchronizationContext返回什麼?我的蜘蛛感覺告訴我,這是基於一種誤解......

是否本地QueueUserWorkItem功能使用相同的線程池的管理ThreadPool.QueueUserWorkItem方法?

我不這麼認爲,即使是這種情況,你不能針對特定的線程。你只能針對特定的游泳池。

爲什麼你需要在同一個線程上執行?通常,詢問這些人的人確實需要其他東西。


您的創建和返回任務的方式非常奇怪。爲什麼你不使用基於TaskCompletionSource的標準模式?


我認爲你有一個GC洞,因爲沒有任何東西讓asyncInfo.Callback活着。當本地電話正在進行時,它可以被收集起來。在回調中使用GC.KeepAlive

+0

我使用'Task' ctor而不是'TaskCompletionSource',因爲這允許我在運行任務時指定調度器。調度程序的類型爲「System.Threading.Tasks.SynchronizationContextTaskScheduler」。感謝您指出GC孔。 – tcb

+0

嘗試在回調線程上等待後執行代碼,以避免線程之間的上下文切換 – tcb

+0

您可以爲任務指定調度程序,但不爲延續指定調度程序。在任何特定的線程上運行'return readResult;'沒有意義。 '避免上下文切換'好吧,所以爲continuations指定'ExecuteSynchronously'。這工作99%的時間。然後,扔掉時髦的TCS仿真並使用TCS。這是否回答這個問題? – usr

2

您不應該在現代代碼中使用Task構造函數。完全一樣。永遠。沒有用例。

在這種情況下,您應該使用TaskCompletionSource<T>

如何使調用後運行的代碼等待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方法?

不是。那些是兩個完全不同的線程池。

+0

有趣。是否有一個MSDN不鼓勵使用「任務」控制器。你基本上聲稱'Task' ctor和'Task.RunSynchronously'方法都被棄用了。 – tcb

+0

@tcb:不。MSDN(和大多數Microsoft文檔)是描述性的,而不是說明性的。我在我的博客中總結了我的觀點,關於[爲什麼'Task'構造函數沒用](http://blog.stephencleary.com/2014/05/a-tour-of-task-part-1-constructors.html)和[爲什麼'RunSynchronously'沒用](http://blog.stephencleary.com/2015/02/a-tour-of-task-part-8-starting.html)。 –

+2

值得注意的是,在ASP.NET和IIS(@tcb指定爲運行時環境)下,'await ReadAsync(data)'不會在同一個線程上繼續,除非它'等待ReadAsync(data).ConfigureAwait(false) 。 – Noseratio