Wielowątkowość: czy robię to źle?


23

Pracuję nad aplikacją do odtwarzania muzyki.

Podczas odtwarzania często rzeczy muszą się dziać na osobnych wątkach, ponieważ muszą zdarzyć się jednocześnie. Na przykład, nuty potrzeby akordów do bycia wysłuchanym razem, więc każdy ma przydzielony własny wątek, aby być odtwarzane w (Edit wyjaśnienie. Wywołujący note.play()zawiesza wątek aż notatka jest gotowe do gry, a to dlatego, że potrzebne są trzy oddzielne wątki, aby jednocześnie usłyszeć trzy nuty).

Ten rodzaj zachowania tworzy wiele wątków podczas odtwarzania utworu muzycznego.

Rozważmy na przykład utwór muzyczny z krótką melodią i krótką sekwencją akordów. Całą melodię można odtwarzać w jednym wątku, ale progresja wymaga trzech wątków, ponieważ każdy z jej akordów zawiera trzy nuty.

Tak więc pseudo-kod do odtwarzania progresji wygląda następująco:

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

Zakładając, że progresja ma 4 akordy i gramy dwa razy, niż otwieramy 3 notes * 4 chords * 2 times= 24 wątki. A to tylko raz.

Właściwie to działa dobrze w praktyce. Nie zauważam żadnych zauważalnych opóźnień ani wynikających z tego błędów.

Ale chciałem zapytać, czy to jest poprawna praktyka, czy też robię coś zasadniczo nie tak. Czy rozsądne jest tworzenie tak wielu wątków za każdym razem, gdy użytkownik naciska przycisk? Jeśli nie, jak mogę to zrobić inaczej?


14
Może zamiast tego powinieneś rozważyć miksowanie dźwięku? Nie wiem, jakiego frameworku używasz, ale oto przykład: wiki.libsdl.org/SDL_MixAudioFormat Lub możesz skorzystać z kanałów: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind

5
Is it reasonable to create so many threads...zależy od modelu wątków języka. Wątki używane do równoległości są często obsługiwane na poziomie systemu operacyjnego, aby system operacyjny mógł zmapować je na wiele rdzeni. Takie wątki są kosztowne w tworzeniu i przełączaniu między nimi. Wątki dla współbieżności (przeplatanie dwóch zadań, niekoniecznie wykonywanie obu jednocześnie) mogą być implementowane na poziomie języka / maszyny wirtualnej i mogą być bardzo „tanie” w produkcji i przełączaniu się między nimi, dzięki czemu można, powiedzmy, rozmawiać z 10 gniazdami sieciowymi mniej więcej jednocześnie, ale niekoniecznie uzyskasz w ten sposób większą przepustowość procesora.
Doval

3
Poza tym inni zdecydowanie mają rację, że wątki są niewłaściwym sposobem obsługi wielu dźwięków jednocześnie.
Doval

3
Czy wiesz dużo, jak działają fale dźwiękowe? Zazwyczaj tworzysz akord, łącząc wartości dwóch fal dźwiękowych (określonych przy tej samej przepływności) razem w nową falę dźwiękową. Złożone fale można zbudować z prostych; do odtworzenia utworu potrzebny jest tylko jeden przebieg.
KChaloux

Ponieważ mówisz, że note.play () nie jest asynchroniczny, wątek dla każdej note.play () jest zatem podejściem do odtwarzania wielu nut jednocześnie. O ile nie ... możesz połączyć te nuty w jedną, którą następnie zagrasz w jednym wątku. Jeśli nie jest to możliwe, wtedy przy swoim podejściu będziesz musiał użyć jakiegoś mechanizmu, aby upewnić się, że pozostaną one zsynchronizowane
pnizzle

Odpowiedzi:


46

Jedno założenie, które przyjmujesz, może być nieprawidłowe: potrzebujesz (między innymi), aby Twoje wątki były wykonywane jednocześnie. Może działać dla 3, ale w pewnym momencie system będzie musiał ustalić priorytety, które wątki mają zostać uruchomione jako pierwsze, a które czekają.

