在实现生产者/消费者模式时使用Task.Yield克服ThreadPool饥饿

时间:2018-11-12 13:30:57

标签: c# multithreading async-await task-parallel-library threadpool

回答问题:Task.Yield - real usages? 我建议使用Task.Yield允许池线程由其他任务重用。在这种模式下:

  CancellationTokenSource cts;
  void Start()
  {
        cts = new CancellationTokenSource();

        // run async operation
        var task = Task.Run(() => SomeWork(cts.Token), cts.Token);
        // wait for completion
        // after the completion handle the result/ cancellation/ errors
    }

    async Task<int> SomeWork(CancellationToken cancellationToken)
    {
        int result = 0;

        bool loopAgain = true;
        while (loopAgain)
        {
            // do something ... means a substantial work or a micro batch here - not processing a single byte

            loopAgain = /* check for loop end && */  cancellationToken.IsCancellationRequested;
            if (loopAgain) {
                // reschedule  the task to the threadpool and free this thread for other waiting tasks
                await Task.Yield();
            }
        }
        cancellationToken.ThrowIfCancellationRequested();
        return result;
    }

    void Cancel()
    {
        // request cancelation
        cts.Cancel();
    }

但是一个用户写了

  

我不认为使用Task.Yield来克服ThreadPool饥饿   实施生产者/消费者模式是一个好主意。我建议你   问一个单独的问题,是否要详细说明原因。

任何人都知道,为什么不是一个好主意?

2 个答案:

答案 0 :(得分:1)

在您的问题的评论中还有一些要点。作为您引用的用户,我只想总结一下:为工作使用正确的工具。

使用ThreadPool似乎不是执行多个连续的CPU约束任务的正确工具,即使您尝试通过将协作转化为状态机来组织一些协作执行,这些状态机也会与{ {1}}。线程切换相当昂贵。通过在紧密循环上执行await Task.Yield(),会增加大量开销。此外,您绝不应该接管整个await Task.Yield(),因为.NET框架(和底层OS进程)可能需要它来做其他事情。与此相关的是,TPL甚至具有ThreadPool选项,该选项请求不要在TaskCreationOptions.LongRunning线程上运行任务(相反,它创建了一个普通线程,其中ThreadPool位于幕后)。 / p>

也就是说,在某些专用的,池外线程上使用并行性有限的 custom new Thread()可能对各个长期运行的任务具有线程亲和力 TaskScheduler延续将发布在同一线程上,这将有助于减少切换开销。这使我想起了我之前尝试使用ThreadAffinityTaskScheduler解决的另一个问题。

不过,根据特定的情况,通常最好使用现有的成熟工具和经过测试的工具。仅举几例:Parallel ClassTPL DataflowSystem.Threading.ChannelsReactive Extensions

还有各种现有的行业实力解决方案可以处理发布-订阅模式(RabbitMQ,PubNub,Redis,Azure服务总线,Firebase云消息传递(FCM),Amazon Simple Queue Service(SQS)等)。

答案 1 :(得分:0)

与其他用户就此问题进行了一些辩论之后,他们担心上下文切换及其对性能的影响。 我知道他们在担心什么。

但是我的意思是:在循环内做点什么……是一项艰巨的任务-通常采用消息处理程序的形式,该处理程序从队列中读取消息并进行处理。消息处理程序通常是用户定义的,消息总线使用某种调度程序执行它们。用户可以实现一个同步执行的处理程序(没人知道用户会做什么),而没有Task.Yield的执行程序将阻塞线程以循环处理这些同步任务。

不要空洞,我向github添加了测试:https://github.com/BBGONE/TestThreadAffinity 他们将ThreadAffinityTaskScheduler,具有BlockingCollection的.NET ThreadScheduler和具有Threading.Channels的.NET ThreadScheduler进行了比较。

测试表明,对于超短作业,性能下降为 约15%。在不降低性能(甚至很小)的情况下使用Task.Yield -不要使用非常短的任务,如果任务太短,则将较短的任务组合成更大的批处理。

[上下文切换的价格] = [上下文切换持续时间] /([作业持续时间] + [上下文切换持续时间])

在这种情况下,任务切换对性能的影响可以忽略不计。但是它增加了系统更好的任务协作和响应能力。

对于长时间运行的任务,最好使用自定义的调度程序,该调度程序在自己的专用线程池(例如WorkStealingTaskScheduler)上执行任务。

对于混合作业-可能包含不同的部分-短期运行的CPU绑定,异步和长期运行的代码部分。最好将任务分为子任务。

<input type="text" v-model="keyword" class="form-control" id="telefoon" placeholder="Search...">


<script>
var app = new Vue({
  el: '#app',
  data: {
    keyword: '',
    samsungList: []
  },
  computed: {
    samsungFilteredList() {
      return this.samsungList.filter((samsung) => {
        return samsung.title.toLowerCase().includes(this.keyword.toLowerCase());
      });
    }
  }
});
</script>