多线程应用程序与记录器线程的交互

时间:2009-10-23 14:10:15

标签: c# .net multithreading logging concurrent-programming

在这里,我再次提出有关多线程和我的并发编程类的练习的问题。

我有一个多线程服务器 - 使用.NET 异步编程模型实现 - 使用GET download )和PUT(< em>上传)文件服务。这部分已经完成并经过测试。

问题的陈述恰好说这个服务器必须具有 logging 活动,对服务器响应时间的影响最小,并且它应该由低优先级线程支持 - 记录器线程 - 为此效果创建。所有日志消息都应由线程传递给 logger线程,使用可能不会的通信机制strong>锁定调用它的线程(除了必要的锁定以确保互斥)并假设某些日志消息可能被忽略。

以下是我目前的解决方案,请帮助验证这是否是所述问题的解决方案:

using System;
using System.IO;
using System.Threading;

// Multi-threaded Logger
public class Logger {
    // textwriter to use as logging output
    protected readonly TextWriter _output;
    // logger thread
    protected Thread _loggerThread;
    // logger thread wait timeout
    protected int _timeOut = 500; //500ms
    // amount of log requests attended
    protected volatile int reqNr = 0;
    // logging queue
    protected readonly object[] _queue;
    protected struct LogObj {
        public DateTime _start;
        public string _msg;
        public LogObj(string msg) {
            _start = DateTime.Now;
            _msg = msg;
        }
        public LogObj(DateTime start, string msg) {
            _start = start;
            _msg = msg;
        }
        public override string ToString() {
            return String.Format("{0}: {1}", _start, _msg);
        }
    }

    public Logger(int dimension,TextWriter output) {
        /// initialize queue with parameterized dimension
        this._queue = new object[dimension];
        // initialize logging output
        this._output = output;
        // initialize logger thread
        Start();
    }
    public Logger() {
        // initialize queue with 10 positions
        this._queue = new object[10];
        // initialize logging output to use console output
        this._output = Console.Out;
        // initialize logger thread
        Start();
    }

    public void Log(string msg) {
        lock (this) {
            for (int i = 0; i < _queue.Length; i++) {
                // seek for the first available position on queue
                if (_queue[i] == null) {
                    // insert pending log into queue position
                    _queue[i] = new LogObj(DateTime.Now, msg);
                    // notify logger thread for a pending log on the queue
                    Monitor.Pulse(this);
                    break;
                }
                // if there aren't any available positions on logging queue, this
                // log is not considered and the thread returns
            }
        }
    }

    public void GetLog() {
        lock (this) {
            while(true) {
                for (int i = 0; i < _queue.Length; i++) {
                    // seek all occupied positions on queue (those who have logs)
                    if (_queue[i] != null) {
                        // log
                        LogObj obj = (LogObj)_queue[i];
                        // makes this position available
                        _queue[i] = null;
                        // print log into output stream
                        _output.WriteLine(String.Format("[Thread #{0} | {1}ms] {2}",
                                                        Thread.CurrentThread.ManagedThreadId,
                                                        DateTime.Now.Subtract(obj._start).TotalMilliseconds,
                                                        obj.ToString()));
                    }
                }
                // after printing all pending log's (or if there aren't any pending log's),
                // the thread waits until another log arrives
                //Monitor.Wait(this, _timeOut);
                Monitor.Wait(this);
            }
        }
    }

    // Starts logger thread activity
    public void Start() {
        // Create the thread object, passing in the Logger.Start method
        // via a ThreadStart delegate. This does not start the thread.
        _loggerThread = new Thread(this.GetLog);
        _loggerThread.Priority = ThreadPriority.Lowest;
        _loggerThread.Start();
    }

    // Stops logger thread activity
    public void Stop() {
        _loggerThread.Abort();
        _loggerThread = null;
    }

    // Increments number of attended log requests
    public void IncReq() { reqNr++; }

}

基本上,以下是此代码的要点:

  1. 启动低优先级线程,循环日志记录队列并将待处理日志打印到输出。在此之后,线程将暂停,直到新的 log 到达;
  2. 当日志到达时,记录器线程被唤醒并且它正常工作。
  3. 此解决方案是线程安全吗?我一直在阅读生产者 - 消费者问题和解决方案算法,但在这个问题上虽然我有多个生产者,但我只有一个读者。

    提前感谢您的所有关注。

4 个答案:

答案 0 :(得分:4)

它似乎应该有效。生产者 - 消费者在单一消费者的情况下不应该有很大的改变。小挑剔:

  • 获得锁定可能是一项昂贵的操作(正如@Vitaliy Lipchinsky所说)。我建议使用互锁操作对您的记录器进行基准测试,以对照天真的“直写”记录器和记录器。另一种方法是在GetLog中将现有队列与空队列交换,并立即离开临界区。这样,没有一个生产者不会被消费者的长期操作所阻止。

  • 制作LogObj引用类型(类)。因为你无论如何都要装箱,所以制作它是没有意义的。或者让_queue字段为LogObj[]类型(无论如何都要好)。

  • 设置您的主题背景,以便在不调用Stop的情况下不会阻止关闭您的程序。

  • 冲洗TextWriter。或者你甚至冒着失去那些设法排队的记录的风险(10项有点小恕我直言)

  • 实施IDisposable和/或终结器。你的记录器拥有线程和文本编写器,那些应该被释放(并刷新 - 见上文)。