Wdrożenie będzie ostatecznie zależeć od interfejsu API, ale większość współczesnych interfejsów API pozwoli z wyprzedzeniem powiedzieć, w co chcesz grać, i zadbać o czas i kolejkowanie. Jeśli miałbyś sam kodować taki interfejs API, ignorując istniejący systemowy interfejs API (dlaczego miałbyś ?!), kolejka zdarzeń mieszająca nuty i odtwarzająca je z jednego wątku wygląda na lepsze podejście niż model wątku na nutę.


36
Lub inaczej mówiąc - system nie gwarantuje i nie może zagwarantować kolejności, sekwencji ani czasu trwania wątku po uruchomieniu.
James Anderson

2
@JamesAnderson, z wyjątkiem sytuacji, gdy podjęlibyśmy ogromne wysiłki w zakresie synchronizacji, co skończyłoby się znowu niemal sfletarnym zakończeniem.
Mark

Przez „API” masz na myśli bibliotekę audio, której używam?
Aviv Cohn

1
@Prog Tak. Jestem prawie pewien, że ma coś wygodniejszego niż note.play ()
ptyx

26

Nie polegaj na wątkach wykonywanych w trybie blokady. Każdy system operacyjny, o którym wiem, nie gwarantuje, że wątki będą wykonywane w czasie ze sobą spójnie. Oznacza to, że jeśli procesor działa 10 wątków, niekoniecznie otrzymują równy czas w danej sekundzie. Mogą szybko zsynchronizować się lub mogą zachować doskonałą synchronizację. Taka jest natura wątków: nic nie jest gwarantowane, ponieważ zachowanie ich wykonania jest niedeterministyczne .

W przypadku tej konkretnej aplikacji uważam, że potrzebujesz jednego wątku, który zużywa nuty. Zanim będzie mógł zużywać nuty, jakiś inny proces musi połączyć instrumenty, pięciolinie, cokolwiek w jeden utwór muzyczny .

Załóżmy, że masz np. Trzy wątki tworzące notatki. Zsynchronizowałbym je na jednej strukturze danych, w której wszyscy mogliby umieścić swoją muzykę. Kolejny wątek (konsument) czyta połączone nuty i odtwarza je. Może być konieczne krótkie opóźnienie, aby mieć pewność, że synchronizacja wątków nie spowoduje utraty notatek.

Literatura pokrewna: problem producent-konsument .


1
Dzięki za odpowiedź. Nie jestem w 100% pewien, że rozumiem twoją odpowiedź, ale chciałem upewnić się, że rozumiesz, dlaczego potrzebuję 3 wątków, aby jednocześnie odtwarzać 3 nuty: note.play()dzieje się tak, ponieważ wywołanie powoduje zawieszenie wątku, dopóki nuta nie zostanie odtworzona . Aby więc móc play()3 notatki jednocześnie, potrzebuję 3 różnych wątków, aby to zrobić. Czy twoje rozwiązanie rozwiązuje moją sytuację?
Aviv Cohn

Nie, i nie było to jasne z pytania. Czy nić nie może zagrać akordu?

4

Klasycznym podejściem do tego problemu muzycznego byłoby użycie dokładnie dwóch wątków. Po pierwsze, wątek o niższym priorytecie obsługiwałby interfejs użytkownika lub kod generujący dźwięk

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

(zwróć uwagę na koncepcję tylko asynchronicznego uruchamiania notatki i kontynuowania bez czekania na jej zakończenie)

a drugi wątek w czasie rzeczywistym konsekwentnie przeglądałby wszystkie nuty, dźwięki, próbki itp., które powinny być odtwarzane; wymieszaj je i wyślij końcowy przebieg. Ta część może być (i często jest) pobierana z dopracowanej biblioteki strony trzeciej.

