简体   繁体   English

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

[英]Multi-threaded application interaction with logger thread

Here I am again with questions about multi-threading and an exercise of my Concurrent Programming class.在这里,我再次提出有关多线程的问题和我的并发编程课程的练习。

I have a multi-threaded server - implemented using .NET Asynchronous Programming Model - with GET ( download ) and PUT ( upload ) file services.我有一个多线程服务器 - 使用 .NET异步编程模型实现- 带有GET下载)和PUT上传)文件服务。 This part is done and tested.这部分已完成并经过测试。

It happens that the statement of the problem says this server must have logging activity with the minimum impact on the server response time, and it should be supported by a low priority thread - logger thread - created for this effect.碰巧问题的陈述说这个服务器必须具有对服务器响应时间影响最小的日志记录活动,并且它应该得到一个低优先级线程-记录器线程- 为此效果创建的支持。 All logging messages shall be passed by the threads that produce them to this logger thread , using a communication mechanism that may not lock the thread that invokes it (besides the necessary locking to ensure mutual exclusion) and assuming that some logging messages may be ignored.所有日志消息都应由产生它们的线程传递给这个日志线程,使用可能不锁定调用它的线程的通信机制(除了必要的锁定以确保互斥)并假设一些日志消息可能会被忽略。

Here is my current solution, please help validating if this stands as a solution to the stated problem:这是我目前的解决方案,请帮助验证这是否是上述问题的解决方案:

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++; }

}

Basically, here are the main points of this code:基本上,这里是这段代码的要点:

  1. Start a low priority thread that loops the logging queue and prints pending logs to the output.启动一个低优先级线程,循环记录队列并将挂起的日志打印到输出。 After this, the thread is suspended till new log arrives;此后,线程暂停,直到新日志到达;
  2. When a log arrives, the logger thread is awaken and does it's work.当日志到达时,记录器线程被唤醒并开始工作。

Is this solution thread-safe ?这个解决方案是线程安全的吗? I have been reading Producers-Consumers problem and solution algorithm, but in this problem although I have multiple producers, I only have one reader.我一直在阅读Producers-Consumers问题和求解算法,但是在这个问题中虽然我有多个生产者,但我只有一个读者。

It seems it should be working.似乎它应该工作。 Producers-Consumers shouldn't change greatly in case of single consumer.生产者-消费者在单一消费者的情况下不应该有很大变化。 Little nitpicks:小挑剔:

  • acquiring lock may be an expensive operation (as @Vitaliy Lipchinsky says).获取锁可能是一项昂贵的操作(如@Vitaliy Lipchinsky 所说)。 I'd recommend to benchmark your logger against naive 'write-through' logger and logger using interlocked operations.我建议使用互锁操作将您的记录器与天真的“直写”记录器和记录器进行基准测试。 Another alternative would be exchanging existing queue with empty one in GetLog and leaving critical section immediately.另一种选择是将现有队列与GetLog空队列交换并立即离开临界区。 This way none of producers won't be blocked by long operations in consumers.这样,任何生产者都不会被消费者的长时间操作所阻塞。

  • make LogObj reference type (class).使 LogObj 引用类型(类)。 There's no point in making it struct since you are boxing it anyway.将其设置为 struct 是没有意义的,因为您无论如何都要对其进行装箱。 or else make _queue field to be of type LogObj[] (that's better anyway).或者使_queue字段的类型为LogObj[] (无论如何都更好)。

  • make your thread background so that it won't prevent closing your program if Stop won't be called.使您的线程成为后台,以便在不调用Stop时它不会阻止关闭您的程序。

  • Flush your TextWriter .刷新你的TextWriter Or else you are risking to lose even those records that managed to fit queue (10 items is a bit small IMHO)否则,您甚至可能会丢失那些设法适合队列的记录(恕我直言,10 个项目有点小)

  • Implement IDisposable and/or finalizer.实现 IDisposable 和/或终结器。 Your logger owns thread and text writer and those should be freed (and flushed - see above).您的记录器拥有线程和文本编写器,它们应该被释放(并刷新 - 见上文)。

