繁体   English   中英

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

[英]Multi-threaded application interaction with logger thread

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

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

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

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

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. 启动一个低优先级线程,循环记录队列并将挂起的日志打印到输出。 此后,线程暂停,直到新日志到达;
  2. 当日志到达时,记录器线程被唤醒并开始工作。

这个解决方案是线程安全的吗? 我一直在阅读Producers-Consumers问题和求解算法,但是在这个问题中虽然我有多个生产者,但我只有一个读者。

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

  • 获取锁可能是一项昂贵的操作(如@Vitaliy Lipchinsky 所说)。 我建议使用互锁操作将您的记录器与天真的“直写”记录器和记录器进行基准测试。 另一种选择是将现有队列与GetLog空队列交换并立即离开临界区。 这样,任何生产者都不会被消费者的长时间操作所阻塞。

  • 使 LogObj 引用类型(类)。 将其设置为 struct 是没有意义的,因为您无论如何都要对其进行装箱。 或者使_queue字段的类型为LogObj[] (无论如何都更好)。

  • 使您的线程成为后台,以便在不调用Stop时它不会阻止关闭您的程序。

  • 刷新你的TextWriter 否则,您甚至可能会丢失那些设法适合队列的记录(恕我直言,10 个项目有点小)

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

虽然它看起来是线程安全的,但我认为它并不是特别理想的。 我会建议沿着这些路线的解决方案

注意:只需阅读其他回复。 下面是一个基于您自己的相当优化的乐观锁定解决方案。 主要区别在于锁定内部类,最小化“临界区”,并提供优雅的线程终止。 如果您想完全避免锁定,那么您可以像@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;
}

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

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

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

我只是在这里做一个思想实验,因为我现在没有时间实际尝试代码,但我认为如果你有创意,你可以完全不用锁来做到这一点。

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

因为在设置信号量之前您不会尝试将事物从队列中弹出,并且在将事物推入队列之后才设置信号量,所以我认为这将是安全的。 根据队列类的 MSDN 文档,如果您正在枚举队列并且另一个线程修改了集合,则会引发异常。 抓住那个例外,你应该很好。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM