멀티 스레딩 : 내가 잘못하고 있습니까?


23

음악을 재생하는 응용 프로그램을 만들고 있습니다.

재생 중에는 종종 스레드가 동시에 발생해야하기 때문에 별도의 스레드에서 발생해야합니다. 호출이 :. 예를 들어, 각자가 연주하는 자신의 스레드를 할당하도록 코드가 필요의 노트 (편집 명확히하기 위해 함께들을 수 note.play()메모가 완료 재생 될 때까지 스레드를 정지, 나는 세 가지 필요한 이유입니다 세 개의 음이 동시에 들리도록 별도의 스레드.)

이런 종류의 동작은 음악을 재생하는 동안 많은 스레드를 만듭니다.

예를 들어 짧은 멜로디와 짧은 코드 진행을 가진 음악을 생각해보십시오. 전체 멜로디는 하나의 스레드에서 연주 될 수 있지만, 각 코드에는 3 개의 음표가 포함되어 있으므로 진행하려면 3 개의 스레드가 필요합니다.

따라서 진행을 재생하는 의사 코드는 다음과 같습니다.

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...언어의 스레딩 모델에 따라 다릅니다. 병렬 처리에 사용되는 스레드는 종종 OS 수준에서 처리되므로 OS가 스레드를 여러 코어에 매핑 할 수 있습니다. 이러한 스레드는 작성하고 전환하는 데 비용이 많이 듭니다. 동시성 스레드 (두 작업을 동시에 실행하지 않아도 됨)는 언어 / VM 수준에서 구현할 수 있으며 10 개의 네트워크 소켓과 통신 할 수 있도록 매우 "저렴하게"만들어서 전환 할 수 있습니다. 동시에, 그러나 그렇게 많은 CPU 처리량을 얻을 필요는 없습니다.
Doval

3
그 외에도 다른 사람들은 스레드가 한 번에 여러 사운드를 재생하는 잘못된 방법이라는 것에 대해 옳습니다.
Doval

3
음파의 작동 방식에 대해 잘 알고 있습니까? 일반적으로 두 개의 음파 (동일한 비트 전송률로 지정)의 값을 새로운 음파로 구성하여 코드를 만듭니다. 단순한 파도로 복잡한 파도를 만들 수 있습니다. 노래를 재생하려면 단일 파형 만 있으면됩니다.
KChaloux

note.play ()는 비동기 적이 지 않기 때문에 각 note.play ()의 스레드는 여러 음을 동시에 연주하는 방법입니다. UNLESS .., 당신은 그 음을 하나의 실에서 연주 할 수있는 음표로 결합 할 수 있습니다. 이것이 가능하지 않다면 당신의 접근 방식으로 동기화를 유지하기 위해 어떤 메커니즘을 사용해야 할 것입니다
pnizzle

답변:


46

하나의 가정이 유효하지 않을 수도 있습니다. 다른 것들 중에서 스레드가 동시에 실행되어야합니다. 3까지 작동 할 수 있지만 어느 시점에서 시스템은 먼저 실행할 스레드와 대기하는 스레드의 우선 순위를 정해야합니다.

구현은 궁극적으로 API에 의존하지만 대부분의 최신 API를 사용하면 원하는 것을 미리 알려주고 타이밍과 큐잉을 처리 할 수 ​​있습니다. 기존 시스템 API를 무시하고 (왜 그런가?!) 이러한 API를 직접 코딩해야한다면, 메모를 혼합하고 단일 스레드에서 재생하는 이벤트 큐는 메모 당 스레드 모델보다 더 나은 방법으로 보입니다.


36
또는 다르게 말하면 시스템은 일단 시작된 스레드의 순서, 순서 또는 지속 시간을 보장하지 않을 수도 있습니다.
James Anderson

2
@JamesAnderson 동기화를 위해 막대한 노력을 기울일 경우를 제외하고는 결국 거의 다시 말끔하게 끝날 것입니다.
Mark

'API'는 내가 사용하는 오디오 라이브러리를 의미합니까?
Aviv Cohn

1
@Prog 예. 나는 그 note.play () 더 편리한 무언가가 확실 해요
ptyx

26

lockstep에서 실행중인 스레드에 의존하지 마십시오. 내가 아는 모든 운영 체제가 스레드가 서로 일관되게 실행되도록 보장하지는 않습니다. 즉, CPU가 10 개의 스레드를 실행하는 경우 주어진 시간 (초)에 반드시 동일한 시간을받는 것은 아닙니다. 그들은 신속하게 동기화에서 벗어나거나 완벽하게 동기화 상태를 유지할 수 있습니다. 이것이 스레드의 특성입니다. 실행 동작이 결정적이지 않기 때문에 보장되는 것은 없습니다 .

이 특정 응용 프로그램의 경우 음악 노트를 소비 하는 단일 스레드 가 필요하다고 생각합니다 . 노트를 사용하기 전에 악기, 스태프 등을 하나 의 음악 으로 병합해야하는 다른 프로세스도 있습니다.

