Task.Delay是否开始一个新线程?

Kr0*_*r0e 10 c# mono multithreading async-await .net-4.5

下面的代码应该(至少在我看来)创建100 Tasks,它们都在并行等待(这是关于并发的点,右边:D?)并且几乎同时完成.我想,每Task.Delay一个Timer对象在内部创建.

public static async Task MainAsync() {

    var tasks = new List<Task>();
    for (var i = 0; i < 100; i++) {
        Func<Task> func = async () => {
            await Task.Delay(1000);
            Console.WriteLine("Instant");
        };
        tasks.Add(func());
    }
    await Task.WhenAll(tasks);
}

public static void Main(string[] args) {
    MainAsync().Wait();
}
Run Code Online (Sandbox Code Playgroud)

但!当我在Mono上运行时,我会得到非常奇怪的行为:

  • Tasks不会在同一时间完成,存在着巨大的延迟(大概在500-600ms)
  • 在控制台单声道显示了很多创建的线程:

加载程序集:/Users/xxxxx/Programming/xxxxx/xxxxxxxxxx/bin/Release/xxxxx.exe

线程开始:#2

线程开始:#3

线程开始:#4

线程开始:#5

线程开始:#6

线程开始:#7

线程完成:#3 < - 显然延迟1000ms完成了?

线程完成:#2 < - 显然延迟1000ms完成了?

线程开始:#8

线程开始:#9

线程开始:#10

线程开始:#11

线程开始:#12

线程开始:#13

... 你懂了.

这实际上是一个错误吗?或者我使用错误的库?

[编辑]我使用Timer测试了一个自定义睡眠方法:

    public static async Task MainAsync() {
        Console.WriteLine("Started");
        var tasks = new List<Task>();
        for (var i = 0; i < 100; i++) {
            Func<Task> func = async () => {
                await SleepFast(1000);
                Console.WriteLine("Instant");
            };
            tasks.Add(func());
        }
        await Task.WhenAll(tasks);
        Console.WriteLine("Ready");
    }

    public static Task SleepFast(int amount) {
        var source = new TaskCompletionSource<object>();
        new Timer(state => {
            var oldSrc = (TaskCompletionSource<object>)state;
            oldSrc.SetResult(null);
        }, source, amount, 0);
        return source.Task;
    }
Run Code Online (Sandbox Code Playgroud)

这一次,所有任务都是即时完成的.所以,我认为这是一个非常糟糕的实现或错误.

[Edit2]仅供参考:我Task.Delay现在使用Windows 8.1在.NET上测试了原始代码(使用),它按预期运行(1000 Tasks,并行等待1秒并完成).

所以答案是:Mono的impl.(某些)方法并不完美.一般来说Task.Delay,不启动线程,甚至很多线程都不应该创建多个线程.

Dzm*_*oda 5

在 .NET Framework 桌面上。

简而言之,有一个特殊的 VM 线程,它定期检查计时器队列并在线程池队列上运行计时器的委托。Task.Delay 不会创建新线程,但仍然可能很重,并且不能保证执行顺序或精确的截止日期。据我了解,传递取消Task.Delay可能最终只是从集合中删除项目,而没有线程池工作排队。

通过创建新的System.Threading.Timer将Task.Delay安排为DelayPromise。所有计时器都存储在 AppDomain 的TimerQueue单例中。本机 VM 计时器用于回调 .NET 以检查是否需要从队列中触发任何计时器。计时器委托计划通过ThreadPool.UnsafeQueueUserWorkItem执行 。

从性能角度来看,如果延迟提前结束,取消延迟似乎更好:

open System.Threading
open System.Threading.Tasks

// takes 0.8% CPU
while true do
  Thread.Sleep(10)
  Task.Delay(50)

// takes 0.4% CPU
let mutable a = new CancellationTokenSource()
while true do
  Thread.Sleep(10)
  a.Cancel()
  a.Dispose()
  a <- new CancellationTokenSource()
  let token = a.Token
  Task.Delay(50,token)
Run Code Online (Sandbox Code Playgroud)


Iai*_*ard 4

Task库的设计更多是为了管理阻塞任务而不阻塞整个工作流程(任务异步,微软混淆地称为“任务并行”),而不是为了进行大块并发计算(并行执行)。

任务库使用调度程序并对准备执行的作业进行排队。当作业运行时,它们将在线程池线程上执行,并且这些线程的数量非常有限。有逻辑可以扩展线程数,但除非您有数百个 CPU 核心,否则它将保持较低的数字。

因此,为了回答这个问题,您的一些任务正在排队等待池中的线程,而其他延迟任务已由调度程序发出。

调度程序和线程池逻辑可以在运行时更改,但如果您试图快速完成大量计算,Task则不适合这项工作。如果您想处理大量缓慢的资源(例如磁盘、数据库或互联网资源),Task可能有助于保持应用程序的响应能力。

如果您只是想了解Task尝试这些:

  • 更像是您可以使用任务库来确保您的服务器不会被 IO 阻塞。任务不会解决性能问题,但它是一个有用的工具。它们更像是“绿色线程”或 Erlang 线程——它们不会让你的硬件运行得更快,但它们可以帮助更好地利用可用的电源。一如既往的优化,*首先测量* (5认同)