Многопоточность: я делаю это неправильно?

23

Я работаю над приложением, которое играет музыку.

Во время воспроизведения часто вещи должны происходить в отдельных потоках, потому что они должны происходить одновременно. Например, ноты аккорда должны быть услышаны вместе, поэтому каждой из них назначается собственный поток для воспроизведения. (Изменить, чтобы уточнить: вызов note.play()останавливает поток, пока не закончится воспроизведение ноты, и поэтому мне нужно три отдельные темы для прослушивания трех нот одновременно.)

Такое поведение создает много потоков во время воспроизведения музыкального произведения.

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

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

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            runOnNewThread( func(){ note.play(); } );
}

Таким образом, предполагая, что у прогрессии есть 4 аккорда, и мы играем это дважды, чем мы открываем 3 notes * 4 chords * 2 times= 24 потока. И это только для того, чтобы играть в нее один раз.

На самом деле, это прекрасно работает на практике. Я не замечаю заметных задержек или ошибок, связанных с этим.

Но я хотел спросить, является ли это правильной практикой или я делаю что-то в корне неправильно. Разумно ли создавать столько потоков каждый раз, когда пользователь нажимает кнопку? Если нет, как я могу сделать это по-другому?

Авив Кон
источник
14
Возможно, вам стоит подумать о микшировании звука? Я не знаю , какие рамки вы используете , но вот пример: wiki.libsdl.org/SDL_MixAudioFormat Или, вы можете использовать каналы: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind
5
Is it reasonable to create so many threads...зависит от потоковой модели языка. Потоки, используемые для параллелизма, часто обрабатываются на уровне ОС, поэтому ОС может сопоставлять их с несколькими ядрами. Такие потоки дороги в создании и переключении между ними. Потоки для параллелизма (чередование двух задач, не обязательно выполняющих обе одновременно) могут быть реализованы на уровне языка / виртуальной машины и могут быть чрезвычайно «дешевыми» для создания и переключения между ними, так что вы можете, скажем, общаться с 10 сетевыми сокетами более или менее одновременно, но вы не обязательно получите больше пропускной способности процессора таким образом.
Довал
3
Кроме того, остальные, безусловно, правы в том, что потоки - это неправильный способ обрабатывать несколько звуков одновременно.
Довал
3
Вы хорошо знакомы с тем, как работают звуковые волны? Как правило, вы создаете аккорд, объединяя значения двух звуковых волн (заданных на одном и том же битрейте) в новую звуковую волну. Сложные волны могут быть построены из простых; для воспроизведения песни вам нужен только один сигнал.
KChaloux
Поскольку вы говорите, что note.play () не асинхронный, поток для каждого note.play () является подходом к одновременному воспроизведению нескольких нот. ЕСЛИ ... вы можете объединить эти ноты в одну, которую вы затем играете в одном потоке. Если это невозможно, то при вашем подходе вам придется использовать какой-то механизм, чтобы обеспечить их синхронизацию
pnizzle

Ответы:

46

Одно из предположений, которое вы делаете, может быть неверным: вам требуется (помимо прочего), чтобы ваши потоки выполнялись одновременно. Может работать на 3, но в какой-то момент система должна будет расставить приоритеты, какие потоки запускать первыми, а какие ждать.

Ваша реализация в конечном итоге будет зависеть от вашего API, но большинство современных API позволит вам заранее сказать, во что вы хотите играть, и позаботиться о сроках и очереди. Если бы вы сами программировали такой API, игнорируя какой-либо существующий системный API (зачем вам ?!), очередь событий, смешивающая ваши заметки и воспроизводящая их из одного потока, выглядит как лучший подход, чем модель потока на одну заметку.

ptyx
источник
36
Или сформулировать это по-другому - система не будет и не может гарантировать порядок, последовательность или продолжительность любого потока после его запуска.
Джеймс Андерсон
2
@JamesAnderson, за исключением случаев, когда кто-то предпримет огромные усилия по синхронизации, что в итоге приведет к тому, что в конце концов он снова будет почти пьяным.
Марк
Под «API» вы подразумеваете аудио библиотеку, которую я использую?
Авив Кон
1
@Prog Да. Я почти уверен, что в нем есть что-то более удобное, чем note.play ()
ptyx
26

Не полагайтесь на потоки, выполняющиеся в режиме lockstep. Каждая операционная система, о которой я знаю, не дает гарантии того, что потоки выполняются во времени в соответствии друг с другом. Это означает, что если процессор запускает 10 потоков, они не обязательно получают равное время в любую данную секунду. Они могут быстро потерять синхронизацию или идеально синхронизироваться. Такова природа потоков: ничто не гарантировано, поскольку поведение их выполнения недетерминировано .

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

Давайте предположим, что у вас есть, например, три потока, производящие заметки. Я бы синхронизировал их в единой структуре данных, где они все могли бы поместить свою музыку. Другой поток (потребитель) читает объединенные заметки и воспроизводит их. Здесь может потребоваться небольшая задержка, чтобы гарантировать, что синхронизация потоков не приведет к потере заметок.

Связанное чтение: проблема производителя-потребителя .


источник
1
Спасибо за ответ. Я не уверен на 100%, что понимаю ваш ответ, но я хотел убедиться, что вы понимаете, почему мне нужно 3 потока для одновременного воспроизведения 3 нот: это потому, что колл note.play()останавливает поток до тех пор, пока не закончится воспроизведение ноты. Поэтому для того, чтобы я мог иметь play()3 ноты одновременно, мне нужно 3 разных потока для этого. Ваше решение соответствует моей ситуации?
Авив Кон
Нет, и это не было ясно из вопроса. Нет ли возможности для темы сыграть аккорд?
4

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

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            otherthread.startPlaying(note);
}