Wątek ten często byłby bardzo wrażliwy na „głodzenie” zasobów - jeśli jakiekolwiek rzeczywiste przetwarzanie wyjścia dźwięku zostanie zablokowane na dłużej niż wyjście buforowane na karcie dźwiękowej, spowoduje to słyszalne artefakty, takie jak przerwy lub trzaski. Jeśli masz 24 wątki, które bezpośrednio wysyłają dźwięk, masz znacznie większą szansę, że jeden z nich się zacina w pewnym momencie. Jest to często uważane za niedopuszczalne, ponieważ ludzie są raczej wrażliwi na usterki dźwięku (znacznie więcej niż na artefakty wizualne) i zauważają nawet niewielkie jąkanie.


1
OP twierdzi, że API może odtwarzać tylko jedną nutę naraz
Mooing Duck

4
@MooingDuck, jeśli API może mieszać, to powinno się mieszać; jeśli OP mówi, że API nie może mieszać, wówczas rozwiązaniem jest wmieszanie twojego kodu i sprawienie, aby inny wątek wykonał my_mixed_notes_from_whole_chord_progression.play () za pośrednictwem interfejsu API.
Peteris

1

Tak, robisz coś złego.

Pierwszą rzeczą jest to, że tworzenie wątków jest drogie. Ma znacznie większy narzut niż tylko wywoływanie funkcji.

Tak więc, jeśli potrzebujesz wielu wątków do tego zadania, musisz je ponownie przetworzyć.

Jak mi się wydaje, masz jeden główny wątek, który wykonuje planowanie innych wątków. Tak więc główny wątek prowadzi przez muzykę i rozpoczyna nowe wątki, gdy tylko pojawi się nowa nuta do odtworzenia. Zamiast więc pozwolić wątkom umrzeć i zrestartować je, lepiej trzymaj pozostałe wątki przy życiu w pętli uśpienia, gdzie sprawdzają co x milisekund (lub nanosekund), czy jest nowa nuta do odtworzenia i inaczej śpij. Główny wątek nie rozpoczyna wtedy nowych wątków, ale informuje istniejącą pulę wątków, aby odtwarzały nuty. Tylko jeśli nie ma wystarczającej liczby wątków w puli, może tworzyć nowe wątki.

Kolejna to synchronizacja. Prawie żaden współczesny system wielowątkowy nie gwarantuje, że wszystkie wątki są wykonywane jednocześnie. Jeśli na komputerze działa więcej wątków i procesów niż rdzeni (co jest w większości przypadków), wątki nie otrzymują 100% czasu procesora. Muszą dzielić procesor. Oznacza to, że każdy wątek otrzymuje niewielką ilość czasu procesora, a następnie po udostępnieniu następny wątek pobiera procesor na krótki czas. System nie gwarantuje, że Twój wątek otrzyma taki sam czas procesora jak inne wątki. Oznacza to, że jeden wątek może oczekiwać na zakończenie drugiego, a zatem może zostać opóźniony.

Powinieneś raczej rzucić okiem, jeśli nie można odtworzyć wielu nut w jednym wątku, aby wątek przygotował wszystkie nuty, a następnie wydał tylko polecenie „start”.

Jeśli musisz to zrobić z wątkami, przynajmniej użyj ich ponownie. Nie musisz mieć tyle wątków, ile nut jest w całym utworze, ale potrzebujesz tylko tyle wątków, ile wynosi maksymalna liczba nut granych jednocześnie.


„[Czy] można odtwarzać wiele nut w jednym wątku, dzięki czemu wątek przygotuje wszystkie nuty, a następnie wyda tylko polecenie„ start ”.” - To była moja pierwsza myśl. Czasami zastanawiam się, czy bombardowanie komentarzem na temat wielowątkowości (np. Programmers.stackexchange.com/questions/43321/... ) nie prowadzi wielu programistów na manowce podczas projektowania. Sceptycznie podchodzę do wszelkich dużych, początkowych procesów, które kończą się na potrzebie wielu wątków. Radzę uważnie poszukać rozwiązania z jednym wątkiem.
user1172763,

1

