web-dev-qa-db-ja.com

NLogはどの程度スレッドセーフですか?

上手、

私はこの問題を投稿することを決定する前に数日待っていました。これをどのように述べるかわからなかったので、長い詳細な投稿に帰着しました。しかし、この時点でコミュニティの助けを求めることは適切だと思います。

基本的に、私はNLogを使用して数百のスレッドのロガーを構成しようとしました。これは非常に簡単だと思いましたが、数十秒後にこの例外が発生しました: "InvalidOperationException:コレクションが変更されました;列挙操作が実行されない可能性があります"

これがコードです。

//Launches threads that initiate loggers
class ThreadManager
{
    //(...)
    for (int i = 0; i<500; i++)
    {
        myWorker wk = new myWorker();
        wk.RunWorkerAsync();
    }

    internal class myWorker : : BackgroundWorker
    {             
       protected override void OnDoWork(DoWorkEventArgs e)
       {              
           // "Logging" is Not static - Just to eliminate this possibility 
           // as an error culprit
           Logging L = new Logging(); 
           //myRandomID is a random 12 characters sequence
           //iLog Method is detailed below
          Logger log = L.iLog(myRandomID);
          base.OnDoWork(e);
       }
    }
}

public class Logging
{   
        //ALL THis METHOD IS VERY BASIC NLOG SETTING - JUST FOR THE RECORD
        public Logger iLog(string loggerID)
        {
        LoggingConfiguration config;
        Logger logger;
        FileTarget FileTarget;            
        LoggingRule Rule; 

        FileTarget = new FileTarget();
        FileTarget.DeleteOldFileOnStartup = false;
        FileTarget.FileName =  "X:\\" + loggerID + ".log";

        AsyncTargetWrapper asyncWrapper = new AsyncTargetWrapper();
        asyncWrapper.QueueLimit = 5000;
        asyncWrapper.OverflowAction = AsyncTargetWrapperOverflowAction.Discard;
        asyncWrapper.WrappedTarget = FileTarget;

        //config = new LoggingConfiguration(); //Tried to Fool NLog by this trick - bad idea as the LogManager need to keep track of all config content (which seems to cause my problem;               
        config = LogManager.Configuration;                
        config.AddTarget("File", asyncWrapper);                
        Rule = new LoggingRule(loggerID, LogLevel.Info, FileTarget);

        lock (LogManager.Configuration.LoggingRules)
            config.LoggingRules.Add(Rule);                

        LogManager.Configuration = config;
        logger = LogManager.GetLogger(loggerID);

        return logger;
    }
}   

だから私はここに質問を投稿して家族のような時間を過ごすのではなく、週末を過ごしてそれを掘り下げました(ラッキーボーイ!)NLOG 2.0の最新の安定したリリースをダウンロードして、それを自分の事業。私はそれが吹いた正確な場所を追跡することができました:

logFactory.cs内:

    internal void GetTargetsByLevelForLogger(string name, IList<LoggingRule> rules, TargetWithFilterChain[] targetsByLevel, TargetWithFilterChain[] lastTargetsByLevel)
    {
        //lock (rules)//<--Adding this does not fix it
            foreach (LoggingRule rule in rules)//<-- BLOWS HERE
            {
            }
     }

loggingConfiguration.cs内:

internal void FlushAllTargets(AsyncContinuation asyncContinuation)
    {            
        var uniqueTargets = new List<Target>();
        //lock (LoggingRules)//<--Adding this does not fix it
        foreach (var rule in this.LoggingRules)//<-- BLOWS HERE
        {
        }
     }

私によると問題
つまり、私の理解に基づくと、LogManagerが混同されることが起こります異なるスレッドからのconfig.LoggingRules.Add(Rule)の呼び出し whileGetTargetsByLevelForLoggerおよびFlushAllTargetsが呼び出されています。 foreachをねじ込み、forループに置き換えようとしましたが、ロガーが不正になりました(多くのログファイルの作成をスキップしました)

