Багатопоточність: чи я це роблю неправильно?


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...залежить від моделі нарізки мови. Нитки, які використовуються для паралелізму, часто обробляються на рівні ОС, тому ОС може зіставити їх на кілька ядер. Такі нитки дорого створюються та перемикаються між собою. Нитки для одночасності (переплетення двох завдань, не обов'язково виконання обох одночасно) можуть бути реалізовані на рівні мови / VM і можуть бути надзвичайно «дешевими» для створення та перемикання між ними, щоб ви могли, скажімо, поговорити з 10 мережевими розетками більше або менше водночас, але вам не обов’язково отримати більше пропускної здатності процесора таким чином.
Doval

3
Що ж убік, інші, безумовно, мають рацію щодо того, що потоки є неправильним способом обробки відтворення декількох звуків одночасно.
Доваль

3
Чи багато ви знайомі з тим, як працюють звукові хвилі? Як правило, ви створюєте акорд, складаючи значення двох звукових хвиль (зазначених на одному бітрейті) разом у нову звукову хвилю. Складні хвилі можна побудувати з простих; для відтворення пісні вам потрібен лише один сигнал хвилі.
KChaloux

Оскільки ви говорите, що note.play () не є асинхронним, нитка для кожного note.play (), отже, є підходом до відтворення декількох нот одночасно. БІЛЬШЕ .., ви зможете комбінувати ті ноти в одну, яку потім відтворюєте на одній нитці. Якщо це неможливо, тоді під час підходу вам доведеться використовувати якийсь механізм, щоб переконатися, що вони залишаються синхронізованими
pnizzle

Відповіді:


46

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

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


36
Або констатувати це інакше - система не буде і не може гарантувати порядок, послідовність або тривалість будь-якого потоку після запуску.
Джеймс Андерсон

2
@JamesAnderson, за винятком випадків, коли б докласти чималих зусиль у синхронізації, що в кінцевому підсумку знову виявиться майже кричущим.
Марк

Під «API» ви маєте на увазі аудіотеку, яку я використовую?
Авів Кон

1
@Prog Так. Я впевнений, що в ньому є щось зручніше, ніж note.play ()
ptyx,

26

Не покладайтеся на нитки, що виконуються в режимі блокування. Кожна операційна система, про яку я знаю, не дає гарантії того, що потоки виконуються в часі, узгоджуються між собою. Це означає, що якщо ЦП працює 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

Ну так, ви робите щось не так.

Перше, що створити нитки дорого. Це набагато більше накладних витрат, ніж просто виклик функції.

Отже, що вам слід зробити, якщо вам потрібно кілька потоків для цієї роботи, це переробити нитки.

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

Наступний - синхронізація. Навряд чи будь-які сучасні багатопотокові системи фактично гарантують, що всі потоки виконуються одночасно. Якщо на машині працює більше потоків і процесів, ніж ядер (що в основному так), то потоки не отримують 100% часу процесора. Вони мають поділитися процесором. Це означає, що кожен потік отримує невелику кількість процесорного часу, а потім після його поділу наступний потік отримує процесор на короткий час. Система не гарантує, що ваш потік отримує той самий час процесора, що й інші потоки. Це означає, що одна нитка може чекати, коли інша закінчиться, і, таким чином, буде відкладена.

Ви повинні скоріше подивитися, якщо неможливо відтворити кілька нот на одному потоці, так що потік підготує всі ноти, а потім дає лише команду "пуск".

Якщо вам потрібно зробити це з нитками, принаймні використайте нитки. Тоді вам не потрібно мати стільки ниток, скільки нот у цілому творі, а потрібно лише стільки ниток, скільки максимальна кількість відтворених нот одночасно.


"[Чи можливо] відтворити кілька нот на одному потоці, щоб нитка підготувала всі ноти, а потім лише давала команду" пуск "." - Це також була моя перша думка. Мені іноді цікаво, що обстріл коментарями про багатопотоковість (наприклад, programer.stackexchange.com/questions/43321/… ) не призводить до збиття багато програмістів під час проектування. Я скептично ставлюсь до будь-якого великого передового процесу, який закінчується необхідністю створити купу ниток. Я б радив уважно шукати рішення, що має один потік.
користувач1172763

1

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

Як альтернативу розглянемо використання циклу синхронізації та пулу потоків . Цикл синхронізації працює у власному потоці і постійно перевіряє, чи потрібно відтворювати ноту. Це робиться, порівнюючи системний час із часом початку мелодії. Час кожної ноти зазвичай можна легко обчислити з темпу мелодії та послідовності нот. Оскільки потрібно відтворити нову ноту, запозичіть нитку з пулу потоків і зателефонуйте на play()функцію ноти. Цикл синхронізації може працювати різними способами, але найпростішим є безперервний цикл з коротким сном (можливо, між акордами) для мінімізації використання процесора.

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

Як осторонь, я погоджуюся з іншими коментаторами, що функція note.play()дуже погана API для роботи. Будь-який розумний звуковий API дозволить вам змішувати та планувати ноти набагато більш гнучко. Однак, іноді нам доводиться жити з тим, що у нас є :)


0

Мені це чудово виглядає як проста реалізація, якщо припустити, що це API, який ви повинні використовувати . Інші відповіді висвітлюють, чому це не дуже хороший API, тому я не говорю про це більше, я просто припускаю, що це те, з чим вам доведеться жити. У вашому підході буде використовуватися велика кількість потоків, але на сучасному ПК це не повинно викликати занепокоєння, якщо кількість ниток становить десятки.

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

Потім наступним кроком, який трохи ускладнює речі, але не надто багато, і дозволить зменшити вищезазначені затримки, скористайтеся пулом ниток. Тобто, коли нитка закінчує ноту, вона не виходить, а натомість чекає відтворення нової ноти. І коли ви запитаєте почати програвати ноту, спершу спробуйте взяти безкоштовну нитку з пулу та додати нову нитку лише за потреби. Звичайно, це потребуватиме простого спілкування між потоками, замість вашого поточного підходу до забуття.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.