Синхронизация .NET ThreadPool QueueUserWorkItem - PullRequest
9 голосов
/ 28 марта 2010

Я использую ThreadPool.QueueUserWorkItem для воспроизведения некоторых звуковых файлов и не зависаю при этом в графическом интерфейсе.

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

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

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

РЕДАКТИРОВАТЬ: private object sync = new Object(); lock (sync) { .......do sound here }

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

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

РЕДАКТИРОВАТЬ: вышеупомянутый результат Lock Convoy @Aaronaught упоминается?

Ответы [ 8 ]

8 голосов
/ 28 марта 2010

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

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.

6 голосов
/ 28 марта 2010

Вы можете использовать один поток с очередью для воспроизведения всех звуков.

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

Одна из возможных проблем этого метода заключается в том, что если у вас слишком много звуков, которые нужно воспроизвести, вы можете получить постоянно растущее отставание, так что звуки могут появиться на несколько секунд или даже минут позже. Чтобы избежать этого, вы можете установить ограничение на размер очереди и пропустить несколько звуков, если их слишком много.

2 голосов
/ 28 марта 2010

В этом случае идеально подойдет очень простая очередь производителя / потребителя - поскольку у вас есть только 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 # и перейдите к разделу «Очередь производителя / потребителя».

2 голосов
/ 28 марта 2010

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

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

Несмотря на то, что он очень прост, он может привести к проблемам:

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

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

1 голос
/ 29 марта 2010

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

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 голосов
/ 02 октября 2012

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

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

0 голосов
/ 28 марта 2010

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

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

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

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

0 голосов
/ 28 марта 2010

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

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