web-dev-qa-db-ja.com

スレッドセーフのユニットテスト?

私はクラスと多くの単体テストを作成しましたが、スレッドセーフにしませんでした。ここで、クラススレッドを安全にしたいのですが、それを証明してTDDを使用するために、リファクタリングを開始する前に、失敗する単体テストをいくつか作成したいと思います。

これを行うための良い方法はありますか?

私の最初の考えは、いくつかのスレッドを作成し、それらすべてに安全でない方法でクラスを使用させることです。十分な数のスレッドでこれを十分な回数行うと、それが壊れることを確認する必要があります。

66
TheSean

そこで役立つ2つの製品があります。

どちらも(ユニットテストを介して)コードのデッドロックをチェックし、チェスは競合状態もチェックすると思います。

両方のツールの使用は簡単です。簡単な単体テストを作成し、コードを数回実行して、コードでデッドロック/競合状態が発生する可能性があるかどうかを確認します。

編集:Googleは、実行時(テスト中ではない)に競合状態をチェックするツールをリリースしました。これは thread-race-test =。
現在の実行のみを分析し、上記のツールのような考えられるすべてのシナリオを分析するわけではないため、すべての競合状態を検出することはできませんが、発生した競合状態を見つけるのに役立つ場合があります。

更新:TypemockサイトにはRacerへのリンクがなくなり、過去4年間更新されませんでした。プロジェクトは終了したと思います。

21
Dror Helper

問題は、競合状態などのマルチスレッドの問題のほとんどが、その性質上非決定論的であるということです。それらは、エミュレートまたはトリガーできないハードウェアの動作に依存する可能性があります。

つまり、複数のスレッドでテストを行ったとしても、コードに欠陥がある場合は、それらのスレッドが常に失敗することはありません。

10
Max Galkin

Drorの答えはこれを明示的には述べていませんが、少なくともChess(およびおそらくRacer)は、考えられるすべてのインターリーブを介して一連のスレッドを実行し、再現可能なエラーを取得することで機能します。エラーが発生した場合に偶然に発生することを期待して、しばらくの間スレッドを実行するだけではありません。

たとえば、チェスはすべてのインターリーブを実行し、デッドロックが検出されたインターリーブを表すタグ文字列を提供します。これにより、デッドロックの観点から興味深い特定のインターリーブをテストに関連付けることができます。

このツールの正確な内部動作と、デッドロックを修正するために変更する可能性のあるコードにこれらのタグ文字列をマップする方法はわかりませんが、それはあります...私は実際にこのツールを本当に楽しみにしています(およびPex)VSIDEの一部になります。

5
jerryjvl

あなた自身が提案するように、人々がこれを標準の単体テストでテストしようとするのを見てきました。テストは遅く、これまでのところ、当社が苦労している並行性の問題の1つを特定できていません。

多くの失敗の後、そしてユニットテストへの私の愛にもかかわらず、私は並行性のエラーがユニットテストの強みの1つではないことを受け入れるようになりました。私は通常、並行性が主題であるクラスの単体テストを支持して分析とレビューを奨励します。システムの全体的な概要により、多くの場合、スレッドセーフの主張を証明/偽造することが可能です。

とにかく、誰かが私に反対を指すかもしれない何かをくれて欲しいので、私はこの質問を注意深く見ています。

3
daramarak

最近同じ問題に対処しなければならなかったとき、私はそれをこのように考えました。まず第一に、既存のクラスには1つの責任があり、それはいくつかの機能を提供することです。スレッドセーフであることはオブジェクトの責任ではありません。スレッドセーフである必要がある場合は、他のオブジェクトを使用してこの機能を提供する必要があります。ただし、他のオブジェクトがスレッドセーフを提供している場合、コードがスレッドセーフであることを証明できないため、オプションにすることはできません。だからこれは私がそれを扱う方法です:

// This interface is optional, but is probably a good idea.
public interface ImportantFacade
{
    void ImportantMethodThatMustBeThreadSafe();
}

// This class provides the thread safe-ness (see usage below).
public class ImportantTransaction : IDisposable
{
    public ImportantFacade Facade { get; private set; }
    private readonly Lock _lock;

