我怎么能有两个独立的任务调度程序

本文关键字:两个 独立 任务调度程序 怎么能 | 更新日期: 2023-09-27 18:14:04

我正在编写一款游戏,并且使用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的范围?

我怎么能有两个独立的任务调度程序

这是可能的,基本上与微软为Windows窗体和WPF同步上下文所做的相同。

第一部分 - 你在OpenGL线程中,想把一些工作放到线程池中,当这些工作完成后,你想回到OpenGL线程中。

我认为最好的方法是实现你自己的SynchronizationContext。这个东西基本上控制TaskScheduler如何工作以及如何安排任务。默认实现只是将任务发送到线程池。你需要做的是将任务发送到一个专用线程(保存OpenGL上下文),并在那里一个接一个地执行它们。

实现的关键是覆盖PostSend方法。这两个方法都应该执行回调,其中Send必须等待调用完成,而Post则不需要。使用线程池的示例实现是Send直接调用回调,Post将回调委托给线程池。

对于您的OpenGL线程的执行队列,我认为查询BlockingCollection的线程应该做得很好。只需将回调发送到这个队列。你可能还需要一些回调,以防你的post方法被错误的线程调用,你需要等待任务完成。

但是所有这些方法都应该有效。例如,async/await确保在线程池中执行异步调用后恢复SynchronizationContext。所以你应该能够在你把一些工作放到另一个线程后返回到OpenGL线程。

第二部分 - 你在另一个线程中,想要发送一些工作到OpenGL线程并等待该工作完成。

这也是可能的。在这种情况下,我的想法是不使用Task s,而是使用其他可等待对象。一般来说,每个对象都可以是可等待的。它只需要实现一个公共方法getAwaiter(),该方法返回一个实现INotifyCompletion接口的对象。await所做的是将剩余的方法放入新的Action中,并将此操作发送给该接口的OnCompleted方法。一旦等待的操作完成,等待程序就会调用预定的操作。此外,该侍者还必须确保捕获SynchronizationContext,并在捕获的SynchronizationContext上执行延续。这听起来很复杂,但一旦你掌握了窍门,事情就变得相当简单了。对我帮助很大的是YieldAwaiter的参考源(这基本上是如果你使用await Task.Yield()会发生的事情)。这不是你需要的,但我认为这是一个开始的地方。

返回awaiter的方法必须负责将实际工作发送到必须执行它的线程(您可能已经从第一部分获得了执行队列),并且一旦工作完成,awaiter必须触发。

结论

别搞错了。这是大量的工作。但是如果你做了所有这些,你会有更少的问题,因为你可以无缝地使用async/await模式,就像你将在windows窗体或WPF中工作一样,这是一个色调加。

首先,意识到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是在一个正常的线程池线程上调用的)完全删除它们。