While it appears to be thread-safe, I don't believe it is particularly optimal.虽然它看起来是线程安全的,但我认为它并不是特别理想的。 I would suggest a solution along these lines我会建议沿着这些路线的解决方案

NOTE: just read the other responses.注意:只需阅读其他回复。 What follows is a fairly optimal, optimistic locking solution based on your own.下面是一个基于您自己的相当优化的乐观锁定解决方案。 Major differences is locking on an internal class, minimizing 'critical sections', and providing graceful thread termination.主要区别在于锁定内部类,最小化“临界区”,并提供优雅的线程终止。 If you want to avoid locking altogether, then you can try some of that volatile "non-locking" linked list stuff as @Vitaliy Lipchinsky suggests.如果您想完全避免锁定,那么您可以像@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;
}

Actually, you ARE introducing locking here.实际上,您在这里引入了锁定。 You have locking while pushing a log entry to the queue (Log method): if 10 threads simultaneously pushed 10 items into queue and woke up the Logger thread, then 11th thread will wait until the logger thread log all items...您在将日志条目推送到队列时锁定(日志方法):如果 10 个线程同时将 10 个项目推入队列并唤醒 Logger 线程,则第 11 个线程将等待,直到 logger 线程记录所有项目...

If you want something really scalable - implement lock-free queue (example is below).如果您想要真正可扩展的东西 - 实现无锁队列(示例如下)。 With lock-free queue synchronization mechanism will be really straightaway (you can even use single wait handle for notifications).使用无锁队列同步机制将非常简单(您甚至可以使用单个等待句柄进行通知)。

If you won't manage to find lock-free queue implementation in the web, here is an idea how to do this: Use linked list for an implementation.如果您无法在 Web 中找到无锁队列实现,这里有一个方法:使用链表进行实现。 Each node in linked list contains a value and a volatile reference to the next node.链表中的每个节点都包含一个和对下一个节点的易失性引用。 therefore for operations enqueue and dequeue you can use Interlocked.CompareExchange method.因此对于操作入队和出队,您可以使用 Interlocked.CompareExchange 方法。 I hope, the idea is clear.我希望,这个想法很清楚。 If not - let me know and I'll provide more details.如果没有 - 让我知道,我会提供更多细节。

I'm just doing a thought experiment here, since I don't have time to actually try code right now, but I think you can do this without locks at all if you're creative.我只是在这里做一个思想实验,因为我现在没有时间实际尝试代码,但我认为如果你有创意,你可以完全不用锁来做到这一点。

Have your logging class contain a method that allocates a queue and a semaphore each time it's called (and another that deallocates the queue and semaphore when the thread is done).让您的日志类包含一个方法,该方法在每次调用时分配一个队列和一个信号量(另一个在线程完成时释放队列和信号量)。 The threads that want to do logging will call this method when they start.想要进行日志记录的线程将在启动时调用此方法。 When they want to log, they push the message onto their own queue and set the semaphore.当他们想要记录时,他们将消息推送到自己的队列中并设置信号量。 The logger thread has a big loop that runs through the queues and checks the associated semaphores.记录器线程有一个大循环,它在队列中运行并检查相关的信号量。 If the semaphore associated with the queue is greater than zero, then the queue gets popped off and the semaphore decremented.如果与队列关联的信号量大于零,则队列弹出并且信号量递减。

Because you're not attempting to pop things off the queue until after the semaphore is set, and you're not setting the semaphore until after you've pushed things onto the queue, I think this will be safe.因为在设置信号量之前您不会尝试将事物从队列中弹出,并且在将事物推入队列之后才设置信号量,所以我认为这将是安全的。 According to the MSDN documentation for the queue class, if you are enumerating the queue and another thread modifies the collection, an exception is thrown.根据队列类的 MSDN 文档,如果您正在枚举队列并且另一个线程修改了集合,则会引发异常。 Catch that exception and you should be good.抓住那个例外,你应该很好。

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

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