.NET ThreadPool QueueUserWorkItem同步

时间:2010-03-28 18:17:53

标签: c# .net synchronization threadpool

我正在使用ThreadPool.QueueUserWorkItem播放一些声音文件而不是挂起GUI。

它有效,但有不良副作用。

在执行QueueUserWorkItem CallBack Proc时,没有什么可以阻止它启动新线程。这会导致线程中的样本重叠。

如何使它等待已经运行的线程完成运行,然后才运行下一个请求?

编辑:private object sync = new Object(); lock (sync) { .......do sound here }

这是有效的。按顺序播放声音。

但是当一个正在播放的声音请求结束之前我继续发送声音请求时,一些样本会被多次播放。将调查。

编辑:上面是Lock Convoy @Aaronaught提到的结果吗?

8 个答案:

答案 0 :(得分: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...
        }
    }
}

其中声音是您用来表示要播放的声音的任何对象。当多个声音争夺游戏时间时,这种机制是“先到先得”。


正如另一个回应中提到的那样,小心过度的'堆积'声音,因为你将开始束缚ThreadPool。

答案 1 :(得分:6)

您可以使用带队列的单个线程来播放所有声音。

当您想要播放声音时,请将一个请求插入队列并向播放线程发出信号,告知有一个新的声音文件要播放。声音播放线程看到新请求并播放它。一旦声音完成,它会检查队列中是否还有声音,如果是,则播放下一个声音,否则等待下一个请求。

这种方法的一个可能的问题是,如果你有太多需要播放的声音,你可能会得到一个不断增长的积压,这样声音可能会持续几秒钟甚至几分钟。为避免这种情况,您可能希望对队列大小设置限制,并在有太多声音时删除一些声音。

答案 2 :(得分:2)

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

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

尽管非常简单,它可能会产生问题:

  1. 如果你同时播放很多(较长)的声音会有很多锁,很多线程池线程都会用完。
  2. 您将声音排入队列的顺序并不是他们将要播放的顺序。
  3. 在实践中,如果您经常播放大量声音或声音很长,这些问题应该是相关的。

答案 3 :(得分:2)

一个非常简单的生产者/消费者队列在这里是理想的 - 因为你只有1个生产者和1个消费者,你可以用最少的锁定来完成它。

不要在实际的lock方法/操作周围使用关键部分(Play语句),因为有些人建议,您可以很容易地得到一个lock convoy。你确实需要锁定,但你应该只在非常短的时间内完成,而不是在声音实际播放时,这在计算机时间是永恒的。

这样的事情:

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的原因只是维护对托管线程的引用,并能够提供适当的清理。

这只使用一个线程和一个锁,所以你永远不会有锁定护卫队,并且永远不会同时播放声音。

如果您在理解此问题或需要更多详细信息时遇到任何问题,请查看Threading in C#并转到“生产者/消费者队列”部分。

答案 4 :(得分:1)

另一个选择,如果你能够(主要)简化假设,即在第一个声音仍在播放时播放第二个声音的任何尝试只是忽略,则使用单个事件:< / p>

private AutoResetEvent playEvent = new AutoResetEvent(true);

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

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

答案 5 :(得分: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 */
}

答案 6 :(得分:0)

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

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

这允许我让X队列共享Y个线程(即不浪费线程),并且仍然可以获得非常好的异步操作。我将它用于comples UI交易应用程序 - 与中央服务集群(即多个服务)通信的X windows(6,8),它们都使用异步队列来回移动项目(好吧,主要是朝向UI) )。

你需要注意的一件事 - 已经说过了 - 如果你的队列超负荷,它就会倒退。该怎么做取决于你的。我有一个ping / pong消息,定期排队到服务(从窗口),如果没有及时返回,窗口将变为灰色标记“我是陈旧的”,直到它赶上。

答案 7 :(得分:0)

微软的新TPL数据流库对于这类事情来说可能是一个很好的解决方案。在这里查看视频 - 演示的第一个代码示例非常符合您的要求。

http://channel9.msdn.com/posts/TPL-Dataflow-Tour