Я нанимаю ThreadPool. QueueUserWorkItem для проигрывания некоторых звуковых файлов и не зависающий GUI при выполнении так.
Это работает, но имеет нежелательный побочный эффект.
В то время как QueueUserWorkItem CallBack Proc выполняется нет ничего, чтобы мешать ему начать новую дискуссию. Это заставляет образцы в потоках накладываться.
Как я могу сделать его так, чтобы это ожидало уже рабочего потока, чтобы закончить работать и только затем выполнять следующий запрос?
Править: private object sync = new Object(); lock (sync) { .......do sound here }
это работает. игры в звуках в порядке.
но некоторые образцы становятся играемыми несколько раз, когда я продолжаю отправлять звуковые запросы перед тем, играемым концы. займется расследованиями.
Править: выше результата Конвоя Блокировки @Aaronaught упомянут?
Это классическая проблема синхронизации потоков, когда у вас есть несколько клиентов, которые все хотят использовать один и тот же ресурс, и им необходимо контролировать доступ к нему. В этом конкретном случае звуковая система готова воспроизводить более одного звука одновременно (и это часто желательно), но, поскольку вы не хотите такого поведения, вы можете использовать стандартную блокировку для доступа к звуковой системе. :
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...
}
}
}
где Звук - это любой объект, который вы используете для представления воспроизводимого звука. Этот механизм работает по принципу «первым пришел - первым обслужен», когда несколько звуков соперничают за время воспроизведения.
Как упоминалось в другом ответе, будьте осторожны с чрезмерным «скоплением» звуков, так как вы начнете связывать пул потоков.
Здесь идеально подошла бы очень простая очередь производитель / потребитель - поскольку у вас есть только 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);
}
}));
}
Хотя он очень прост, но потенциально может привести к проблемам:
на практике эти проблемы могут возникнуть только в том случае, если вы часто воспроизводите много звуков или если звуки очень длинные.
Другой вариант, если вы можете сделать (главное) упрощающее предположение, что любые попытки воспроизвести второй звук, пока играет первый, будут просто проигнорированы, это использовать одно событие:
private AutoResetEvent playEvent = new AutoResetEvent(true);
public void Play(Sound sound)
{
ThreadPool.QueueUserWorkItem(s =>
{
if (playEvent.WaitOne(0))
{
// Play the sound here
playEvent.Set();
}
});
}
Этот вариант очень простой, с очевидным недостатком, что он будет просто отбрасывать "лишние" звуки вместо того, чтобы ставить их в очередь. Но в данном случае это может быть именно то, что вам нужно, и мы получим возможность использовать пул потоков, потому что эта функция вернется мгновенно, если звук уже играет. По сути, это "lock-free"
.В соответствии с вашей правкой создайте поток следующим образом:
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). Он ставит элементы в очередь, и когда у него есть элемент, он запускает рабочий элемент ThreadPool - не по одному на элемент, ОДИН (если он уже не поставлен в очередь и не завершен). Обратный вызов в основном извлекает элементы из очереди и обрабатывает их.
Это позволяет мне иметь X очередей, совместно использующих Y потоков (т. Е. Не тратить лишние потоки), и при этом получать очень хорошие асинхронные операции. Я использую это для комплексного торгового приложения пользовательского интерфейса - X windows (6, 8) взаимодействует с центральным сервисным кластером (т.е. рядом сервисов), и все они используют асинхронные очереди для перемещения элементов назад и вперед (ну, в основном, в сторону пользовательского интерфейса). ).
Одна вещь, о которой вам НЕОБХОДИМО знать - и это уже было сказано, - это то, что если вы перегрузите свою очередь, она вернется в исходное состояние. Что делать дальше, зависит от вашего. У меня есть сообщение ping / pong, которое регулярно ставится в очередь службы (из окна), и, если оно не возвращается вовремя, окно становится серым с пометкой «Я устарел», пока оно не догонит.