Pragmatyczna odpowiedź brzmi: jeśli działa dla twojej aplikacji i spełnia twoje obecne wymagania, to nie robisz tego źle :) Jeśli jednak masz na myśli „czy jest to skalowalne, wydajne rozwiązanie wielokrotnego użytku”, odpowiedź brzmi „nie”. Projekt przyjmuje wiele założeń dotyczących zachowania się wątków, które mogą być lub nie być prawdziwe w różnych sytuacjach (dłuższe melodie, więcej jednoczesnych nut, inny sprzęt itp.).

Alternatywnie rozważ użycie pętli czasowej i puli wątków . Pętla taktowania działa we własnym wątku i nieustannie sprawdza, czy nuta musi zostać odtworzona. Odbywa się to poprzez porównanie czasu systemowego z czasem rozpoczęcia melodii. Czas każdej nuty można zwykle bardzo łatwo obliczyć na podstawie tempa melodii i sekwencji nut. Ponieważ nowa nuta musi zostać odtworzona, pożycz wątek z puli wątków i wywołaj play()funkcję nuty. Pętla czasowa może działać na różne sposoby, ale najprostsza to ciągła pętla z krótkimi snami (prawdopodobnie między akordami), aby zminimalizować zużycie procesora.

Zaletą tego projektu jest to, że liczba wątków nie przekroczy maksymalnej liczby jednoczesnych nut + 1 (pętla czasowa). Pętla synchronizacji chroni również przed poślizgami w taktowaniu, które mogą być spowodowane opóźnieniem gwintu. Po trzecie, tempo melodii nie jest stałe i można je zmienić, zmieniając obliczenia taktowania.

Nawiasem mówiąc, zgadzam się z innymi komentatorami, że ta funkcja note.play()jest bardzo słabym interfejsem API do pracy. Każdy rozsądny dźwiękowy interfejs API pozwala miksować i planować notatki w znacznie bardziej elastyczny sposób. To powiedziawszy, czasami musimy żyć z tym, co mamy :)


0

Wygląda mi to na prostą implementację, przy założeniu, że jest to interfejs API, którego musisz użyć . Inne odpowiedzi dotyczą tego, dlaczego nie jest to bardzo dobry interfejs API, więc nie mówię o tym więcej, zakładam tylko, że z tym musisz żyć. Twoje podejście będzie korzystać z dużej liczby wątków, ale na nowoczesnym komputerze PC nie powinno to stanowić problemu, o ile liczba wątków jest w dziesiątkach.

Jedną z rzeczy, które powinieneś zrobić, jeśli jest to wykonalne (np. Odtwarzanie z pliku zamiast od użytkownika uderzającego w klawiaturę), jest dodanie pewnego opóźnienia. Więc zaczynasz wątek, który śpi do określonej godziny zegara systemowego i zaczyna odtwarzać nutę we właściwym czasie (uwaga, czasami sen może zostać przerwany wcześniej, więc sprawdź zegar i śpij więcej, jeśli to konieczne). Chociaż nie ma gwarancji, że system operacyjny będzie kontynuował wątek dokładnie po zakończeniu snu (chyba że korzystasz z systemu operacyjnego w czasie rzeczywistym), jest bardzo prawdopodobne, że będzie znacznie dokładniejszy niż wtedy, gdy zaczniesz wątek i zaczniesz grać bez sprawdzania czasu .

Następnie następnym krokiem, który nieco komplikuje rzeczy, ale nie za bardzo i pozwoli ci zmniejszyć wyżej wspomniane opóźnienie, jest użycie puli wątków. Oznacza to, że gdy nić kończy nutę, nie wychodzi, ale czeka na odtworzenie nowej nuty. A kiedy poprosisz o rozpoczęcie odtwarzania nuty, najpierw spróbuj pobrać wolny wątek z puli i dodać nowy wątek tylko w razie potrzeby. Będzie to wymagało oczywiście prostej komunikacji między wątkami zamiast obecnego podejścia typu „strzelaj i zapomnij”.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.