【问题标题】:System.Timers.Timer Elapsed intermittently not firing when using an Task.Run with async from Console AppSystem.Timers.Timer 使用 Task.Run 与控制台应用程序异步时间歇性地不触发
【发布时间】:2015-10-18 06:18:55
【问题描述】:

我正在使用控制台应用程序,我需要读取 20 个 URI 批次,我发现通过执行所有任务并并行运行它们,然后在不同线程中对完成的结果进行排序,可以大大提高速度(允许获取下一批)。

在我目前使用的调用中,每个线程在获取响应流时都会阻塞,我还看到有一个异步版本的相同方法GetResponseAsync

我了解通过使用异步 Await and Async in same line 而不是阻塞来释放线程池有好处:

异步版本

return Task.Run(async () =>
{
    var uri = item.Links.Alternate();
    var request = (HttpWebRequest)WebRequest.Create(uri);

    var response = await request.GetResponseAsync();
    var stream = response.GetResponseStream();
    if (stream == null) return null;
    var reader = new StreamReader(stream);
    return new FetchItemTaskResult(reader.ReadToEnd(), index, uri);
});

屏蔽版

return Task<FetchItemTaskResult>.Factory.StartNew(() =>
{
    var uri = item.Links.Alternate();
    var request = (HttpWebRequest)WebRequest.Create(uri);

    var response = request.GetResponse();
    var stream = response.GetResponseStream();
    if (stream == null) return null;
    var reader = new StreamReader(stream);
    return new FetchItemTaskResult(reader.ReadToEnd(), index, uri);
});

但是,我在异步版本的控制台应用程序上看到奇怪的暂停,其中 System.Timers.Timer elapsed 事件停止被调用很多秒(当它应该每秒关闭时)。

阻塞处理器每秒运行大约 3,500 个项目,所有内核的 CPU 使用率约为 30%。

async on 每秒运行大约 3,800 个事件,CPU 使用率略高于阻塞,但并不高(只有 5%)......但是我使用的计时器似乎暂停了大约 10 到 15 秒每分钟左右一次,在我的Main() 函数中:

private static void Main(string[] args)
{  
    // snip some code that runs the tasks

    var timer = new System.Timers.Timer(1000);

    timer.Elapsed += (source, e) =>
    {
        Console.WriteLine(DateTime.UtcNow);

        // snip non relevant code
        Console.WriteLine("Commands processed: " + commandsProcessed.Sum(s => s.Value) + " (" + logger.CommandsPerSecond() + " per second)");
    };
    timer.Start();
    Console.ReadKey();
}

因此,在使用异步时,计时器和线程池似乎有些相关(并且只有异步,阻塞时没有暂停),或者可能不相关,无论哪种方式,请知道发生了什么以及如何进一步诊断?

【问题讨论】:

  • "snip non-related code" 嗯...那是什么?也许 System.Timers.Timer 不会并行调用回调,不确定。一个回调调用可能会阻止下一个。删除所有代码并测试。
  • 可能所有线程池线程都忙于运行该部分直到等待或任务完成后。所以你的线程池可能没有一个未占用的线程来运行你的计时器的经过事件
  • 为了体验,尝试将ThreadPool 的大小增加为ThreadPool.SetMinthreads/ThreadPool.SetMaxThreads。有什么改变吗?
  • 我在想,作为 POC,也许用您自己的后台线程替换计时器,该线程卡在一个循环上休眠 1000 并运行您在经过的回调中运行的任何内容,例如 @987654322 @
  • 你会说你的异步块中的代码被调用多少次?

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


【解决方案1】:

System.Timers.Timer 有一个(不是那么)好的特性,可以将Elapsed 事件发布到线程池中。如果所有线程都忙或被阻塞,或者动作队列很长,Elapsed 事件将不会被触发,直到它获得资源来执行它。

即使您使用Timer.SynchronizedObject 属性也是如此 - 请参阅https://docs.microsoft.com/en-us/dotnet/api/system.timers.timer.elapsed?view=net-5.0

System.Threading.Timer 也是如此。看起来System.Windows.Threading.DispatcherTimer 可能会在 UI 线程上执行 - 但您不能在控制台应用程序中使用它。

所以我想唯一的方法是为计时器创建一个专用线程,并在那里使用Thread.Sleep() 进行while(true) 循环。

更新

“专用线程计时器”类看起来大致如此。注意,我是凭记忆写的,所以它可能不会像那样编译。

public sealed class DedicatedTimer
{
  public DedicatedTimer(int interval, Action action)
  {
    mInterval = interval; // Create the field
    mAction = action; // Create the field
    var thr = new Thread(Runner, isBackground); // Check the docs on the background flag
    thr.Start();
  }

  private void Runner()
  {
    while (true)
    {
      Thread.Sleep(mInterval);
      action.Invoke();
    }
  }
}

使用后台线程可以让你忘记它,所以当程序停止时它会被杀死。根据您的情况,您可能需要自己的Dispose

作为给读者的练习,您可以将计时器的创建和启动分开,您可以添加CancellationToken,您可以使用计时器列表并计算每次触发后到下一行的延迟。

请记住,该操作是在此线程上执行的。如果你想避免它,请使用SynchronizationContext

【讨论】:

  • @Mike:非常感谢。有没有这种方法的例子?您在新线程中使用了哪种计时器?我没有。只看到了一个在这种情况下也失败的异步方法,我读到这也是因为它在内部使用了 Threading.Timer。
  • 我会把它添加到答案中。
【解决方案2】:

定时器和线程池有些相关

你的怀疑是正确的。发生的事情基本上被称为臭名昭著的线程饥饿,即你所有的线程都很忙,所以ThreadPool 没有足够的线程来运行事件委托。

一旦在 ASP.NET 中运行,autoConfig="True" 会确保您获得足够的线程(在峰值情况下并非总是如此),并确保您不受连接限制的约束。但在控制台应用程序中,您必须自己执行此操作。

所以只需添加这个 sn-p,我敢打赌你的问题就会消失:

ThreadPool.SetMinThreads(100, 100);
ServicePointManager.DefaultConnectionLimit = 1000;

【讨论】:

  • 在同步情况下一切正常。这不应该是线程饥饿。此外,CPU 没有被最大化,这意味着有未使用的线程(因为他从不阻塞)。
猜你喜欢
  • 1970-01-01
  • 2019-07-20
  • 2016-06-21
  • 1970-01-01
  • 1970-01-01
  • 2010-11-04
  • 1970-01-01
  • 1970-01-01
  • 2013-07-11
相关资源
最近更新 更多