System.Timers.Timer在使用Task时间歇性地经过而不触发.从控制台应用程序异步运行

本文关键字:控制台 运行 异步 应用程序 经过 Timer Timers Task 时间 System | 更新日期: 2023-09-27 18:04:53

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

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

我理解通过在同一行使用async Await和async而不是阻塞来释放线程池的好处:

<<p> 异步版本/strong>
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);
});
<<p> 阻塞版本/strong>
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经过的事件停止被调用了许多秒(当它应该每秒关闭)。

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

异步运行大约每秒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();
}
所以似乎定时器和线程池在使用异步(并且只有异步,阻塞时没有暂停)时有些相关,或者可能没有,无论哪种方式,任何想法是什么,请和如何进一步诊断?

System.Timers.Timer在使用Task时间歇性地经过而不触发.从控制台应用程序异步运行

定时器和线程池在某种程度上是相关的

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

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

所以只要添加这段代码,我打赌你的问题就会解决:

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

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

即使使用Timer.SynchronizedObject属性也是如此-参见https://learn.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 .