GeirGrusom
GeirGrusom

Reputation: 1019

How can I have two separate task schedulers?

I am writing a game, and using OpenGL I require that some work be offloaded to the rendering thread where an OpenGL context is active, but everything else is handled by the normal thread pool.

Is there a way I can force a Task to be executed in a special thread-pool, and any new tasks created from an async also be dispatched to that thread pool?

I want a few specialized threads for rendering, and I would like to be able to use async and await for example for creating and filling a vertex buffer.

If I just use a custom task scheduler and a new Factory(new MyScheduler()) it seems that any subsequent Task objects will be dispatched to the thread pool anyway where Task.Factory.Scheduler suddenly is null.

The following code should show what I want to be able to do:

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

Is there any way to achieve this, or is it outside the scope of async/await?

Upvotes: 3

Views: 404

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456417

First, realize that await introduces the special behavior after the method is called; that is to say, this code:

this.VertexBuffer = await CreateVertexBuffer();

is pretty much the same as this code:

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

So, you'll have to explicitly schedule code to execute a method within a different context.

You mention using a MyScheduler but I don't see your code using it. Something like this should work:

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

I would look into combining those multiple Task.Run calls, or (if Initialize is called on a normal thread pool thread) removing them completely.

Upvotes: 1

Nitram
Nitram

Reputation: 6716

This is possible and basically the same thing what Microsoft did for the Windows Forms and WPF Synchronization Context.

First Part - You are in the OpenGL thread, and want to put some work into the thread pool, and after this work is done you want back into the OpenGL thread.

I think the best way for you to go about this is to implement your own SynchronizationContext. This thing basically controls how the TaskScheduler works and how it schedules the task. The default implementation simply sends the tasks to the thread pool. What you need to do is to send the task to a dedicated thread (that holds the OpenGL context) and execute them one by one there.

The key of the implementation is to overwrite the Post and the Send methods. Both methods are expected to execute the callback, where Send has to wait for the call to finish and Post does not. The example implementation using the thread pool is that Sendsimply directly calls the callback and Post delegates the callback to the thread pool.

For the execution queue for your OpenGL thread I am think a Thread that queries a BlockingCollection should do nicely. Just send the callbacks to this queue. You may also need some callback in case your post method is called from the wrong thread and you need to wait for the task to finish.

But all in all this way should work. async/await ensures that the SynchronizationContext is restored after a async call that is executed in the thread pool for example. So you should be able to return to the OpenGL thread after you did put some work off into another thread.

Second Part - You are in another thread and want to send some work into the OpenGL thread and await the completion of that work.

This is possible too. My idea in this case is that you don't use Tasks but other awaitable objects. In general every object can be awaitable. It just has to implement a public method getAwaiter() that returns a object implementing the INotifyCompletion interface. What await does is that it puts the remaining method into a new Action and sends this action to the OnCompleted method of that interface. The awaiter is expected to call the scheduled actions once the operation it is awaiting is done. Also this awaiter has to ensure that the SynchronizationContext is captured and the continuations are executed on the captured SynchronizationContext. That sounds complicated, but once you get the hang of it, it goes fairly easy. What helped me a lot is the reference source of the YieldAwaiter (this is basically what happens if you use await Task.Yield()). This is not what you need, but I think it is a place to start.

The method that returns the awaiter has to take care of sending the actual work to the thread that has to execute it (you maybe already have the execution queue from the first part) and the awaiter has to trigger once that work is done.

Conclusion

Make no mistake. That is a lot of work. But if you do all that you will have less problem down the line because you can seamless use the async/await pattern as if you would be working inside windows forms or WPF and that is a hue plus.

Upvotes: 2

Related Questions