.Чистая синхронизации метод queueuserworkitem класса ThreadPool

Я использую ThreadPool.QueueUserWorkItem, чтобы воспроизвести некоторые звуковые файлы и не повесить GUI при этом.

это работает, но имеет нежелательный побочный эффект.

пока выполняется обратный вызов QueueUserWorkItem Proc, ничто не мешает ему запустить новый поток. Это приводит к тому, что образцы в потоках перекрываются.

Как я могу сделать так, чтобы он ждал, пока уже запущенный поток завершит работу, и только затем запустите следующая просьба?

изменить: private object sync = new Object(); lock (sync) { .......do sound here }

это работает. играет в звуки по порядку.

но некоторые образцы воспроизводятся более одного раза, когда я продолжаю отправлять звуковые запросы до окончания воспроизведения. будет расследовать.

EDIT: является ли вышеуказанное результатом упомянутого Lock Convoy @Aaronaught?

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 производитель и 1 потребитель, вы можете сделать это с минимальной блокировкой.

не используйте критический раздел (lock заявление) вокруг фактического Play метод / операция как предлагают некоторые люди, Вы можете очень легко закончить с замок конвой. Вам нужно заблокировать, но вы должны делать это только в течение очень коротких периодов времени, а не когда звук на самом деле игра, которая является вечностью в компьютерном времени.

что-то вроде этого:

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 в C# и перейдите в раздел "очередь производителя / потребителя".


самый простой код, который вы могли бы написать, будет следующим:

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

хотя очень простой, он pontentially может принести проблемы:

  1. если вы играете много (более длинных) звуков одновременно, будет много замков и много потоков threadpool.
  2. заказ вам в очередь звуки не necesesarily порядке, в котором они будут воспроизводиться.

на практике эти проблемы должны быть актуальны, если вы играть много звуков часто или если звуки очень длинные.


другой вариант, если вы можете сделать (основное) упрощающее предположение, что любые попытки воспроизвести второй звук, пока первый все еще играет, будут просто игнорировать - использовать одно событие:

private AutoResetEvent playEvent = new AutoResetEvent(true);

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

Это очень просто, с очевидным недостатком, что он просто отбросит "дополнительные" звуки вместо того, чтобы выстраивать их в очередь. Но в этом случае это может быть именно то, что вы хотите, и мы можем использовать пул потоков, потому что эта функция вернется мгновенно, если звук уже играет. Это в основном "без блокировки"."


в соответствии с вашим редактированием создайте свой поток следующим образом:

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 */
}

использование ThreadPool не является ошибкой. Ошибка заключается в очереди каждого звука в качестве рабочего элемента. Естественно, пул потоков запустит больше потоков. Это то, что он должен делать.

создайте свою собственную очередь. У меня есть один (AsyncActionQueue). Он выстраивает элементы в очередь, и когда у него есть элемент, он запустит WorkItem ThreadPool-не по одному на элемент, один (если он уже не поставлен в очередь и не завершен). Обратный вызов в основном unqeueues элементы и обрабатывает их.

Это позволяет мне у X очередей есть y потоков (т. е. не ненужные потоки) и все еще очень хорошие асинхронные операции. Я использую это для торгового приложения comples UI-X windows (6, 8), взаимодействующего с центральным кластером служб (т. е. рядом служб), и все они используют асинхронные очереди для перемещения элементов туда и обратно (ну, в основном вперед к пользовательскому интерфейсу).

одна вещь, которую вам нужно знать - и это уже было сказано, - это то, что если вы перегрузите свою очередь, она упадет. Что делать тогда зависит на ваш. У меня есть сообщение ping/pong, которое регулярно попадает в очередь на службу (из окна), и если не возвращается вовремя, окно становится серым, отмечая "я черствый", пока он не догонит.


новая библиотека потоков данных Microsoft TPL может быть хорошим решением для такого рода вещей. Посмотрите видео здесь - первый продемонстрированный пример кода соответствует вашим требованиям в значительной степени точно.

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