【问题标题】:What are the disadvantages of using this asynchronous logging code?使用这种异步日志记录代码有什么缺点?
【发布时间】:2010-07-10 11:44:37
【问题描述】:

我刚刚写的一些代码如下。

它演示了将 PostSharp 方面应用于方法,以便以异步方式记录方法调用的持续时间 - 因此,如果日志记录过程很慢,则装饰方法的调用者不会看到这种性能损失方面。

它似乎可以工作,MyFirstMethod 完成后,日志记录方法在单独的线程中启动,并且 MySecondMethod 并行运行。这个想法是在一个非常高流量的 Web 应用程序(即一个高度多线程的环境)中的方法用类似的工具来装饰。

这样做有什么陷阱? (例如,我担心在任何给定时间达到允许的线程数限制)。

using System;
using System.Threading.Tasks;
using NUnit.Framework;
using PostSharp.Aspects;

namespace Test
{
    [TestFixture]
    public class TestClass
    {        
        [Test]
        public void MyTest()
        {            
            MyFirstMethod();
            MySecondMethod();
        }

        [PerformanceInstrument]
        private void MyFirstMethod()
        {
            //do nothing
        }

        private void MySecondMethod()
        {
            for (int x = 0; x < 9999999; x++);
        }
    }

    [Serializable]
    public class PerformanceInstrument : MethodInterceptionAspect
    {                    
        public override void OnInvoke(MethodInterceptionArgs args)
        {            
            var startDtg = DateTime.Now;
            args.Proceed();
            var duration = DateTime.Now - startDtg;
            Task.Factory.StartNew(() => MyLogger.MyLoggingMethod(duration)); //invoke the logging method asynchronously
        }        
    }

    public static class MyLogger
    {
        public static void MyLoggingMethod(TimeSpan duration)
        {
            for (int x = 0; x < 9999999; x++);
            Console.WriteLine(duration);
        }
    }
}

【问题讨论】:

    标签: c# postsharp task-parallel-library multithreading


    【解决方案1】:

    我在这里看到的唯一可能的缺点是管理任务的开销,我确信这可能微不足道,但我还没有深入研究 TPL 的东西来确定。

    我在大型 Web 应用程序中使用的另一种方法是让日志记录将日志消息记录写入内存列表,然后我有一个后台线程负责在后台写出日志消息.目前,解决方案让线程每隔一段时间检查一次列表,如果列表长度超过某个阈值或列表没有刷新超过特定时间,则将列表刷新到磁盘(在我们的例子中为数据库),这永远是第一位的。

    这类似于 Producer/Consumer 模式,您的代码会生成日志消息,而消费者负责将这些消息刷新到持久性介质。

    【讨论】:

    • 只是为了澄清。在最坏的情况下,应用程序有大量用户,并且记录需要很长时间,据我了解,阻塞的任务数量,等待完成他们的记录将迅速增加。你不相信这会是个问题吗?
    • 我的直觉是,使用我描述的生产者/消费者类型模式对系统的影响比为每个日志条目排队单独的任务要小,但正如我所说,我对 TPL 并不十分熟悉可以肯定的是,它可能会完美地处理这个问题。使用 Produder/Consumer 唯一的附加线程是负责持久化日志消息的后台线程。
    • 克里斯 - 感谢您的见解。请您稍微扩展一下您的生产者/消费者实施。您的内存列表可能必须是线程安全的(或被锁包围)。这似乎排除了异步发生日志记录的优势,因为通过方法调用的进度将阻塞对线程安全集合的访问。我怀疑我对您的实施有些不理解。
    • @Ben,使用 .NET 中的简单列表需要锁定。对于我们的实现,我实现了一个无锁列表。在你走这条路之前,如果我是你,我会确认锁的争用会引入任何可测量的开销。我怀疑锁定争用是否可以衡量。至于消费者方面,使用无锁列表可以很容易地不必锁定列表来删除项目。您还可以使用空列表解锁来锁定和交换,然后消费者将在下一个刷新周期中交换的被动列表排出。您知道网站的请求/秒速率吗?
    • @Ben,顺便说一句。对于 .NET 4,您可以查看提供无锁队列的 ConcurrentQueue (msdn.microsoft.com/en-us/library/dd267265.aspx)。这可能是理想的选择。
    【解决方案2】:

    您的方法可能会产生意想不到的后果,因为 ASP.NET 引擎和任务并行库都是在 .NET 线程池上调度任务。每个 Web 请求都由线程池中的一个线程提供服务。如果您安排任务来处理日志记录,那么您将使用线程池上的任务,这些任务不能再用于服务 Web 请求。这可能会降低吞吐量。

    TPL 团队在此处发布了有关此内容的博客。

    http://blogs.msdn.com/b/pfxteam/archive/2010/02/08/9960003.aspx

    生产者/消费者模式意味着您的 MethodInterceptionAspect 只需将一个条目添加到全局队列中(如 Ben 所建议的),然后您将拥有一个处理所有条目的(长时间运行的)任务。所以你的intercaption方法变成了:

    ConcurrentQueue<TimeSpan> _queue;
    
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        var startDtg = DateTime.Now;
        args.Proceed();
        var duration = DateTime.Now - startDtg;
        Task.Factory.StartNew(() => _queue.Add(duration)); 
    }
    

    您在其他地方处理队列:

    foreach (var d in _queue.GetConsumingEnumerable())
    {
        Console.WriteLine(d);
    }
    

    以下帖子展示了一个类似的实现,其中由 Parallel.For 循环创建的多个任务将图像添加到 BlockingCollection,而单个任务处理图像。

    Parallel Task Library WaitAny Design

    这在某种程度上取决于请求处理的长度、每个请求要处理的日志条目数以及整体服务器负载等。您必须注意的一件事是,总体而言,您需要能够比添加请求更快地从队列中删除请求。

    您是否考虑过编写自己的性能计数器并让性能计数器基础架构为您处理繁重工作的方法?这将使您无需实施任何此类记录基础架构。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-07-03
    • 1970-01-01
    • 2015-07-14
    • 1970-01-01
    • 2017-08-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多