答案 1 :(得分:3)

嘿那里。快速浏览一下,虽然看起来是线程安全的,但我认为它不是特别优化。我建议按照这些方式提出解决方案

注意:只需阅读其他回复。以下是基于您自己的相当优化,乐观的锁定解决方案。主要区别在于锁定内部类,最小化“关键部分”,并提供优雅的线程终止。如果你想完全避免锁定,那么你可以尝试一些不稳定的“非锁定”链接列表,正如@Vitaliy Lipchinsky建议的那样。

using System.Collections.Generic;
using System.Linq;
using System.Threading;

...

public class Logger
{
    // BEST PRACTICE: private synchronization object. 
    // lock on _syncRoot - you should have one for each critical
    // section - to avoid locking on public 'this' instance
    private readonly object _syncRoot = new object ();

    // synchronization device for stopping our log thread.
    // initialized to unsignaled state - when set to signaled
    // we stop!
    private readonly AutoResetEvent _isStopping = 
        new AutoResetEvent (false);

    // use a Queue<>, cleaner and less error prone than
    // manipulating an array. btw, check your indexing
    // on your array queue, while starvation will not
    // occur in your full pass, ordering is not preserved
    private readonly Queue<LogObj> _queue = new Queue<LogObj>();

    ...

    public void Log (string message)
    {
        // you want to lock ONLY when absolutely necessary
        // which in this case is accessing the ONE resource
        // of _queue.
        lock (_syncRoot)
        {
            _queue.Enqueue (new LogObj (DateTime.Now, message));
        }
    }

    public void GetLog ()
    {
        // while not stopping
        // 
        // NOTE: _loggerThread is polling. to increase poll
        // interval, increase wait period. for a more event
        // driven approach, consider using another
        // AutoResetEvent at end of loop, and signal it
        // from Log() method above
        for (; !_isStopping.WaitOne(1); )
        {
            List<LogObj> logs = null;
            // again lock ONLY when you need to. because our log
            // operations may be time-intensive, we do not want
            // to block pessimistically. what we really want is 
            // to dequeue all available messages and release the
            // shared resource.
            lock (_syncRoot)
            {
                // copy messages for local scope processing!
                // 
                // NOTE: .Net3.5 extension method. if not available
                // logs = new List<LogObj> (_queue);
                logs = _queue.ToList ();
                // clear the queue for new messages
                _queue.Clear ();
                // release!
            }
            foreach (LogObj log in logs)
            {
                // do your thang
                ...
            }
        }
    }
}
...
public void Stop ()
{
    // graceful thread termination. give threads a chance!
    _isStopping.Set ();
    _loggerThread.Join (100);
    if (_loggerThread.IsAlive)
    {
        _loggerThread.Abort ();
    }
    _loggerThread = null;
}

答案 2 :(得分:1)

实际上,你在这里引入锁定。在将日志条目推送到队列时锁定(Log方法):如果10个线程同时将10个项目推入队列并唤醒Logger线程,则第11个线程将等待记录器线程记录所有项目...

如果你想要一些真正可扩展的东西 - 实现无锁队列(例如下面的例子)。使用无锁队列同步机制将非常直接(您甚至可以使用单个等待句柄进行通知)。

如果你无法在网上找到无锁队列实现,这里有一个想法如何做到这一点: 使用链接列表进行实施。链表中的每个节点都包含和对下一个节点的易失性引用。因此,对于操作入队和出队,您可以使用Interlocked.CompareExchange方法。我希望,这个想法很清楚。如果没有 - 让我知道,我会提供更多细节。

答案 3 :(得分:0)

我现在只是在做一个思考实验,因为我现在没有时间真正尝试代码,但我认为如果你有创意,你可以完全没有锁定。

让您的日志记录类包含一个方法,该方法在每次调用时分配队列和信号量(以及另一个在线程完成时释放队列和信号量的方法)。想要进行日志记录的线程在启动时会调用此方法。当他们想要记录时,他们将消息推送到他们自己的队列并设置信号量。记录器线程有一个大循环,它遍历队列并检查相关的信号量。如果与队列关联的信号量大于零,则弹出队列并减少信号量。

因为在设置信号量之前你没有尝试从队列中弹出东西,并且在你把东西推到队列之前你没有设置信号量,我这将是安全的。根据队列类的MSDN文档,如果要枚举队列而另一个线程修改集合,则会引发异常。赶上那个例外,你应该做得很好。