예를 들어 메모를 생성하는 세 개의 스레드가 있다고 가정합니다. 모든 음악을 넣을 수있는 단일 데이터 구조로 동기화합니다. 다른 스레드 (소비자)는 결합 된 음을 읽고 재생합니다. 스레드 타이밍으로 인해 메모가 누락되지 않도록하기 위해 약간의 지연이 필요할 수 있습니다.

관련 읽기 : 생산자-소비자 문제 .


1
대답 해줘서 고마워. 100 % 확신 할 수는 없지만, 3 개의 음표를 동시에 연주하기 위해 3 개의 스레드가 필요한 이유를 이해하고 싶었 note.play()습니다. 음표 연주가 완료 될 때까지 호출 하면 스레드가 정지 되기 때문 입니다. 그래서 play()동시에 3 개의 노트 를 만들 수 있으려면 3 개의 다른 스레드가 필요합니다. 귀하의 솔루션이 내 상황을 해결합니까?
Aviv Cohn

아니, 그 질문에서 명확하지 않았습니다. 실이 코드를 연주 할 수있는 방법이 없습니까?

4

이 음악 문제에 대한 고전적인 접근법은 정확히 두 개의 스레드 를 사용하는 것 입니다. 하나, 우선 순위가 낮은 스레드는 UI 또는 사운드 생성 코드를 처리합니다.

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

(메모를 비동기식으로 시작 하고 완료를 기다리지 않고 진행 한다는 개념에 유의하십시오 )

둘째, 실시간 스레드는 현재 연주해야하는 모든 음표, 사운드, 샘플 등을 지속적으로 확인합니다. 그것들을 혼합하고 최종 파형을 출력하십시오. 이 부분은 세련된 타사 라이브러리에서 가져온 것일 수 있습니다.

이 스레드는 종종 리소스의 "고갈"에 매우 민감합니다. 실제 사운드 출력 처리가 사운드 카드 버퍼 출력보다 오래 차단 된 경우 중단이나 팝과 같은 소리가 들릴 수 있습니다. 오디오를 직접 출력하는 24 개의 스레드가있는 경우 스레드 중 하나가 언젠가 끊길 가능성이 훨씬 높습니다. 인간은 오디오 결함 (시각적 아티팩트보다 훨씬)에 민감하고 사소한 말더듬을 느끼기 때문에 종종 받아 들일 수없는 것으로 간주됩니다.


1
영업 이익은 API는 한 번에 하나 개의 음을 연주 할 수 있다고
음매을 오리

4
@MooingDuck API가 섞일 수 있다면 섞어 야합니다. OP가 API가 혼합 할 수 없다고 말하면 솔루션은 코드를 혼합하고 다른 스레드가 API를 통해 my_mixed_notes_from_whole_chord_progression.play ()를 수행하도록하는 것입니다.
Peteris

1

글쎄, 당신은 뭔가 잘못하고 있습니다.

첫 번째는 스레드를 만드는 것이 비싸다는 것입니다. 단순히 함수를 호출하는 것보다 훨씬 많은 오버 헤드가 있습니다.

따라서이 작업에 여러 스레드가 필요한 경우 스레드를 재활용하는 것입니다.

나에게 보이는 것처럼 다른 스레드를 예약하는 하나의 기본 스레드가 있습니다. 따라서 메인 스레드는 음악을 통해 리드되고 새로운 연주 음이있을 때마다 새로운 스레드를 시작합니다. 따라서 스레드를 죽이고 다시 시작하는 대신 다른 스레드를 절전 루프에 유지하는 것이 좋습니다. 여기서 새 음표가 연주되고 잠자기 상태가 있으면 매 x 밀리 초 (또는 나노초)마다 검사합니다. 주 스레드는 새 스레드를 시작하지 않지만 기존 스레드 풀에 메모를 재생하도록 지시합니다. 풀에 스레드가 충분하지 않은 경우에만 새 스레드를 만들 수 있습니다.

다음은 동기화입니다. 현대의 모든 멀티 스레딩 시스템은 실제로 모든 스레드가 동시에 실행되도록 보장하지 않습니다. 코어보다 많은 스레드와 프로세스가 머신에서 실행되는 경우 (대부분의 경우) 스레드는 CPU 시간의 100 %를 얻지 못합니다. 그들은 CPU를 공유해야합니다. 이것은 모든 스레드가 적은 양의 CPU 시간을 얻은 다음 공유 한 후 다음 스레드가 짧은 시간 동안 CPU를 얻는다는 것을 의미합니다. 시스템은 스레드가 다른 스레드와 동일한 CPU 시간을 얻도록 보장하지 않습니다. 즉, 한 스레드가 다른 스레드가 완료되기를 기다리고 있으므로 지연 될 수 있습니다.