(обратите внимание на концепцию только асинхронного запуска заметки и продолжения без ожидания ее завершения)

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

Этот поток часто очень чувствителен к «истощению» ресурсов - если какая-либо фактическая обработка вывода звука блокируется дольше, чем вывод с буферизацией вашей звуковой карты, то это вызовет слышимые артефакты, такие как прерывания или щелчки. Если у вас есть 24 потока, которые напрямую выводят звук, то у вас гораздо больше шансов, что один из них в какой-то момент заикается. Это часто считается неприемлемым, поскольку люди довольно чувствительны к звуковым сбоям (гораздо больше, чем к визуальным артефактам) и замечают даже незначительное заикание.

Петерис
источник
1
ОП говорит, что API может играть только одну ноту за раз
Mooing Duck
4
@MooingDuck, если API может смешиваться, то он должен смешиваться; если ОП говорит, что API не может смешивать, то решение состоит в том, чтобы смешать в вашем коде и заставить этот другой поток выполнять my_mixed_notes_from_whole_chord_progression.play () через API.
Петерис
1

Ну да, вы делаете что-то не так.

Во-первых, создание потоков стоит дорого. Это намного больше накладных расходов, чем просто вызов функции.

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

Как мне кажется, у вас есть один основной поток, который выполняет планирование других потоков. Таким образом, основной поток проходит через музыку и запускает новые темы, когда появляется новая нота для воспроизведения. Таким образом, вместо того, чтобы позволить потокам умирать и перезапускать их, лучше поддерживать другие потоки живыми в цикле сна, где они проверяют каждые x миллисекунд (или наносекунд), есть ли новая нота для воспроизведения, и в противном случае спят. Затем основной поток не запускает новые потоки, а указывает существующему пулу потоков воспроизводить ноты. Только если в пуле недостаточно потоков, он может создавать новые потоки.

Следующим является синхронизация. Вряд ли какие-либо современные многопоточные системы действительно гарантируют, что все потоки выполняются одновременно. Если на машине запущено больше потоков и процессов, чем ядер (что в большинстве случаев имеет место), то потоки не получают 100% времени ЦП. Они должны делиться процессором. Это означает, что каждый поток получает небольшое количество процессорного времени, а затем, после того как он разделен, следующий поток получает процессор на короткое время. Система не гарантирует, что ваш поток получит то же время процессора, что и другие потоки. Это означает, что один поток может ожидать завершения другого, и поэтому может быть отложен.

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

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

Dakkaron
источник
«Можно ли воспроизвести несколько нот в одном потоке, чтобы поток подготовил все ноты, а затем дал только команду« пуск ».» - Это была и моя первая мысль. Я иногда задаюсь вопросом, не засыпает ли комментарии комментариями о многопоточности (например, programmers.stackexchange.com/questions/43321/… ) многим программистам во время проектирования. Я скептически отношусь к любому крупному, прямому процессу, который заканчивается тем, что возникает необходимость в куче потоков. Я бы посоветовал искать пристальное решение для одного потока.
user1172763
1

Прагматичный ответ заключается в том, что если он работает для вашего приложения и отвечает вашим текущим требованиям, то вы не ошибаетесь :) Однако, если вы имеете в виду «это масштабируемое, эффективное, многократно используемое решение», то ответ отрицательный. Дизайн делает много предположений о том, как будут вести себя потоки, что может быть или не быть правдой в разных ситуациях (более длинные мелодии, больше одновременных заметок, различное оборудование и т. Д.).

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

Преимущество такой конструкции состоит в том, что количество потоков не будет превышать максимального количества одновременных заметок + 1 (цикл синхронизации). Также петля синхронизации защищает вас от проскальзываний во времени, которые могут быть вызваны задержкой потока. В-третьих, темп мелодии не является фиксированным и может быть изменен путем изменения расчета времени.

Кроме того, я согласен с другими комментаторами, что эта функция note.play()- очень плохой API для работы. Любое разумное API позволит вам смешивать и планировать заметки гораздо более гибким способом. Тем не менее, иногда мы должны жить с тем, что у нас есть :)

Джон
источник
0

Мне кажется, что это простая реализация, если предположить, что это API, который вы должны использовать . Другие ответы охватывают, почему это не очень хороший API, так что я не буду говорить об этом больше, я просто предполагаю, что это то, с чем вам придется жить. Ваш подход будет использовать большое количество потоков, но на современном ПК это не должно беспокоить, пока число потоков составляет десятки.

Одна вещь, которую вы должны сделать, если это возможно (например, играть из файла вместо того, чтобы пользователь нажимал на клавиатуру), это добавить некоторую задержку. Таким образом, вы запускаете поток, он спит до определенного времени системных часов и начинает воспроизводить ноту в нужное время (обратите внимание, иногда сон может быть прерван раньше, поэтому проверяйте часы и больше спите, если это необходимо). Хотя нет гарантии, что ОС продолжит поток именно тогда, когда закончится ваш сон (если вы не используете операционную систему в реальном времени), скорее всего, это будет гораздо более точным, чем если вы просто запустите поток и начнете играть без проверки времени ,

Затем следующий шаг, который немного усложняет, но не слишком сильно, и позволит вам уменьшить вышеупомянутую задержку, - это использование пула потоков. То есть, когда нить заканчивает ноту, она не завершается, а вместо этого ожидает воспроизведения новой ноты. И когда вы запрашиваете начало воспроизведения заметки, вы сначала пытаетесь извлечь бесплатную ветку из пула и добавлять новую, только если это необходимо. Конечно, это потребует некоторого простого взаимодействия между потоками вместо вашего текущего подхода «забей и забудь».

Хайд
источник