SOoooo [〜#〜]最後に[〜#〜]
NLOGはスレッドセーフであるとどこにでも書かれていますが、私はさらに掘り下げて、これは使用シナリオに依存すると主張するいくつかの投稿を目にしました。私の場合はどうですか?私は何千ものロガーを作成する必要があります(すべてを同時にではありませんが、それでも非常に速いペースで)。

私が見つけた回避策は、同じマスタースレッドですべてのロガーを作成することです。これは[〜#〜]本当に[〜#〜]不便です。アプリの起動時にすべてのアプリロガーを作成するからです(一種のロガープール)。そして、それはうまく機能しますが、それは受け入れられるデザインではありません。

だからあなたはそれをすべての人々に知っています。コーダーが彼の家族にもう一度会うのを手伝ってください。

27
Mehdi LAMRANI

私はあなたの問題に対する答えを本当に持っていませんが、私はいくつかの観察といくつかの質問があります:

コードによると、スレッドごとにロガーを作成し、そのロガーに、渡されたID値にちなんで名付けられたファイルにログを記録させたいようです。したがって、IDが「abc」のロガーは「x:\ abc.log」にログを記録し、「def」は「x:\ def.log」にログを記録します。プログラムではなく、NLog構成を介してこれを実行できると思います。それがもっとうまくいくかどうか、またはNLogがあなたが抱えているのと同じ問題を抱えているかどうかはわかりません。

私の第一印象は、スレッドごとにファイルターゲットを作成する、スレッドごとに新しいルールを作成する、新しいロガーインスタンスを取得するなど、多くの作業を行っているということです。達成するために。

NLogでは、少なくともいくつかのNLog LayoutRendererに基づいて、出力ファイルに動的に名前を付けることができます。たとえば、私はこれが機能することを知っています:

fileName="${level}.log"

次のようなファイル名が表示されます。

Trace.log
Debug.log
Info.log
Warn.log
Error.log
Fatal.log

したがって、たとえば、次のようなパターンを使用して、スレッドIDに基づいて出力ファイルを作成できるようです。

fileName="${threadid}.log"

そして、スレッド101と102を使用することになった場合、101.logと102.logの2つのログファイルが作成されます。

あなたの場合、あなたはあなた自身のIDに基づいてファイルに名前を付けたいと思います。 IDをMappedDiagnosticContext(スレッドローカルの名前と値のペアを格納できる辞書)に格納してから、パターンで参照することができます。

ファイル名のパターンは次のようになります。

fileName="${mdc:myid}.log"

したがって、コードでこれを行うことができます。

         public class ThreadManager
         {
           //Get one logger per type.
           private static readonly Logger logger = LogManager.GetCurrentClassLogger();

           protected override void OnDoWork(DoWorkEventArgs e)
           {
             // Set the desired id into the thread context
             NLog.MappedDiagnosticsContext.Set("myid", myRandomID);

             logger.Info("Hello from thread {0}, myid {1}", Thread.CurrentThread.ManagedThreadId, myRandomID);
             base.OnDoWork(e);  

             //Clear out the random id when the thread work is finished.
             NLog.MappedDiagnosticsContext.Remove("myid");
           }
         }

このようなものにより、ThreadManagerクラスに「ThreadManager」という名前の単一のロガーを含めることができます。メッセージをログに記録するたびに、フォーマットされた文字列がInfo呼び出しに記録されます。ロガーがファイルターゲットにログを記録するように構成されている場合(構成ファイルで、ファイル名レイアウトが次のようなファイルターゲットに「* .ThreadManager」を送信するルールを作成します。

fileName="${basedir}/${mdc:myid}.log"

メッセージがログに記録されると、NLogはfileNameレイアウトの値に基づいてファイル名を決定します(つまり、ログ時にフォーマットトークンを適用します)。ファイルが存在する場合、メッセージがそのファイルに書き込まれます。ファイルがまだ存在しない場合は、ファイルが作成され、メッセージがログに記録されます。

各スレッドに「aaaaaaaaaaaa」、「aaaaaaaaaaab」、「aaaaaaaaaaac」などのランダムなIDがある場合は、次のようなログファイルを取得する必要があります。

aaaaaaaaaaaa.log
aaaaaaaaaaab.log
aaaaaaaaaaac.log

等々。

この方法でそれを行うことができれば、NLogのプログラムによる構成(ルールとファイルターゲットの作成)のすべてを行う必要がないため、作業が簡単になるはずです。また、NLogに出力ファイル名の作成について心配させることができます。

これがあなたがやっていたことよりもうまくいくかどうかはわかりません。または、たとえそうだとしても、全体像で何をしているのかを本当に理解する必要があるかもしれません。テストして、それが機能することを確認するのは簡単です(つまり、MappedDiagnosticContextの値に基づいて出力ファイルに名前を付けることができます)。それでうまくいく場合は、何千ものスレッドを作成している場合に試してみることができます。

更新:

サンプルコードは次のとおりです。

このプログラムの使用:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using NLog;
using System.Threading;
using System.Threading.Tasks;

namespace NLogMultiFileTest
{
  class Program
  {
    public static Logger logger = LogManager.GetCurrentClassLogger();

    static void Main(string[] args)
    {

      int totalThreads = 50;
      TaskCreationOptions tco = TaskCreationOptions.None;
      Task task = null;

      logger.Info("Enter Main");

      Task[] allTasks = new Task[totalThreads];
      for (int i = 0; i < totalThreads; i++)
      {
        int ii = i;
        task = Task.Factory.StartNew(() =>
        {
          MDC.Set("id", "_" + ii.ToString() + "_");
          logger.Info("Enter delegate.  i = {0}", ii);
          logger.Info("Hello! from delegate.  i = {0}", ii);
          logger.Info("Exit delegate.  i = {0}", ii);
          MDC.Remove("id");
        });

        allTasks[i] = task;
      }

      logger.Info("Wait on tasks");

      Task.WaitAll(allTasks);

      logger.Info("Tasks finished");

      logger.Info("Exit Main");
    }
  }
}

そしてこのNLog.configファイル:

<?xml version="1.0" encoding="utf-8" ?>
<!-- 
  This file needs to be put in the application directory. Make sure to set 
  'Copy to Output Directory' option in Visual Studio.
  -->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <targets>
        <target name="file" xsi:type="File" layout="${longdate} | ${processid} | ${threadid} | ${logger} | ${level} | id=${mdc:id} | ${message}" fileName="${basedir}/log_${mdc:item=id}.txt" />
    </targets>

    <rules>
        <logger name="*" minlevel="Debug" writeTo="file" />
    </rules>
</nlog>

デリゲートの実行ごとに1つのログファイルを取得できます。ログファイルは、MDC(MappedDiagnosticContext)に保存されている「id」にちなんで名付けられています。

したがって、サンプルプログラムを実行すると、50個のログファイルが取得されます。各ログファイルには、「Enter ...」、「Hello ...」、「Exit ...」の3行が含まれています。各ファイルの名前はlog__X_.txtここで、Xはキャプチャされたカウンタ(ii)の値であるため、log _。txt、log _1。txt、log _1 = .txtなど、log _49。txt。各ログファイルには、デリゲートの1回の実行に関連するログメッセージのみが含まれます。

これはあなたがやりたいことと似ていますか?私のサンプルプログラムは、スレッドではなくタスクを使用しています。これは、以前に作成したものだからです。テクニックはあなたがしていることに十分簡単に​​適応するべきだと思います。

同じNLog.configファイルを使用して、この方法(デリゲートの実行ごとに新しいロガーを取得する)を行うこともできます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using NLog;
using System.Threading;
using System.Threading.Tasks;

namespace NLogMultiFileTest
{
  class Program
  {
    public static Logger logger = LogManager.GetCurrentClassLogger();

    static void Main(string[] args)
    {

      int totalThreads = 50;
      TaskCreationOptions tco = TaskCreationOptions.None;
      Task task = null;

      logger.Info("Enter Main");

      Task[] allTasks = new Task[totalThreads];
      for (int i = 0; i < totalThreads; i++)
      {
        int ii = i;
        task = Task.Factory.StartNew(() =>
        {
          Logger innerLogger = LogManager.GetLogger(ii.ToString());
          MDC.Set("id", "_" + ii.ToString() + "_");
          innerLogger.Info("Enter delegate.  i = {0}", ii);
          innerLogger.Info("Hello! from delegate.  i = {0}", ii);
          innerLogger.Info("Exit delegate.  i = {0}", ii);
          MDC.Remove("id");
        });

        allTasks[i] = task;
      }

      logger.Info("Wait on tasks");

      Task.WaitAll(allTasks);

      logger.Info("Tasks finished");

      logger.Info("Exit Main");
    }
  }
}
34
wageoghe

NLogはわかりませんが、上記の部分とAPIドキュメント(http://nlog-project.org/help/)からわかるように、静的構成は1つだけです。したがって、このメソッドを使用して、ロガーが作成されたときにのみ(それぞれ異なるスレッドから)構成にルールを追加する場合は、同じ構成オブジェクトを編集しています。 NLogのドキュメントで確認できる限り、ロガーごとに個別の構成を使用する方法はないため、すべてのルールが必要です。

ルールを追加する最善の方法は、非同期ワーカーを開始する前にルールを追加することですが、それはあなたが望むものではないと思います。

すべてのワーカーに1つのロガーを使用することも可能です。ただし、すべてのワーカーが個別のファイルに必要であると想定します。

各スレッドが独自のロガーを作成し、独自のルールを構成に追加している場合は、その周りにロックをかける必要があります。コードを同期している場合でも、ルールを変更しているときに、他のコードがルールを列挙している可能性があることに注意してください。ご覧のとおり、NLogはこれらのコードビットをロックしません。したがって、スレッドセーフの主張は実際のログ書き込み方法のみを対象としていると思います。

既存のロックが何をするのかはわかりませんが、意図したとおりに機能していないとは思いません。だから、変更

...
lock (LogManager.Configuration.LoggingRules)
config.LoggingRules.Add(Rule);                

LogManager.Configuration = config;
logger = LogManager.GetLogger(loggerID);

return logger;

...
lock(privateConfigLock){
    LogManager.Configuration.LoggingRules.Add(Rule);                

    logger = LogManager.GetLogger(loggerID);
}
return logger;

「所有」しているオブジェクト、つまりクラス専用のオブジェクトのみをロックすることがベストプラクティスと見なされていることに注意してください。これにより、他のコード(ベストプラクティスに準拠していない)の一部のクラスが、デッドロックを作成する可能性のある同じコードでロックされるのを防ぎます。したがって、privateConfigLockをクラスのプライベートとして定義する必要があります。また、次のように、すべてのスレッドが同じオブジェクト参照を参照できるように、静的にする必要があります。

public class Logging{
    // object used to synchronize the addition of config rules and logger creation
    private static readonly object privateConfigLock = new object();
...
7
paulmey

これは古い質問ですが、NLogの現在の所有者として、私は次の洞察を持っています。

  • ロガーの作成はスレッドセーフです
  • ログメッセージの書き込みはスレッドセーフです
  • コンテキストクラスとレンダラーの使用法は(GDC、MDCなど)スレッドセーフです
  • 実行時に新しいターゲット+ルールを追加することはスレッドセーフです(LoggingConfiguration.AddRule + ReconfigExistingLoggersを使用する場合)
  • LoggingConfigurationのリロードを実行すると、リロードが完了するまで、アクティブなロガーからのLogEventsがドロップされます。
  • 実行時に既存のルールとターゲットの値を変更することはスレッドセーフではありません

実行時に既存のアイテムの値を変更することは避けてください。代わりに、 コンテキストレンダラー${event-properties}${GDC}${MDLC}など)を使用する必要があります。

2
Julian

lockを正しく使用していない可能性があると思います。メソッドに渡されたオブジェクトをロックしているので、これは、異なるスレッドが異なるオブジェクトをロックする可能性があることを意味すると思います。

ほとんどの人はこれをクラスに追加するだけです。

private static readonly object Lock = new object();

次に、それをロックして、常に同じオブジェクトになることを保証します。

これで問題が解決するかどうかはわかりませんが、現在行っていることは私には奇妙なことです。他の人は反対するかもしれません。

0
Buh Buh

コレクションのコピーを列挙することで、この問題を解決できます。 NLogソースに次の変更を加えましたが、問題が解決したようです。

internal void GetTargetsByLevelForLogger(string name, IList<LoggingRule> rules, TargetWithFilterChain[] targetsByLevel, TargetWithFilterChain[] lastTargetsByLevel)
{
   foreach (LoggingRule rule in rules.ToList<LoggingRule>())
   {
   ... 
0
alanh

彼らのウィキ のドキュメントによると、それらはスレッドセーフです:

ロガーはスレッドセーフであるため、ロガーを一度作成して静的変数に格納するだけで済みます。

発生するエラーに基づいて、例外の状態を正確に実行している可能性があります。foreachループの反復中に、コードのどこかでコレクションを変更します。リストは参照によって渡されるため、反復中に別のスレッドがルールのコレクションを変更する場所を想像するのは難しくありません。 2つのオプションがあります。

  1. すべてのルールを事前に作成してから、まとめて追加します(これが必要だと思います)。
  2. リストを読み取り専用コレクション(rules.AsReadOnly())にキャストしますが、生成したスレッドが新しいルールを作成するときに発生する更新を見逃します。
0
Joel B