ロギング(およびその他の目的)のために、プロジェクトの1つでEnterprise Library 4を使用しています。別のスレッドでログを記録することで軽減できる、実行中のログ記録にはコストがかかることに気づきました。
これを行う方法は、LogEntryオブジェクトを作成してから、Logger.Writeを呼び出すデリゲートでBeginInvokeを呼び出すことです。
new Action<LogEntry>(Logger.Write).BeginInvoke(le, null, null);
私が本当にやりたいことは、ログメッセージをキューに追加し、シングルスレッドでLogEntryインスタンスをキューからプルして、ログ操作を実行することです。これの利点は、ロギングによって実行中の操作が妨げられることはなく、すべてのロギング操作がジョブをスレッドプールにスローするわけではないことです。
スレッドセーフな方法で多くのライターと1つのリーダーをサポートする共有キューを作成するにはどうすればよいですか?多くのライター(同期/ブロックを引き起こさない)と単一のリーダーをサポートするように設計されたキュー実装のいくつかの例は、本当にありがたいです。
代替アプローチに関する推奨事項も高く評価されますが、私はロギングフレームワークの変更には興味がありません。
私はしばらく前にこのコードを書いたので、自由に使ってください。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace MediaBrowser.Library.Logging {
public abstract class ThreadedLogger : LoggerBase {
Queue<Action> queue = new Queue<Action>();
AutoResetEvent hasNewItems = new AutoResetEvent(false);
volatile bool waiting = false;
public ThreadedLogger() : base() {
Thread loggingThread = new Thread(new ThreadStart(ProcessQueue));
loggingThread.IsBackground = true;
loggingThread.Start();
}
void ProcessQueue() {
while (true) {
waiting = true;
hasNewItems.WaitOne(10000,true);
waiting = false;
Queue<Action> queueCopy;
lock (queue) {
queueCopy = new Queue<Action>(queue);
queue.Clear();
}
foreach (var log in queueCopy) {
log();
}
}
}
public override void LogMessage(LogRow row) {
lock (queue) {
queue.Enqueue(() => AsyncLogMessage(row));
}
hasNewItems.Set();
}
protected abstract void AsyncLogMessage(LogRow row);
public override void Flush() {
while (!waiting) {
Thread.Sleep(1);
}
}
}
}
いくつかの利点:
これは少し改良されたバージョンです。私はそれに対してほとんどテストを実行していませんが、いくつかの小さな問題に対処しています。
public abstract class ThreadedLogger : IDisposable {
Queue<Action> queue = new Queue<Action>();
ManualResetEvent hasNewItems = new ManualResetEvent(false);
ManualResetEvent terminate = new ManualResetEvent(false);
ManualResetEvent waiting = new ManualResetEvent(false);
Thread loggingThread;
public ThreadedLogger() {
loggingThread = new Thread(new ThreadStart(ProcessQueue));
loggingThread.IsBackground = true;
// this is performed from a bg thread, to ensure the queue is serviced from a single thread
loggingThread.Start();
}
void ProcessQueue() {
while (true) {
waiting.Set();
int i = ManualResetEvent.WaitAny(new WaitHandle[] { hasNewItems, terminate });
// terminate was signaled
if (i == 1) return;
hasNewItems.Reset();
waiting.Reset();
Queue<Action> queueCopy;
lock (queue) {
queueCopy = new Queue<Action>(queue);
queue.Clear();
}
foreach (var log in queueCopy) {
log();
}
}
}
public void LogMessage(LogRow row) {
lock (queue) {
queue.Enqueue(() => AsyncLogMessage(row));
}
hasNewItems.Set();
}
protected abstract void AsyncLogMessage(LogRow row);
public void Flush() {
waiting.WaitOne();
}
public void Dispose() {
terminate.Set();
loggingThread.Join();
}
}
元の上の利点:
はい、プロデューサー/コンシューマーキューが必要です。私のスレッディングチュートリアルにこの例が1つあります。私の "deadlocks/monitor methods" ページを見ると、後半にコードがあります。
もちろん、オンラインには他にもたくさんの例があります。NET4.0は、フレームワークにも1つ付属しています(私のものよりも完全に機能しています!)。 .NET 4.0では、おそらく ConcurrentQueue<T>
BlockingCollection<T>
。
そのページのバージョンは一般的ではありません(long以前に書かれています)おそらくあなたはそれを一般的にしたいと思うでしょう-それは取るに足らない。
各「通常の」スレッドからProduce
を呼び出し、1つのスレッドからConsume
を呼び出して、ループし、消費したものをログに記録します。コンシューマースレッドをバックグラウンドスレッドにするのがおそらく最も簡単なので、アプリが終了するときにキューを「停止」することを心配する必要はありません。つまり、最終的なログエントリが失われる可能性はかなりあります(アプリの終了時に書き込みの途中である場合)。さらに、消費/ログできるよりも速く生成している場合はさらに多くなります。
これが私が思いついたものです... Sam Saffronの回答も参照してください。この回答は、コードに表示されて更新したい問題がある場合のコミュニティWikiです。
/// <summary>
/// A singleton queue that manages writing log entries to the different logging sources (Enterprise Library Logging) off the executing thread.
/// This queue ensures that log entries are written in the order that they were executed and that logging is only utilizing one thread (backgroundworker) at any given time.
/// </summary>
public class AsyncLoggerQueue
{
//create singleton instance of logger queue
public static AsyncLoggerQueue Current = new AsyncLoggerQueue();
private static readonly object logEntryQueueLock = new object();
private Queue<LogEntry> _LogEntryQueue = new Queue<LogEntry>();
private BackgroundWorker _Logger = new BackgroundWorker();
private AsyncLoggerQueue()
{
//configure background worker
_Logger.WorkerSupportsCancellation = false;
_Logger.DoWork += new DoWorkEventHandler(_Logger_DoWork);
}
public void Enqueue(LogEntry le)
{
//lock during write
lock (logEntryQueueLock)
{
_LogEntryQueue.Enqueue(le);
//while locked check to see if the BW is running, if not start it
if (!_Logger.IsBusy)
_Logger.RunWorkerAsync();
}
}
private void _Logger_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
LogEntry le = null;
bool skipEmptyCheck = false;
lock (logEntryQueueLock)
{
if (_LogEntryQueue.Count <= 0) //if queue is empty than BW is done
return;
else if (_LogEntryQueue.Count > 1) //if greater than 1 we can skip checking to see if anything has been enqueued during the logging operation
skipEmptyCheck = true;
//dequeue the LogEntry that will be written to the log
le = _LogEntryQueue.Dequeue();
}
//pass LogEntry to Enterprise Library
Logger.Write(le);
if (skipEmptyCheck) //if LogEntryQueue.Count was > 1 before we wrote the last LogEntry we know to continue without double checking
{
lock (logEntryQueueLock)
{
if (_LogEntryQueue.Count <= 0) //if queue is still empty than BW is done
return;
}
}
}
}
}
Sam Safronsの投稿に応えて、私はフラッシュを呼び出して、すべての書き込みが本当に終了したことを確認したかった。私の場合、キュースレッドでデータベースに書き込みを行っており、すべてのログイベントがキューに入れられていましたが、すべての書き込みが完了する前にアプリケーションが停止することがあり、私の状況では受け入れられません。私はあなたのコードのいくつかのチャンクを変更しましたが、私が共有したかった主なものはフラッシュでした:
public static void FlushLogs()
{
bool queueHasValues = true;
while (queueHasValues)
{
//wait for the current iteration to complete
m_waitingThreadEvent.WaitOne();
lock (m_loggerQueueSync)
{
queueHasValues = m_loggerQueue.Count > 0;
}
}
//force MEL to flush all its listeners
foreach (MEL.LogSource logSource in MEL.Logger.Writer.TraceSources.Values)
{
foreach (TraceListener listener in logSource.Listeners)
{
listener.Flush();
}
}
}
私はそれが誰かにいくつかの欲求不満を救うと思います。これは、大量のデータをログに記録する並列プロセスで特に顕著です。
あなたの解決策を共有してくれてありがとう、それは私を良い方向に導きました!
-ジョニーS
以前の投稿は役に立たなかったと言いたいです。 AutoFlushをtrueに設定するだけで、すべてのリスナーをループする必要がなくなります。しかし、ロガーをフラッシュしようとする並列スレッドで、まだクレイジーな問題がありました。キューのコピーとLogEntry書き込みの実行中にtrueに設定される別のブール値を作成する必要があり、フラッシュルーチンでそのブール値をチェックして、何かがまだキューになく、何も処理されていないことを確認する必要がありました。戻る前に。
これで、複数のスレッドが並行してこの問題に遭遇する可能性があり、flushを呼び出すと、本当にフラッシュされていることがわかります。
public static void FlushLogs()
{
int queueCount;
bool isProcessingLogs;
while (true)
{
//wait for the current iteration to complete
m_waitingThreadEvent.WaitOne();
//check to see if we are currently processing logs
lock (m_isProcessingLogsSync)
{
isProcessingLogs = m_isProcessingLogs;
}
//check to see if more events were added while the logger was processing the last batch
lock (m_loggerQueueSync)
{
queueCount = m_loggerQueue.Count;
}
if (queueCount == 0 && !isProcessingLogs)
break;
//since something is in the queue, reset the signal so we will not keep looping
Thread.Sleep(400);
}
}
ただの更新:
.NET 4.0でentepriseライブラリ5.0を使用すると、次の方法で簡単に実行できます。
static public void LogMessageAsync(LogEntry logEntry)
{
Task.Factory.StartNew(() => LogMessage(logEntry));
}
参照: http://randypaulo.wordpress.com/2011/07/28/c-enterprise-library-asynchronous-logging/
追加レベルの間接参照がここで役立ちます。
最初の非同期メソッド呼び出しは、同期されたキューにメッセージを書き込み、イベントを設定できます。これにより、ロックはワーカースレッドではなくスレッドプールで発生します。その後、イベントが発生すると、別のスレッドがキューからメッセージをプルします。発生します。
別のスレッドで何かをログに記録すると、アプリケーションがクラッシュした場合にメッセージが書き込まれず、役に立たなくなります。
理由は、書き込みを行うたびに常にフラッシュする必要がある理由です。
念頭に置いているのがSHAREDキューの場合、書き込み、プッシュ、ポップを同期する必要があると思います。
しかし、それでも、共有キューの設計を目指す価値はあると思います。ロギングのIO=と比較して、おそらくアプリが実行している他の作業と比較して、プッシュとポップのブロックの短い量はおそらく重要ではありません。