스레드가 모든 음표를 준비한 다음 "시작"명령 만 제공 할 수 있도록 한 스레드에서 여러 음표를 연주 할 수 없는지 살펴보아야합니다.

스레드로 수행해야하는 경우 최소한 스레드를 재사용하십시오. 그런 다음 전체 곡에 음표가있는만큼 많은 스레드를 가질 필요는 없지만 동시에 연주되는 최대 음표 수만큼 많은 스레드 만 필요합니다.


"[한 스레드에서 여러 음을 연주 할 수 있으므로 스레드가 모든 음을 준비한 다음"start "명령 만 제공합니다." -이것도 저의 첫 생각이었습니다. 멀티 스레딩 (예 : programmers.stackexchange.com/questions/43321/… ) 에 대한 해설로 충격을받는 것이 디자인 중에 많은 프로그래머를 잃어 버리지 않는 경우가 있습니다. 나는 많은 스레드가 필요하다는 결론을 내리는 크고 선행 프로세스에 회의적입니다. 단일 스레드 솔루션을 열심히 찾는 것이 좋습니다.
user1172763

1

실용적인 대답은 응용 프로그램에서 작동하고 현재 요구 사항을 충족하면 잘못하고 있지 않다는 것입니다.) 그러나 "이것이 확장 가능하고 효율적이며 재사용 가능한 솔루션입니까?" 디자인은 다른 상황 (더 긴 멜로디, 더 많은 동시 음표, 다른 하드웨어 등)에서 스레드가 동작하는 방식에 대해 많은 가정을합니다.

대안으로 타이밍 루프 및 스레드 사용을 고려하십시오 . 타이밍 루프는 자체 스레드에서 실행되며 음표를 연주해야하는지 지속적으로 확인합니다. 멜로디가 시작된 시간과 시스템 시간을 비교하여이를 수행합니다. 각 음표의 타이밍은 보통 멜로디의 템포와 음표 순서에서 매우 쉽게 계산할 수 있습니다. 새로운 음을 연주해야하므로, 실 풀에서 실을 빌려 가서 그 play()기능을 호출 하십시오. 타이밍 루프는 다양한 방식으로 작동 할 수 있지만 가장 간단한 것은 CPU 사용을 최소화하기 위해 짧은 수면 (아마도 코드 사이)이있는 연속 루프입니다.

이 설계의 장점은 스레드 수가 최대 동시 노트 수 + 1 (타이밍 루프)을 초과하지 않는다는 것입니다. 또한 타이밍 루프는 스레드 대기 시간으로 인해 발생할 수있는 타이밍의 미끄러짐을 방지합니다. 셋째, 멜로디의 템포는 고정되어 있지 않으며 타이밍 계산을 변경하여 변경할 수 있습니다.

따로, 나는 다른 주석가들과 함수 note.play()가 작업하기에 매우 열악한 API 라는 데 동의합니다 . 합리적인 사운드 API를 사용하면 훨씬 유연한 방식으로 메모를 믹싱하고 예약 할 수 있습니다. 즉, 때때로 우리는 우리가 가진 것과 함께 살아야합니다 :)


0

사용해야하는 API라고 가정하면 간단한 구현으로 잘 보입니다 . 다른 답변은 이것이 왜 API가 좋지 않은지에 대해 다루므로 더 이상 이야기하지 않고 단지 그것이 당신이 살아야 할 것이라고 가정합니다. 여러분의 접근 방식은 많은 수의 스레드를 사용하지만 스레드 수가 수십 개인 한 걱정할 필요가없는 최신 PC에서 사용됩니다.

가능한 경우 (키보드를 누르는 대신 파일에서 재생하는 것과 같이) 가능한 경우 대기 시간을 추가하는 것입니다. 따라서 스레드를 시작하면 시스템 시계의 특정 시간까지 휴면 상태에 있고 적절한 시간에 음표를 연주하기 시작합니다 (참고 : 수면이 일찍 중단 될 수 있으므로 시계를 확인하고 필요한 경우 더 많이 휴면). (실시간 운영 체제를 사용하지 않는 한) 수면이 끝날 때 OS가 스레드를 정확하게 계속한다고 보장하지는 않지만 스레드를 시작하고 타이밍을 확인하지 않고 재생을 시작하는 것보다 훨씬 정확할 가능성이 큽니다. .

그런 다음 다음 단계는 약간 복잡하지만 너무 복잡하지 않으며 위에서 언급 한 대기 시간을 줄일 수 있도록 스레드 풀을 사용하십시오. 즉, 스레드가 메모를 마치면 종료되지 않고 대신 새 메모가 재생되기를 기다립니다. 그리고 음표 연주를 요청하면 먼저 풀에서 빈 스레드를 가져 와서 필요한 경우에만 새 스레드를 추가하십시오. 물론 현재의 fire-and-forget 방식 대신 간단한 스레드 간 통신이 필요합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.