    public ImportantTransaction(ImportantFacade facade, Lock aLock)
    {
        Facade = facade;
        _lock = aLock;
        _lock.Lock();
    }

    public void Dispose()
    {
        _lock.Unlock();
    }
}

// I create a lock interface to be able to fake locks in my tests.
public interface Lock
{
    void Lock();
    void Unlock();
}

// This is the implementation I want in my production code for Lock.
public class LockWithMutex : Lock
{
    private Mutex _mutex;

    public LockWithMutex()
    {
        _mutex = new Mutex(false);
    }

    public void Lock()
    {
        _mutex.WaitOne();
    }

    public void Unlock()
    {
        _mutex.ReleaseMutex();
    }
}

// This is the transaction provider. This one should replace all your
// instances of ImportantImplementation in your code today.
public class ImportantProvider<T> where T:Lock,new()
{
    private ImportantFacade _facade;
    private Lock _lock;

    public ImportantProvider(ImportantFacade facade)
    {
        _facade = facade;
        _lock = new T();
    }

    public ImportantTransaction CreateTransaction()
    {
        return new ImportantTransaction(_facade, _lock);
    }
}

// This is your old class.
internal class ImportantImplementation : ImportantFacade
{
    public void ImportantMethodThatMustBeThreadSafe()
    {
        // Do things
    }
}

ジェネリックスを使用すると、テストで偽のロックを使用して、トランザクションの作成時にロックが常に取得され、トランザクションが破棄されるまで解放されないことを確認できます。これで、重要なメソッドが呼び出されたときにロックが取得されていることを確認することもできます。本番コードでの使用法は次のようになります。

// Make sure this is the only way to create ImportantImplementation.
// Consider making ImportantImplementation an internal class of the provider.
ImportantProvider<LockWithMutex> provider = 
    new ImportantProvider<LockWithMutex>(new ImportantImplementation());

// Create a transaction that will be disposed when no longer used.
using (ImportantTransaction transaction = provider.CreateTransaction())
{
    // Access your object thread safe.
    transaction.Facade.ImportantMethodThatMustBeThreadSafe();
}

重要な実装を他の誰かが作成できないようにすることで(たとえば、プロバイダーで作成してプライベートクラスにするなど)、トランザクションなしではアクセスできず、トランザクションは常に作成時にロックし、廃棄時に解放します。

トランザクションが正しく破棄されることを確認するのは難しい場合があり、そうでない場合は、アプリケーションで奇妙な動作が見られる可能性があります。 (別の回答で提案されているように)Microsoft Chessとしてツールを使用して、そのようなものを探すことができます。または、プロバイダーにファサードを実装させて、次のように実装させることもできます。

    public void ImportantMethodThatMustBeThreadSafe()
    {
        using (ImportantTransaction transaction = CreateTransaction())
        {
            transaction.Facade.ImportantMethodThatMustBeThreadSafe();
        }
    }

これは実装ですが、必要に応じてこれらのクラスを検証するためのテストを理解できることを願っています。

2
Cellfish

RacerやChessのようなツールを使用するほどエレガントではありませんが、スレッドセーフのテストにこの種のものを使用しました。

// from linqpad

void Main()
{
    var duration = TimeSpan.FromSeconds(5);
    var td = new ThreadDangerous(); 

    // no problems using single thread (run this for as long as you want)
    foreach (var x in Until(duration))
        td.DoSomething();

    // thread dangerous - it won't take long at all for this to blow up
    try
    {           
        Parallel.ForEach(WhileTrue(), x => 
            td.DoSomething());

        throw new Exception("A ThreadDangerException should have been thrown");
    }
    catch(AggregateException aex)
    {
        // make sure that the exception thrown was related
        // to thread danger
        foreach (var ex in aex.Flatten().InnerExceptions)
        {
            if (!(ex is ThreadDangerException))
                throw;
        }
    }

    // no problems using multiple threads (run this for as long as you want)
    var ts = new ThreadSafe();
    Parallel.ForEach(Until(duration), x => 
        ts.DoSomething());      

}

class ThreadDangerous
{
    private Guid test;
    private readonly Guid ctrl;

    public void DoSomething()
    {           
        test = Guid.NewGuid();
        test = ctrl;        

        if (test != ctrl)
            throw new ThreadDangerException();
    }
}

