【问题标题】:Parallel.ForEach Pretty Logging Impossible?Parallel.ForEach 不可能进行漂亮的日志记录?
【发布时间】:2021-10-02 22:40:08
【问题描述】:

我认为使用 Parallel.ForEach 时不可能有漂亮的日志。除非有人可以向我展示一些技巧吗?

namespace Parallel_ForEach_Logging
{
    class Program
    {
        private static uint _callCount = 0;

        static void Main(string[] args)
        {
            IEnumerable<int> data = Enumerable.Range(0, 2);

            Console.Out.WriteLine("***");

            ParallelOptions options = new();
            options.MaxDegreeOfParallelism = Environment.ProcessorCount;

            Parallel.ForEach(data, options, datum =>
            {
                PrintNum(datum, _callCount++);
            });

            Console.Out.Flush();
            Console.Out.WriteLine("***");
        }

        public static void PrintNum(int num, uint callCount)
        {
            Console.Out.WriteLine($">>> IN {callCount}");
            // Console.Out.WriteLine($"  {num}");
            Console.Out.WriteLine($"<<< OUT {callCount}");
            PrintString($"\"{num.ToString()}\"", callCount);
        }

        public static void PrintString(string str, uint callCount)
        {
            Console.Out.WriteLine($">>> IN {callCount}");
            // Console.Out.WriteLine($"  {str}");
            Console.Out.WriteLine($"<<< OUT {callCount}");
        }
    }
}

当我说漂亮的日志时,我想到的是:

>>> IN: Method A
  >>> IN : Method 1 called.
  <<< OUT: Method 1 done.
  >>> IN : Method 2 called.
  <<< OUT: Method 2 done.
<<< OUT: Method A done in 32 milliseconds.
>>> IN: Method B
...

这是我目前正在获取的示例日志:

>>> IN 1
>>> IN 0
<<< OUT 0
<<< OUT 1
>>> IN 0
<<< OUT 0
>>> IN 1
<<< OUT 1

注意顶部有两个 IN 背靠背,然后两个 OUT 背靠背。

在代码中,_callCount 是我尝试计算用于缩进的调用深度,但我不知道如何进行这项工作。即使我这样做了,我也不确定如果每个日志条目中没有一百万个空格,我是否能做到。

我知道如果我将 Parallel.ForEach 排除在等式之外,我肯定可以让它工作。

如果这真的不可能完成,我没关系。我只需要一个比我聪明的人这么说。 ;)

【问题讨论】:

  • 如果您考虑一下,您创建的问题是您的代码是并行的,但日志接收器是串行的。如果你想美化它,日志也需要是并行的,即它可能需要多列,而不是尝试对行进行排序(行实际上是从过去到未来的时间流,所以要强制那些你必须删除并行性)..如果您要将输出转换为表格,控制台不是一个很好的选择(可以这样做,但很头疼)

标签: c# logging parallel.foreach


【解决方案1】:

使用 StringBuilder 作为缓冲区。将其作为参数添加到您的打印函数中,如下所示:

public static void PrintNum(StringBuilder builder, int num, uint callCount)
{
    builder.Append($">>> IN {callCount}\r\n");
    builder.Append($"<<< OUT {callCount}\r\n");
    PrintString(builder, $"\"{num.ToString()}\"", callCount);
}

public static void PrintString(StringBuilder builder, string str, uint callCount)
{
    builder.Append($">>> IN {callCount}\r\n");
    builder.Append($"<<< OUT {callCount}\r\n");
}

现在你可以这样做了:

Parallel.ForEach(data, options, datum =>
{
    var builder = new StringBuilder();
    PrintNum(builder, datum, _callCount++);
    Console.Out.WriteLine(builder.ToString());
});

【讨论】:

  • 我想我可以用这个。我将把它标记为已接受的答案。谢谢。
【解决方案2】:

您可以锁定对 PrintString 的调用。请记住,这样做会暂停其他线程,直到当前线程释放锁。因此,在这种情况下,您的代码实际上是连续的,但线程打印的顺序实际上是随机的。

【讨论】:

    【解决方案3】:

    除了锁定线程之外,我能想到的唯一其他方法是使用包含调用计数和元组或类中的方法名称的ConcurrentBag。然后在 ForEach 完成后,您可以使用调用计数和方法名称对该集合进行排序,以便它以顺序方式显示。

    这可能是作弊,因为它不是实时的,但除此之外,我不知道你是如何做到这一点的。

    【讨论】:

      【解决方案4】:

      使用PLinq你可以实现:

      var query = data.AsParallel()
          .WithDegreeOfParallelism(Environment.ProcessorCount)
          .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
          .Select(datum => Task.Run(() => PrintNum(datum, _callCount++)));
      
      await Task.WhenAll(query);
      

      以下输出:

      ***
      >>> IN 0
      <<< OUT 0
      >>> IN 1
      <<< OUT 1
      >>> IN 0
      <<< OUT 0
      >>> IN 1
      <<< OUT 1
      ***
      

      【讨论】:

      • 我试过这个,输出也有类似的问题。只是不同的皮肤有同样的问题吗?
      • @jas 我检查了应用程序,似乎只要您使用 .NET Core 3.x 就存在问题。当您使用 .NET Core 5.x 时,它将打印我刚刚发布的输出。
      猜你喜欢
      • 2022-01-08
      • 1970-01-01
      • 2011-10-21
      • 2019-07-16
      • 2011-01-02
      • 1970-01-01
      • 2021-02-27
      • 1970-01-01
      • 2014-12-24
      相关资源
      最近更新 更多