.NET线程池QueueUserWorkItem同步化

9
我正在使用ThreadPool.QueueUserWorkItem播放一些声音文件,同时不会在执行此操作时挂起GUI。
它可以正常工作,但有一个不良副作用。
当执行QueueUserWorkItem回调过程时,没有任何东西可以阻止它启动新线程。这会导致线程中的样本重叠。
我该如何使其等待已经运行的线程完成后再运行下一个请求?
编辑:private object sync = new Object(); lock (sync) { .......do sound here } 这个方法有效。按顺序播放声音。
但是,当我在正在播放的声音结束之前不断发送声音请求时,某些样本会被重复播放。我将进行调查。
编辑:上述是否是@Aaronaught提到的锁竞争的结果?

可能是重复问题?:https://dev59.com/8UrSa4cB1Zd3GeqPZdG-#1907501 - Oliver
8个回答

8

这是一个经典的线程同步问题,当你有多个客户端需要使用相同的资源并且需要控制它们的访问时。在这种情况下,声音系统愿意同时播放多个声音(通常是可取的),但是由于你不想要这种行为,所以你可以使用标准的锁定来控制对声音系统的访问:

public static class SequentialSoundPlayer
{
    private static Object _soundLock = new object();

    public static void PlaySound(Sound sound)
    {
        ThreadPool.QueueUserWorkItem(AsyncPlaySound, sound);
    }

    private static void AsyncPlaySound(Object state)
    {
        lock (_soundLock)
        {
            Sound sound = (Sound) state;
            //Execute your sound playing here...
        }
    }
}

这里的“Sound”指代用于表示要播放的声音的任何对象。当多个声音争夺播放时间时,此机制采用“先到先得”的原则。


正如其他回答中提到的那样,要注意避免声音过度“堆积”,否则会占用线程池。


通过将需要播放的声音放入一个单独的AsyncProc中,并使用Lock(..),我的问题已得到解决。 - iTEgg

6
您可以使用一个带有队列的单个线程来播放所有声音。当您想要播放声音时,将请求插入队列并向正在播放的线程发出信号,表示有新的声音文件需要播放。声音播放线程看到新请求后会播放它。一旦声音播放完成,它会检查队列中是否还有更多的声音,如果有,则播放下一个声音,否则等待下一个请求。这种方法可能存在的一个问题是,如果您需要播放太多的声音,您可能会得到一个越来越长的积压,以至于声音可能会延迟几秒甚至几分钟。为了避免这种情况,您可能希望限制队列的大小,并在声音过多时丢弃一些声音。

4
换言之,不要使用ThreadPool,而是自己进行线程处理。ThreadPool通过设计利用多个线程,在较短的时间内完成所有排队的工作项。 - Erv Walter
如果您感兴趣,可以查看http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!1780.entry中的F#策略。 - Brian
实际上,您可以使用线程池 - 拥有一个单独的队列,当有项目进来且没有等待处理的进程时,将会排队一个工作项。工作项具有回调函数,将处理所有等待的项目。每个队列只有在没有任何排队时才会排队。我在实时交易应用程序中大量使用它,效果很好 - 并且避免了手动管理线程。但是将每个请求都倾倒到TreadPool中是不好的 ;) - TomTom

2

你可以编写的最简单的代码如下:

private object playSoundSync = new object();
public void PlaySound(Sound someSound)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(delegate
    {
      lock (this.playSoundSync)
      {
        PlaySound(someSound);
      }
    }));
}

虽然非常简单,但潜在地可能会出现问题:

  1. 如果同时播放很多(较长的)声音,将会有很多锁定和大量线程池线程被使用。
  2. 您排队的声音顺序不一定是它们将被播放回来的顺序。

实际上,只有在频繁播放大量声音或声音非常长时,这些问题才应该是相关的。


锁定听起来很有前途。是否有办法在以下情况下使用它?ThreadPool.QueueUserWorkItem(new WaitCallback(FireResultProc), fireResult); - iTEgg
这个可以运行,但不能确保声音以正确的顺序播放,这可能很重要。 - dan
1
它不仅不能确保声音按正确顺序播放,而且如果许多声音快速连续播放,这将创建巨大的锁争用 - 如果声音足够长,甚至可能耗尽线程池。 - Aaronaught

