waveOutWrite 和 waveOutGetPosition 死锁的问题

Posted

技术标签:

【中文标题】waveOutWrite 和 waveOutGetPosition 死锁的问题【英文标题】:Problem with waveOutWrite and waveOutGetPosition deadlock 【发布时间】:2010-03-16 05:43:51 【问题描述】:

我正在开发一个应用程序,该应用程序使用来自winmm.dllwaveOut... API 连续播放音频。该应用程序使用“跳跃式”缓冲区,它们基本上是一堆样本数组,您可以将它们转储到音频队列中。 Windows 按顺序无缝播放它们,并且随着每个缓冲区的完成,Windows 调用一个回调函数。在这个函数中,我将下一组样本加载到缓冲区中,然后处理它们,然后将缓冲区转储回音频队列。这样,音频就可以无限播放了。

出于动画目的,我正在尝试将waveOutGetPosition 合并到应用程序中(因为“缓冲区完成”回调非常不规则,足以导致生涩的动画)。 waveOutGetPosition 返回播放的当前位置,所以是超精确的。

问题是在我的应用程序中,对waveOutGetPosition 的调用最终会导致应用程序锁定 - 声音停止并且调用永远不会返回。我把事情归结为一个简单的应用程序来演示这个问题。您可以在此处运行该应用程序:

http://www.musigenesis.com/SO/waveOut%20demo.exe

如果您只是一遍又一遍地听到一点点钢琴声,它就起作用了。它只是为了证明问题。这个项目的源代码在这里(所有的肉都在 LeapFrogPlayer.cs 中):

http://www.musigenesis.com/SO/WaveOutDemo.zip

第一个按钮以跳跃模式运行应用程序,而不调用waveOutGetPosition。如果单击此按钮,应用程序将永远播放而不会中断(X 按钮将关闭它并关闭它)。第二个按钮启动跳跃器并启动一个表单计时器,该计时器调用waveOutGetPosition 并显示当前位置。单击此按钮,应用程序将运行一会儿,然后锁定。在我的笔记本电脑上,它通常会在 15-30 秒内锁定;最多需要一分钟。

我不知道如何解决这个问题,因此非常欢迎任何帮助或建议。我发现关于这个问题的帖子很少,但似乎存在潜在的死锁,无论是多次调用waveOutGetPosition,还是同时调用该调用和waveOutWrite。可能是我调用的太频繁了,系统无法处理。

编辑:忘了说,我在 Windows Vista 上运行它。这在其他操作系统上可能根本不会发生。

编辑 2:我在网上几乎没有找到关于这个问题的信息,除了这些(未答复的)帖子:

http://social.msdn.microsoft.com/Forums/en-US/windowsgeneraldevelopmentissues/thread/c6a1e80e-4a18-47e7-af11-56a89f638ad7

编辑 3:嗯,我现在可以随意重现这个问题了。如果我在waveOutWrite 之后立即调用waveOutGetPosition(在下面的代码行中),应用程序每次都会挂起。它还以一种特别糟糕的方式挂起 - 它似乎将我的整个操作系统锁定了一段时间,而不仅仅是应用程序本身。因此,如果waveOutGetPositionwaveOutWrite 发生在几乎相同的时间,而不是字面意义上的同时发生,那么它似乎会死锁,这可能解释了为什么锁对我不起作用。是的。

【问题讨论】:

【参考方案1】:

它在 mmsys API 代码中死锁。当主线程忙于执行 waveOutWrite() 时,在回调死锁中调用 waveOutGetPosition()。它是可以修复的,你需要一个锁,这样这两个函数就不能同时执行。将此字段添加到 LeapFrogPlayer:

    private object mLocker = new object();

并在 GetElapsedMilliseconds() 中使用它:

        if (!noAPIcall)
        
          lock (mLocker) 
            ret = WaveOutX.waveOutGetPosition(_hWaveOut, ref _timestruct,
                _timestructsize);
          
        

和 HandleWaveCallback():

        // play the next buffer
        lock (mLocker) 
          int ret = WaveOutX.waveOutWrite(_hWaveOut, ref _header[_currentBuffer],
              Marshal.SizeOf(_header[_currentBuffer]));
          if (ret != WaveOutX.MMSYSERR_NOERROR) 
            throw new Exception("error writing audio");
          
        

这可能有副作用,不过我没有注意到。看看NAudio project.

下次创建项目的可上传 .zip 时,请使用 Build + Clean。

【讨论】:

我试过了,但它仍然被锁定。我还将尝试锁定对waveOutPrepareHeader 的调用。为什么需要构建 + 清理?编译项目是否困难? 在我的机器上工作。使用 Debug + Break All、Debug + Windows + Threads 查看线程卡住的位置。没有人喜欢从不受信任的 Internet URL 下载带有 .exe 文件的大容量文件。 如果有的话,这种修改会使问题变得更糟。现在,它似乎锁定得更快了;现在大约有一半的时间在第一次调用waveOutGetPosition 时被锁定。这可以在您的计算机上使用吗? 你运行的是 Vista 还是其他的? Windows 7。我对死锁问题进行了重现。【参考方案2】:

我经常使用NAudio 和查询WaveOut.GetPosition(),并且在使用Callback 策略时也经常看到死锁。这本质上与 OP 遇到的问题相同,因此我认为此解决方案可能对其他人有所帮助。

我尝试使用基于窗口的策略(如答案中所述),但当大量消息通过消息队列推送时,音频会卡顿。所以我切换到Callback 策略。然后我开始陷入僵局。

我正在以 60 fps 的速度查询音频位置以同步动画,因此我经常遇到死锁(平均运行大约 20 秒)。 注意: 我确信我可以减少调用 API 的次数,但这不是我的重点!

似乎winmm.dll 调用都在内部锁定在同一个对象/句柄上。如果这个假设成立,那么我几乎可以肯定 NAudio 会陷入僵局。这是有两个线程的场景:A(UI 线程);和Bwinmm.dll 中的回调线程)和两个锁waveOutLock(如在 NAudio 中)和mmdll(我假设 winmm.dll 正在使用的锁):

    A -> 锁定 (waveOutLock) --- 获得 B -> 锁定 (mmdll) 用于回调 --- 已获取 B -> 回调到用户代码中 B -> 尝试锁定 (waveOutLock) -- 等待 A 释放 A -> 由于 B 等待而恢复 A -> 调用 waveOutGetPosition A -> 尝试锁定 (mmdll) -- 死锁

我的解决方案是将回调中完成的工作委托给我自己的线程,以便回调可以立即返回并释放(假设的)mmdll 锁。这似乎对我来说非常有效,因为僵局已经过去了。

对于那些感兴趣的人,我已经 forked and modified NAudio 源来包含我的更改。我使用了线程池,音频偶尔会有点噼啪声。这可能是由于线程池线程管理,所以可能有性能更好的解决方案。

【讨论】:

您可能希望对该解决方案进行大量测试并确保其 100% 正常工作。在我的实际应用程序中,需要调用waveOutGetPosition 的工作是在单独的线程上完成的。它工作得很好,但它仍然偶尔会死锁 - 比我在这里发布的演示应用程序要少得多,但经常足以让我非常担心。在我当前使用 wndProc 方法的版本中,播放引擎是 100% 可靠的;我让它一次运行了好几天,我还没有看到一个死锁实例。但是使用回调它很少会运行... ...在锁定之前连续超过一两个小时,并且它偶尔会在几秒钟或几分钟后锁定。因此,请确保您进行了一些真正长期运行的测试,以便您知道自己拥有它。 顺便说一句,在使用 wndProc 方法时,我从未听说过像您所描述的那样的口吃。发生这种情况时,您的程序和系统中发生了什么? 感谢@MusiGenesis。在与 NAudio 的作者讨论后,我最终分叉并修改了 WaveOutEvent 以返回 GetPosition,这最终是我真正需要的。他在头部做了同样的改变,最终应该可以在官方版本中使用。他坚决反对使用函数回调模式。至于应用程序在口吃期间所做的事情:这是一个复杂的 WPF 布局更改,其中一些肯定不是最佳的组件加载。每次都会出现这种卡顿,因为缓冲区没有被新数据填充,因此会自行重放。【参考方案3】:

解决方案非常简单(感谢 Larry Osterman):将回调替换为 WndProc。

waveOutOpen 方法可以采用委托(用于回调)或窗口句柄。我使用的是委托方法,这显然天生就容易死锁(很有意义,尤其是在托管代码中)。我能够简单地让我的播放器类从Control 继承并覆盖WndProc 方法,并在此方法中执行与回调中相同的操作。现在我可以永远拨打waveOutGetPosition 并且它永远不会锁定。

【讨论】:

以上是关于waveOutWrite 和 waveOutGetPosition 死锁的问题的主要内容,如果未能解决你的问题,请参考以下文章

在 Windows 上播放任意声音?

Mac OS X 声音输出

.NET EventWaitHandle 慢

& 和 && 区别和联系,| 和 || 区别和联系

第三十一节:扫盲并发和并行同步和异步进程和线程阻塞和非阻塞响应和吞吐等

shell中$()和 ` `${}${!}${#}$[] 和$(()),[ ] 和(( ))和 [[ ]]