为什么此 ThreadPool.QueueUserWorkItem 代码没有通过我的测试?

发布于 2025-01-08 05:06:28 字数 1431 浏览 1 评论 0原文

有问题的代码

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 描述判断,我相信我的单元测试应该失败:

对方法进行排队以供执行。当线程池线程变得可用时,该方法将执行。

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

这里发生了什么事?

The code in question

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));
        }
    });
}

and my test:

[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!");
}

I believe my unit test should fail, judging by the MSDN description of ThreadPool.QueueUserWorkItem:

Queues a method for execution. The method executes when a thread pool thread becomes available.

The default ThreadPool size is 512, so two threads should immediately be available to process the StartPlaying call. I believe my code should fail since I haven't provided any safeguards from race conditions in which both threads can access the same resource.

What's happening here?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

如此安好 2025-01-15 05:06:28

因为仅当有项目要播放时调用 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(对象发送者,TypedAudioFileEventArgs e)
    <代码>{
    如果(!_once)
    _once = true;
    其他
    Assert.Fail("不应多次调用!");
    }

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

Because the StartedPlaying event is only raised if StartPlaying is called when there are items to play.

_playlist.Dequeue(); dequeues the file you enqueue. Therefore the second time you get to while (_playlist.Count > 0) it will immediately fail, passing the second call to StartPlaying straight through without raising the event.

Also, as Bruno Silva points out, the thread spawned by the second call to StartPlaying may not have a chance to execute anything before the test exits.

For what it's worth, there are about a million at least 2 threading mistakes in this code also:

// 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();
}

If you want to properly unit test, you need to define preconditions, expectations, actions, and postconditions:

  • Preconditions: you have an initialized TypedAudioFilePlayer with one file queued:

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

  • Expectations: The StartedPlaying event will be raised only once if StartPlaying is called twice

    target.StartedPlaying += StartedPlaying_Once;

  • Actions: The StartPlaying method will be called twice:

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

  • Postconditions: The StartedPlaying event was only raised once:

    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!");
    }

Now, your test succeeds. That's not good in this case, because of what I explain above. You need to get your test to a failing state by eliminating the queue bug and race condition, then work on making the test pass the right way.

梦断已成空 2025-01-15 05:06:28

您似乎正在使用两个线程之间的共享资源,因此当第二次调用 Play 时,once 可能不会设置为 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!");
}
}

It seems that you are working with a shared resource between two threads so once might not be set to true when the Play is called for the second time. You can use a lock to allow executing part of your code by one thread at a time :

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!");
}
}
波浪屿的海角声 2025-01-15 05:06:28

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

请参阅 http://msdn.microsoft.com/en-us/ Library/system.threading.waithandle.aspx 为例。

Could those be failing outside the text execution? Your test ends right after you queue the item so I'm not sure what is happening to those threads when the test method ends its execution. Have you tried using WaitHandle to wait for the them to finish inside the test?

See http://msdn.microsoft.com/en-us/library/system.threading.waithandle.aspx for an example.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文