3

我正在寫一個遊戲,並且使用OpenGL,我需要將一些工作轉移到OpenGL上下文處於活動狀態的渲染線程中,但其他一切都由普通線程池處理。我怎樣纔能有兩個獨立的任務計劃?

有沒有一種方法可以強制任務在特殊線程池中執行,並且從async創建的任何新任務也將被分派到該線程池?

我想要一些專門用於渲染的線程,我希望能夠使用例如asyncawait來創建和填充頂點緩衝區。

如果我只是用一個自定義的任務調度和new Factory(new MyScheduler())似乎任何後續Task對象將被分派到線程池反正這裏Task.Factory.Scheduler突然是null

下面的代碼應該表現出什麼,我希望能夠做到:

public async Task Initialize() 
{ 
    // The two following tasks should run on the rendering thread pool 
    // They cannot run synchronously because that will cause them to fail. 
    this.VertexBuffer = await CreateVertexBuffer(); 
    this.IndexBuffer = await CreateIndexBuffer(); 

    // This should be dispatched, or run synchrounousyly, on the normal thread pool 
    Vertex[] vertices = CreateVertices(); 
    // Issue task for filling vertex buffer on rendering thread pool 
    var fillVertexBufferTask = FillVertexBufffer(vertices, this.VertexBuffer); 

    // This should be dispatched, or run synchrounousyly, on the normal thread pool 
    short[] indices = CreateIndices(); 

    // Wait for tasks on the rendering thread pool to complete. 
    await FillIndexBuffer(indices, this.IndexBuffer); 
    await fillVertexBufferTask; // Wait for the rendering task to complete. 
} 

有什麼辦法來實現這一點,或者是它的async/await範圍之外?

回答

1

這是可能的,基本上是微軟爲Windows窗體和WPF同步上下文所做的一樣。

第一部分 - 您在OpenGL的線程,並希望把一些工作進入線程池,這項工作完成後,你想回的OpenGL線程。

我認爲你最好的方法是實現你自己的SynchronizationContext。這件事基本上控制了TaskScheduler的工作方式以及它如何調度任務。默認實現只是將任務發送到線程池。你需要做的是將任務發送到一個專用的線程(保存OpenGL上下文)並在那裏逐一執行。

實現的關鍵是覆蓋PostSend方法。預計這兩種方法都會執行回撥,其中Send必須等待呼叫完成,而Post則不需要。使用線程池的示例實現是Send只是直接調用回調,Post將回調委託給線程池。

對於你的OpenGL線程的執行隊列,我想一個線程查詢BlockingCollection應該很好。只需將回調發送到此隊列即可。如果您的post方法是從錯誤的線程調用的,並且您需要等待任務完成,您可能還需要一些回調。

但所有這一切都應該工作。例如,async/await確保SynchronizationContext在線程池中執行的異步調用後恢復。因此,在將某些工作放入另一個線程之後,您應該能夠返回到OpenGL線程。

第二部分 - 您位於另一個線程中,並希望將一些工作發送到OpenGL線程並等待該工作的完成。

這也是可能的。我在這種情況下的想法是,您不使用Task s,而是使用其他等待的對象。一般來說,每個對象可以是等待。它只需要實現一個公共方法getAwaiter(),該方法返回一個實現接口的對象。 await所做的是將剩下的方法放入新的Action中,並將此操作發送到該接口的OnCompleted方法。待命者預計在完成正在等待的操作後調用預定的動作。另外這個服務員必須確保SynchronizationContext被捕獲並且在捕獲的SynchronizationContext上執行延續。這聽起來很複雜,但是一旦你掌握了它,它就會變得相當容易。幫助我很多的是YieldAwaiter的參考資料來源(如果你使用await Task.Yield(),基本上會發生什麼)。這不是你需要的,但我認爲這是一個開始的地方。

返回awaiter的方法必須注意將實際工作發送給必須執行的線程(您可能已經從第一部分開始執行隊列),並且一旦工作完成,awaiter就必須觸發完成。

結論

沒有錯。這是很多工作。但是如果你這樣做了,那麼你的問題就會減少,因爲你可以無縫地使用async/await模式,就好像你將在windows窗體或WPF中工作一樣,這是一種色調加。

+0

這看起來很有希望。有什麼我可以做'async'接受我的自定義圖形任務嗎?我得到一個錯誤,說我必須使用'Task'或'Task ',而C#標準規定它必須是'System.Threading.Tasks.Task'。 – GeirGrusom

+0

不,你不能。但是你可以直接返回你的特殊圖形任務。在那種情況下,你不能在任務中使用'await'。但這不應該要求。請注意,'async'函數總是**在與調用函數相同的同步上下文中運行。 – Nitram

+0

還不錯。我認爲這是我需要的:) – GeirGrusom

1

首先,實現了await在之後引入特殊行爲的方法;也就是說,此代碼:

this.VertexBuffer = await CreateVertexBuffer(); 

幾乎是相同的,因爲這代碼:

var createVertexBufferTask = CreateVertexBuffer(); 
this.VertexBuffer = await createVertexBufferTask; 

所以,你必須明確地調度代碼來將不同的上下文中執行的方法。

您提到使用MyScheduler但我沒有看到您的代碼使用它。像這樣的東西應該工作:

this.factory = new TaskFactory(CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskContinuationOptions.None, new MyScheduler()); 

public async Task Initialize() 
{ 
    // Since you mention OpenGL, I'm assuming this method is called on the UI thread. 

    // Run these methods on the rendering thread pool. 
    this.VertexBuffer = await this.factory.StartNew(() => CreateVertexBuffer()).Unwrap(); 
    this.IndexBuffer = await this.factory.StartNew(() => CreateIndexBuffer()).Unwrap(); 

    // Run these methods on the normal thread pool. 
    Vertex[] vertices = await Task.Run(() => CreateVertices()); 
    var fillVertexBufferTask = Task.Run(() => FillVertexBufffer(vertices, this.VertexBuffer)); 
    short[] indices = await Task.Run(() => CreateIndices()); 
    await Task.Run(() => FillIndexBuffer(indices, this.IndexBuffer)); 

    // Wait for the rendering task to complete. 
    await fillVertexBufferTask; 
} 

我會考慮組合這些多個Task.Run呼叫,或者(如果Initialize被稱爲在正常的線程池線程)完全卸下。

+0

理想情況下,初始化是在普通線程池上運行的。圖形線程只能執行特定於圖形的操作,主要是複製數據,創建或刪除本機對象。我可以說Initialize是在圖形線程上運行的,但是它有點像copout。 我希望這是儘可能無縫。在普通線程池上運行圖形操作將會以靜默方式失敗,因此會導致難以調試的問題。 – GeirGrusom