为什么这个ThreadPool.QueueUserWorkItem代码没有失败我的测试?

时间:2012-02-20 23:56:55

标签: c# multithreading tdd threadpool

有问题的代码

public void StartPlaying()
{
    ThreadPool.QueueUserWorkItem(ignoredState =>
    {
        while (_playlist.Count > 0)
        {
            var audioFile = _playlist.Dequeue();

            if (StartedPlaying != null)
                StartedPlaying(this, new TypedAudioFileEventArgs(audioFile));

            audioFile.SoundPlayer.PlaySync();
            audioFile.SoundPlayer.Dispose();

            if (StoppedPlaying != null)
                StoppedPlaying(this, new TypedAudioFileEventArgs(audioFile));
        }
    });
}

和我的测试:

[TestMethod()]
public void StartPlayIsCalledTwice_OnlyRunningOnce()
{
    int timeBetweenPlays = 0; 
    var target = new TypedAudioFilePlayer(timeBetweenPlays);

    target.AddFile(TypedAudioFileCreator.CreateWord(1, "bl"));
    target.StartedPlaying += StartedPlaying_Once;

    target.StartPlaying();
    target.StartPlaying();
}

private bool _once = false;
private void StartedPlaying_Once(object sender, TypedAudioFileEventArgs e)
{
    if (!_once)
        _once = true;
    else
        Assert.Fail("Should not be called more than once!");
}

我相信我的单元测试应该会失败,从ThreadPool.QueueUserWorkItem的MSDN描述来判断:

  

对执行方法进行排队。该方法在线程池线程可用时执行。

默认的ThreadPool大小为512,因此应立即有两个线程可用于处理StartPlaying调用。我相信我的代码应该失败,因为我没有提供任何竞争条件的保障,两个线程都可以访问相同的资源。

这里发生了什么?

3 个答案:

答案 0 :(得分:3)

因为只有在有要播放的项目时调用StartPlaying才会引发StartedPlaying事件。

_playlist.Dequeue();将您排队的文件出列。因此,第二次到达while (_playlist.Count > 0)时,它会立即失败,直接通过第二次调用StartPlaying而不会引发事件。

另外,正如Bruno Silva指出的那样,第二次调用StartPlaying时产生的线程可能没有机会在测试退出之前执行任何操作。

对于它的价值,此代码中还有大约一百万至少2个线程错误:

// Where did _playlist come from?  Is it shared state among the player threads?
// If so, all access to it should be in locks, since queues are not thread safe
while (_playlist.Count > 0) 


// Both of these start threads and then immediately return.  
// The test will probably exit before either of those threads do anything much
    target.StartPlaying();
    target.StartPlaying();
}

如果要进行适当的单元测试,则需要定义前置条件,期望,操作和后置条件:

  • 前提条件:您有一个已初始化的TypedAudioFilePlayer,其中一个文件已排队:

    var target = new TypedAudioFilePlayer(timeBetweenPlays); target.AddFile(TypedAudioFileCreator.CreateWord(1, "bl"));

  • 期望:如果两次调用StartPlaying,则只会引发一次StartedPlaying事件

    target.StartedPlaying += StartedPlaying_Once;

  • 操作: StartPlaying方法将被调用两次:

    target.StartPlaying(); target.StartPlaying();

  • 后置条件: StartedPlaying事件只提出一次:

    private bool _once = false;

    private void StartedPlaying_Once(object sender, TypedAudioFileEventArgs e) { if (!_once) _once = true; else Assert.Fail("Should not be called more than once!"); }

现在,您的测试成功了。在这种情况下,这并不好,因为我在上面解释了。您需要通过消除队列错误和竞争条件来使测试进入失败状态,然后努力使测试通过正确的方式。

答案 1 :(得分:1)

您似乎正在使用两个线程之间的共享资源,因此在第二次调用Play时可能不会将其设置为true。您可以使用锁来允许一次一个线程执行部分代码:

private readonly object lock_object=new object();
private void StartedPlaying_Once(object sender, TypedAudioFileEventArgs e)
{
lock(lock_object)
{    
if (!_once)
        _once = true;
    else
        Assert.Fail("Should not be called more than once!");
}
}

答案 2 :(得分:0)

那些是否会在文本执行之外失败?您的测试在排队项目后立即结束,因此当测试方法结束执行时,我不确定这些线程发生了什么。您是否尝试过使用WaitHandle等待他们在测试中完成?

有关示例,请参阅http://msdn.microsoft.com/en-us/library/system.threading.waithandle.aspx