2
一个非常简单的生产者/消费者队列在这里是理想的 - 因为你只有一个生产者和一个消费者,所以可以使用最小的锁定来完成它。 不要像一些人建议的那样,在实际的Play方法/操作周围使用临界区(lock语句),你很容易就会遇到锁定车队问题。你确实需要锁定,但你应该只在非常短的时间内进行锁定,而不是在计算机时间中永远播放声音的时候。
像这样:
public class SoundPlayer : IDisposable
{
    private int maxSize;
    private Queue<Sound> sounds = new Queue<Sound>(maxSize);
    private object sync = new Object();
    private Thread playThread;
    private bool isTerminated;

    public SoundPlayer(int maxSize)
    {
        if (maxSize < 1)
            throw new ArgumentOutOfRangeException("maxSize", maxSize,
                "Value must be > 1.");
        this.maxSize = maxSize;
        this.sounds = new Queue<Sound>();
        this.playThread = new Thread(new ThreadStart(ThreadPlay));
        this.playThread.Start();
    }

    public void Dispose()
    {
        isTerminated = true;
        lock (sync)
        {
            Monitor.PulseAll(sync);
        }
        playThread.Join();
    }

    public void Play(Sound sound)
    {
        lock (sync)
        {
            if (sounds.Count == maxSize)
            {
                return;   // Or throw exception, or block
            }
            sounds.Enqueue(sound);
            Monitor.PulseAll(sync);
        }
    }

    private void PlayInternal(Sound sound)
    {
        // Actually play the sound here
    }

    private void ThreadPlay()
    {
        while (true)
        {
            lock (sync)
            {
                while (!isTerminated && (sounds.Count == 0))
                    Monitor.Wait(sync);
                if (isTerminated)
                {
                    return;
                }
                Sound sound = sounds.Dequeue();
                Play(sound);
            }
        }
    }
}

这将允许您通过将maxSize设置为一些合理的限制(如5)来限制播放声音的数量,此后它将简单地丢弃新的请求。我使用Thread而不是ThreadPool的原因仅仅是为了维护对托管线程的引用并能够提供适当的清理。

这只使用一个线程和一个锁,因此您永远不会有锁竞争,并且不会同时播放声音。

如果您对此有任何疑问或需要更多详细信息,请查看C#中的线程处理,并前往“生产者/消费者队列”部分。


1

另一个选择是,如果你能够做出(重要的)简化假设,即在第一个声音仍在播放时尝试播放第二个声音将被忽略,那么可以使用单个事件:

private AutoResetEvent playEvent = new AutoResetEvent(true);

public void Play(Sound sound)
{
    ThreadPool.QueueUserWorkItem(s =>
    {
        if (playEvent.WaitOne(0))
        {
            // Play the sound here
            playEvent.Set();
        }
    });
}

这个很容易,但明显的劣势是它会直接丢弃“额外”的声音而不是将它们排队。但在这种情况下,这可能正是您想要的,而且我们可以使用线程池,因为如果一个声音正在播放,这个函数将立即返回。基本上是“无锁的”。


0
根据您的编辑,可以像这样创建您的线程:
MySounds sounds = new MySounds(...);
Thread th = new Thread(this.threadMethod, sounds);
th.Start();

这将是您的线程入口点。

private void threadMethod (object obj)
{
    MySounds sounds = obj as MySounds;
    if (sounds == null) { /* do something */ }

    /* play your sounds */
}

Thread t = new Thread(this.FireAttackProc, fireResult); 出现错误。无法将 'NietzscheBattleships.FireResult' 转换为 'int'。 - iTEgg

0

使用ThreadPool并不是错误,错误在于将每个声音都排队作为工作项。自然地,线程池会启动更多的线程。这就是它应该做的。

建立自己的队列。我有一个(AsyncActionQueue)。它将项目排队,当它有一个项目时,它将启动一个ThreadPool WorkItem - 不是每个项目一个,而是一个(除非已经排队且未完成)。回调基本上取消排队的项目并处理它们。

这使我能够让X个队列共享Y个线程(即不浪费线程),并仍然获得非常好的异步操作。我将其用于复杂的UI交易应用程序 - X个窗口(6,8)与中央服务集群(即多个服务)通信,它们都使用异步队列来移动项目来回(大多数是向UI方向)。

你需要注意的一件事 - 已经说过了 - 如果你超载了队列,它会退回。然后要做什么取决于你。我有一个定期排队到服务的ping/pong消息(从窗口),如果没有及时返回,则窗口变灰标记“我已过期”,直到它赶上。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接