web-dev-qa-db-ja.com

クラスをスレッドセーフにする方法

私はC#アプリケーションを書いています。私は(一種の)ロギングクラスを持っています。このロギングクラスは多くのスレッドで使用されます。このクラスのスレッドを安全にする方法は?シングルトンにする必要がありますか?そこでのベストプラクティスは何ですか?スレッドセーフにする方法について読むことができるドキュメントはありますか?

ありがとう

23

C#では、任意のオブジェクトを使用して「クリティカルセクション」、つまり2つのスレッドで同時に実行してはならないコードを保護できます。

たとえば、次の例ではSharedLogger.Writeメソッドへのアクセスが同期されるため、常に1つのスレッドのみがメッセージをログに記録します。

public class SharedLogger : ILogger
{
   public static SharedLogger Instance = new SharedLogger();

   public void Write(string s)
   {
      lock (_lock)
      {
         _writer.Write(s);
      }
   }

   private SharedLogger() 
   { 
      _writer = new LogWriter();
   }

   private object _lock;
   private LogWriter _writer;
}
19
jeremyalan

ロギングクラスをスレッドセーフにすることについてすでに述べた内容に何かを追加できるかどうかはわかりません。すでに述べたように、これを行うには、リソース、つまりログファイルへのアクセスを同期して、一度に1つのスレッドのみがリソースにログインしようとするようにする必要があります。 C#lockキーワードは、これを行うための適切な方法です。

ただし、(1)シングルトンアプローチ、および(2)最終的に使用することを決定したアプローチの使いやすさについて説明します。

(1)アプリケーションがすべてのログメッセージを単一のログファイルに書き込む場合、シングルトンパターンは間違いなく進むべき道です。ログファイルは起動時に開かれ、シャットダウン時に閉じられます。シングルトンパターンは、この操作の概念に完全に適合します。ただし、@ dtbが指摘しているように、クラスをシングルトンにすることはスレッドセーフを保証するものではないことに注意してください。そのためにlockキーワードを使用します。

(2)アプローチの使いやすさについては、次の提案された解決策を検討してください。

public class SharedLogger : ILogger
{
   public static SharedLogger Instance = new SharedLogger();
   public void Write(string s)
   {
      lock (_lock)
      {
         _writer.Write(s);
      }
   }
   private SharedLogger()
   {
       _writer = new LogWriter();
   }
   private object _lock;
   private LogWriter _writer;
}

最初に、このアプローチは一般的に問題ないと思います。 SharedLogger静的変数を介してInstanceのシングルトンインスタンスを定義し、プライベートコンストラクターによって他のユーザーがクラスをインスタンス化するのを防ぎます。これはシングルトンパターンの本質ですが、行き過ぎる前に C#のシングルトン に関するJon Skeetのアドバイスを読んでそれに従うことを強くお勧めします。

ただし、私が注目したいのは、このソリューションの使いやすさです。 「使いやすさ」とは、この実装を使用してメッセージをログに記録する方法を指します。呼び出しがどのように見えるかを考えてみましょう。

SharedLogger.Instance.Write("log message");

その「インスタンス」部分全体が間違っているように見えますが、実装を考えるとそれを回避する方法はありません。代わりに、次の代替案を検討してください。

public static class SharedLogger : ILogger
{
   private static LogWriter _writer = new LogWriter();
   private static object _lock = new object();
   public static void Write(string s)
   {
       lock (_lock)
       {
           _writer.Write(s);
       }
   }
}

クラスが静的になっていることに注意してください。つまり、そのすべてのメンバーとメソッドは静的である必要があります。前の例と実質的に違いはありませんが、その使用法を検討してください。

SharedLogger.Write("log message");

コーディングははるかに簡単です。

重要なのは、前者の解決策を軽視することではなく、選択した解決策の使いやすさが見落とされない重要な側面であることを示唆することです。優れた使いやすいAPIを使用すると、コードの記述が簡単になり、エレガントになり、保守が容易になります。

12
Matt Davis

堅固で使いやすいものがいくつかあるので、これには既成のロガーを使用します。自分で転がす必要はありません。お勧めします Log4Net。

8
captncraig
  • ローカル変数を使用してほとんどの計算を試してから、オブジェクトの状態を1つのlockedブロックで変更します。
  • 一部の変数は、それらを読んだときと状態を変更するときの間で変わる可能性があることに注意してください。
6
BCS

lock() を使用して、複数のスレッドが同時にロギングを使用しないようにします

3
Nick

BCSの回答によると:

BCSは、ステートレスオブジェクトの場合を説明しています。このようなオブジェクトは、さまざまなtheadからの呼び出しによって破壊される可能性のある独自の変数がないため、本質的にスレッドセーフです。

説明されているロガーには、使用をシリアル化する必要のあるファイルハンドル(C#ユーザーではなく、IDiskFileResourceまたはそのようなMS-ismと呼ばれている)があります。

したがって、メッセージのストレージを、メッセージをログファイルに書き込むロジックから分離します。ロジックは一度に1つのメッセージに対してのみ動作します。

これを行う1つの方法は、ロガーオブジェクトがメッセージオブジェクトのキューを保持することであり、ロガーオブジェクトがメッセージをキューからポップするロジックを持っているだけで、メッセージオブジェクトから有用なものを抽出し、それをに書き込むことです。ログ、次にキュー内の別のメッセージを探します。キューのadd/remove/queue_size/etc操作をスレッドセーフにすることで、そのスレッドを安全にすることができます。これには、ロガークラス、メッセージクラス、およびスレッドセーフキュー(おそらく3番目のクラスであり、そのインスタンスはロガークラスのメンバー変数です)が必要になります。

2
Eric M

私の意見では、上記の提供されたコードはもはやスレッドセーフではありません。以前のソリューションでは、SharedLoggerの新しいオブジェクトをインスタンス化する必要があり、Writeメソッドはすべてのオブジェクトに対して1回存在していました。

これで、すべてのスレッドで使用される1つのWriteメソッドができました。例:

スレッド1:SharedLogger.Write( "スレッド1")

スレッド2:SharedLogger.Write( "スレッド2");

   public static void Write(string s)
   {
       // thread 1 is interrupted here <=
       lock (_lock)
       {
           _writer.Write(s);
       }
   }
  • スレッド1はメッセージを書きたいのですが、スレッド2によって中断されています(コメントを参照)
  • スレッド2はスレッド1のメッセージを上書きし、スレッド1によって中断されます

  • スレッド1はロックを取得し、「スレッド2」を書き込みます

  • スレッド1がロックを解放します
  • スレッド2はロックを取得し、「スレッド2」を書き込みます
  • スレッド2がロックを解除します

私が間違っているときに私を修正してください...

1
Sallac

パフォーマンスが主な問題ではない場合、たとえば、クラスに大きな負荷がかかっていない場合は、次のようにします。

クラスにContextBoundObjectを継承させる

この属性をクラスに適用します[同期]

これで、クラス全体に一度にアクセスできるのは1つのスレッドのみになります。

速度に関しては最悪のケースに近いので、診断には本当に便利ですが、「これは奇妙な問題であるレーシング状態ですか」をすばやく特定し、それをスローして、テストを再実行します。問題が解消した場合...あなたはそれがスレッドの問題だと知っています...

よりパフォーマンスの高いオプションは、ロギングクラスにスレッドセーフのメッセージキューを作成することです(ロギングメッセージを受け入れ、それらを取り出して順次処理するだけです...

たとえば、新しい並列処理のConcurrentQueueクラスは、優れたスレッドセーフキューです。

または、すでに スレッドセーフ であるユーザーlog4net RollingLogFileAppender。

1