[英]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++; }
}
基本上,这里是这段代码的要点:
这个解决方案是线程安全的吗? 我一直在阅读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.