class ThreadSafe
{
    private Guid test;
    private readonly Guid ctrl;
    private readonly object _lock = new Object();

    public void DoSomething()
    {   
        lock(_lock)
        {
            test = Guid.NewGuid();
            test = ctrl;        

            if (test != ctrl)
                throw new ThreadDangerException();
        }
    }
}

class ThreadDangerException : Exception 
{
    public ThreadDangerException() : base("Not thread safe") { }
}

IEnumerable<ulong> Until(TimeSpan duration)
{
    var until = DateTime.Now.Add(duration);
    ulong i = 0;
    while (DateTime.Now < until)
    {
        yield return i++;
    }
}

IEnumerable<ulong> WhileTrue()
{
    ulong i = 0;
    while (true)
    {
        yield return i++;
    }
}

非常に短い時間でスレッドの危険な状態を一貫して発生させることができる場合、状態の破損を観察せずに比較的長い時間待機することで、スレッドセーフな状態を引き起こし、それらを検証できるはずです。

これは原始的な方法であり、複雑なシナリオでは役に立たない可能性があることを認めます。

1
Ronnie Overby

testNGまたはspringframeworksテストモジュール(または他の拡張機能)を備えたJUnitは、同時実行テストの基本的なサポートを備えています。

このリンクはあなたに興味があるかもしれません

http://www.cs.rice.edu/~javaplt/papers/pppj2009.pdf

1
surajz

懸念される同時実行シナリオごとにテストケースを作成する必要があります。これには、競合の可能性を高めるために、効率的な操作をより遅い同等のもの(またはモック)に置き換え、ループで複数のテストを実行する必要がある場合があります

特定のテストケースがなければ、特定のテストを提案することは困難です

いくつかの潜在的に有用な参考資料:

1
Steven A. Lowe

これが私のアプローチです。このテストはデッドロックには関係せず、一貫性に関係します。同期されたブロックを使用して、次のようなコードでメソッドをテストしています。

synchronized(this) {
  int size = myList.size();
  // do something that needs "size" to be correct,
  // but which will change the size at the end.
  ...
}

スレッドの競合を確実に発生させるシナリオを作成するのは難しいですが、これが私がしたことです。

まず、ユニットテストで50個のスレッドを作成し、それらをすべて同時に起動して、すべてにメソッドを呼び出させました。カウントダウンラッチを使用して、それらをすべて同時に開始します。

CountDownLatch latch = new CountDownLatch(1);
for (int i=0; i<50; ++i) {
  Runnable runner = new Runnable() {
    latch.await(); // actually, surround this with try/catch InterruptedException
    testMethod();
  }
  new Thread(runner, "Test Thread " +ii).start(); // I always name my threads.
}
// all threads are now waiting on the latch.
latch.countDown(); // release the latch
// all threads are now running the test method at the same time.

これにより、競合が発生する場合と発生しない場合があります。私のtestMethod()は、競合が発生した場合に例外をスローできる必要があります。しかし、これが競合を引き起こすかどうかはまだわかりません。したがって、テストが有効かどうかはわかりません。だからここにトリックがあります:同期されたキーワードをコメントアウトしてテストを実行します。これが競合を引き起こす場合、テストは失敗します。 synchronizedキーワードなしで失敗した場合、テストは有効です。

それが私がしたことであり、私のテストは失敗しなかったので、それは(まだ)有効なテストではありませんでした。しかし、上記のコードをループ内に配置し、100回連続して実行することで、確実に障害を発生させることができました。そのため、メソッドを5000回呼び出します。 (はい、これは遅いテストを生成します。それについて心配しないでください。あなたの顧客はこれに悩まされることはないので、あなたもそうすべきではありません。)

このコードを外部ループ内に配置すると、外部ループの約20回目の反復で障害を確実に確認できました。これで、テストが有効であると確信し、同期されたキーワードを復元して実際のテストを実行しました。 (機能した。)

テストが1つのマシンで有効であり、別のマシンでは有効でないことに気付く場合があります。テストが1台のマシンで有効であり、メソッドがテストに合格した場合、おそらくすべてのマシンでスレッドセーフです。ただし、夜間の単体テストを実行するマシンで有効性をテストする必要があります。

0
MiguelMunoz