WPF -连续显示大量日志消息

mi7gmzs6  于 2023-06-24  发布在  其他
关注(0)|答案(1)|浏览(234)

我正在使用WPF开发应用程序。我使用Serilog进行日志记录,我有一个日志接收器,它在列表框中显示日志事件。传入的日志事件在日志接收器的Emit方法中排队,排队的日志事件在CompositionTarget.Rendering事件中处理。
排队日志事件:

public void EventsLogged(IEnumerable<Event> Events)
        {
            System.Windows.Application.Current?.Dispatcher.Invoke((Action)delegate
                {
                    foreach (var item in Events)
                    {
                        _logMessageQueue.Enqueue(new LogEvent(item));
                    }
                });
        }

渲染事件:

private void CompositionTargetOnRendering(object sender, EventArgs e)
        {
            while (_logMessageQueue.TryDequeue(out var logEvent))
                {
                    Messages.Add(logEvent);
                }          
        }

Messages是绑定到列表框的ItemSource的ObservableCollection
这是我的问题,当在短时间内有大量的排队日志事件时,UI响应变得可怕。有什么建议可以改进我的申请吗?或者任何我能遵循的学习材料都会很好。
谢谢你

ajsxfq5m

ajsxfq5m1#

以下是一些可以提高性能的建议:

    • 如果 * 必须使用Dispatcher来收集日志事件,则应使用具有适当优先级的Dispatcher.InvokeAsync。请注意,您应该避免在Dispatcher中充满任务。如果您希望以高频调用Dispatcher,请考虑使用不同的方法(例如背景线程):
public void EventsLogged(IEnumerable<Event> events)
{
  // Don't block
  Application.Current.Dispatcher.InvokeAsync(
    () =>
    {
      foreach (var item in events)
      {
        _logMessageQueue.Enqueue(new LogEvent(item));
      }
    }, DispatcherPriority.Background);
}
  • 考虑避免Dispatcher。除非LogEvent扩展DispatcherObject,否则您可以在后台线程或异步执行数据结构转换(取决于您的需求):
// In case this member is an event handler, define the method of type 'void' (instead of 'Task')
public async Task EventsLoggedAsync(IEnumerable<Event> events)
{
  // Don't block
  await Task.Run(
    () =>
    {
      foreach (var item in events)
      {
        _logMessageQueue.Enqueue(new LogEvent(item));
      }
    });
}
  • 改善您的消费者部分:while循环是阻塞操作。当Dispatcher空闲时,可以通过使用具有显式优先级的Dispatcher来执行代码来防止阻塞:
private void CompositionTargetOnRendering(object sender, EventArgs e)
{
  while (_logMessageQueue.TryDequeue(out var logEvent))
  {
    // Enqueue task to the Dispatcher queue 
    // and let the Dispatcher decide when to process the tasks by defining a priority
    Application.Current.Dispatcher.InvokeAsync(
      () =>
      {
        Messages.Add(logEvent);
      }, DispatcherPriority.Background);
  }
}

为了进一步改进解决方案,我建议实现 Producer/Consumer 模式。如果使用Channel,则可以使整个实现异步(请参阅Microsoft文档:System.Threading.Channels library)。
为了提高效率,此示例还通过处理CompositionTarget.Rendering事件来避免从队列中阅读。相反,该示例将在后台线程上启动一个循环以使用队列。
Dispatcher.InvokeAsync用于控制Dispatcher上的压力:使用DispatcherPriority.BackgroundDispatcherPriority.ContextIdle应减轻压力,以便主螺纹可以继续渲染表面。
最终的解决方案如下:

class ChannelExample
{
  private Channel<LogEvent> LogEventChannel { get; }

  public ChannelExample()
  {
    var channelOptions = new UnboundedChannelOptions
    {
      SingleReader = true,
      SingleWriter = false,
    };

    this.LogEventChannel = Channel.CreateUnbounded<LogEvent>(channelOptions);

    // Start the consumer loop in the background
    Task.Run(DisplayQueuedLogEventsAsync);
  }

  // Optional: allows to stop the producer/consumer process 
  // and closes the queue for additional writes.
  // Because this is an application logger, this method is very likely redundant for your scenario
  private void CloseLogEventQueueForWrites()
    => this.LogEventChannel.Writer.Complete();

  // Thread-safe implementation allows for concurrency
  public async Task EventsLoggedAsync(IEnumerable<Event> events)
  {
    await Task.Run(
      async () =>
      {
        ChannelWriter<int> logEventChannelWriter = this.LogEventChannel.Writer;

        // Consider to use Parallel.ForEach (must be tested because it is not guaranteed that it will improve the performance)
        foreach (Event event in events)
        {
          var logMessage = new LogEvent(event);

          while (await logEventChannelWriter.WaitToWriteAsync())
          {
            if (valueChannelWriter.TryWrite(logMessage))
            {
              // TODO:: Optionally do something after an item 
              // was successfully written to the channel (e.g. increment counter)
            }
          }
        }
      });
  }

  private async Task DisplayQueuedLogEventsAsync()
  {
    ChannelReader<LogEvent> logEventChannelReader = this.LogEventChannel.Reader;

    // Asynchronous iteration will continue until the Channel is closed (by calling ChannelWriter.Complete()).
    await foreach (LogEvent logEvent in logEventChannelReader.ReadAllAsync())
    {
      // Use priority Background or ContextIdle 
      // to control and relief the pressure on the Dispatcher
      Application.Current.Dispatcher.InvokeAsync(
        () =>
        {
          Messages.Add(logEvent);
        }, DispatcherPriority.Background);
    }
  }
}

相关问题