マルチスレッド:間違っていますか?


23

私は音楽を再生するアプリケーションに取り組んでいます。

再生中は、同時に発生する必要があるため、多くの場合、別々のスレッドで発生する必要があります。例えば、一緒に聞くことにする和音必要のノートは、それぞれがで再生する独自のスレッドが割り当てられているので(編集明確にする:。呼び出しはnote.play()、ノートの再生が完了するまでスレッドをフリーズする、と私は3を必要とする理由です別々のスレッドで同時に3つのノートを聞くことができます。)

このような動作は、音楽の再生中に多くのスレッドを作成します。

たとえば、短いメロディーと短いコード進行を伴う音楽を考えてみましょう。メロディ全体を1つのスレッドで再生できますが、進行には3つのスレッドが必要です。各コードには3つの音符が含まれているためです。

したがって、進行を再生するための擬似コードは次のようになります。

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

したがって、プログレッションには4つのコードがあり、3 notes * 4 chords * 2 times24スレッドを開くよりも2回コードを再生すると仮定します。そして、これは一度だけ再生するためのものです。

実際には、実際にはうまく機能します。顕著な遅延や、これに起因するバグに気付きません。

しかし、私はこれが正しい慣行なのか、それとも根本的に間違っているのかを尋ねたかったのです。ユーザーがボタンを押すたびに非常に多くのスレッドを作成するのは妥当ですか?そうでない場合、どうすれば異なる方法で行うことができますか?


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はそれらを複数のコアにマップできます。このようなスレッドは、作成および切り替えに費用がかかります。並行性(2つのタスクをインターリーブし、必ずしも両方を同時に実行する必要はありません)のスレッドは、言語/ VMレベルで実装でき、生成と切り替えを行うために非常に「安価」にすることができます。同時に、しかし、あなたは必ずしもより多くのCPUスループットをそのように得るとは限りません。
ドーバル14

3
それはさておき、他の人は、スレッドが一度に複数のサウンドの再生を処理する間違った方法であることについて間違いなく正しいです。
ドーバル14

3
音波がどのように機能するかについて詳しく知っていますか?通常、2つの音波(同じビットレートで指定)の値を一緒に新しい音波に合成することで和音を作ります。単純な波から複雑な波を構築できます。曲を再生するために必要な波形は1つだけです。
KChaloux 14

note.play()は非同期ではないと言うので、各note.play()のスレッドは、複数のノートを同時に再生するアプローチです。それ以外の場合は、これらのノートを1つのスレッドに結合して、1つのスレッドで演奏することができます。それが不可能な場合は、あなたのアプローチで、あなたは彼らが同期したままにするために、いくつかのメカニズムを使用する必要があるとしている
pnizzle

回答:


46

前提条件の1つは有効ではない可能性があります。(特に)スレッドを同時に実行する必要があります。3で動作する可能性がありますが、ある時点で、システムはどのスレッドを最初に実行し、どのスレッドを待機させるかを優先する必要があります。

実装は最終的にAPIに依存しますが、最新のAPIのほとんどは、プレイしたいものを事前に指示し、タイミングとキューイング自体を処理します。既存のシステムAPIを無視して、そのようなAPIを自分でコーディングする場合(なぜですか?)、ノートを混合して単一のスレッドから再生するイベントキューは、ノートモデルごとのスレッドよりも優れたアプローチのように見えます。


36
または、別の言い方をすれば、システムは、一度開始されたスレッドの順序、順序、または期間を保証しないし、保証することもできません。
ジェームズアンダーソン14

2
@JamesAndersonは、同期に多大な労力を費やす場合を除き、最終的にはほぼむかつくことになります。
マーク14

「API」とは、私が使用しているオーディオライブラリを意味しますか?
アビブコーン14

1
@Progはい。note.play()
ptyx 14

26

ロックステップで実行されるスレッドに依存しないでください。私が知っているすべてのオペレーティングシステムは、スレッドが互いに一貫した時間で実行されることを保証しません。これは、CPUが10個のスレッドを実行している場合、それらが常に一定の時間を受け取るとは限らないことを意味します。彼らはすぐに同期から外れたり、完全に同期を保つことができます。それがスレッドの性質です。実行の動作は非決定的であるため、何も保証されません。

この特定のアプリケーションでは、音符を消費する単一のスレッドが必要だと思います。ノートを消費する前に、他のプロセスでは、楽器、譜表などを1つの音楽にマージする必要があります。

たとえば、ノートを生成する3つのスレッドがあるとします。単一のデータ構造でそれらを同期して、すべてのユーザーが音楽を配置できるようにします。別のスレッド(消費者)は、結合されたノートを読み取り、再生します。スレッドのタイミングによってノートが失われないようにするには、ここで少し遅延する必要があります。

関連資料:生産者と消費者の問題


1
回答ありがとうございます。私はあなたが答えだと理解していると100%確信していませんが、3つのノートを同時に演奏するために3つのスレッドが必要な理由を理解したかったのです:それは呼び出しnote.play()がノートの演奏が完了するまでスレッドをフリーズするからです。したがって、play()3つのノートに同時にアクセスできるようにするには、3つの異なるスレッドが必要です。あなたのソリューションは私の状況に対処していますか?
アビブコーン14

いいえ、それは質問から明らかではありませんでした。スレッドがコードを演奏する方法はありませんか?

4

この音楽の問題に対する古典的なアプローチは、正確に2つのスレッドを使用することです。1つは、優先度の低いスレッドがUIまたはサウンド生成コードを処理します。

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

(ノートを非同期で開始し、終了するのを待たずに続行するという概念に注意してください)

2番目のリアルタイムスレッドは、現在再生中のすべてのノート、サウンド、サンプルなどを常に確認します。それらをミックスして、最終的な波形を出力します。この部分は、洗練されたサードパーティライブラリから取得される場合があります(多くの場合、取得されます)。

多くの場合、そのスレッドはリソースの「枯渇」に非常に敏感です。実際のサウンド出力処理がサウンドカードのバッファー出力よりも長くブロックされると、割り込みやポップなどの可聴ノイズが発生します。オーディオを直接出力しているスレッドが24個ある場合、そのうちの1つが何らかの時点で途切れる可能性がはるかに高くなります。人間はオーディオのグリッチ(視覚的なアーティファクトよりもはるかに)に敏感であり、わずかなutter音にさえ気付くので、これはしばしば受け入れられないと見なされます。


1
OPは、APIが一度に一つの音を演奏することができますと言う
Mooingをダック

4
@MooingDuck APIが混合できる場合、混合する必要があります。OPがAPIを混合できないと言う場合、解決策はコードを混合し、他のスレッドにAPIを介してmy_mixed_notes_from_whole_chord_progression.play()を実行させることです。
ペティス14

1

まあ、はい、あなたは何か間違ったことをしている。

最初のことは、スレッドの作成には費用がかかるということです。関数を呼び出すよりもはるかにオーバーヘッドが大きくなります。

したがって、このジョブに複数のスレッドが必要な場合は、スレッドをリサイクルする必要があります。

私に見えるように、あなたは他のスレッドのスケジューリングを行う1つのメインスレッドを持っています。そのため、メインスレッドは音楽をリードし、新しいノートが演奏されるたびに新しいスレッドを開始します。そのため、スレッドを停止させて再起動する代わりに、他のスレッドをスリープループで生きた状態に保ち、xミリ秒(またはナノ秒)ごとにチェックする新しいノートがあるかどうかをチェックします。メインスレッドは新しいスレッドを開始せず、既存のスレッドプールにノートを再生するよう指示します。プールに十分なスレッドがない場合のみ、新しいスレッドを作成できます。

次は同期です。最新のマルチスレッドシステムが、すべてのスレッドが同時に実行されることを実際に保証することはほとんどありません。コアよりも多くのスレッドとプロセスがマシン上で実行されている場合(ほとんどの場合)、スレッドはCPU時間の100%を取得しません。CPUを共有する必要があります。これは、すべてのスレッドが少量のCPU時間を取得し、それが共有された後、次のスレッドが短時間CPUを取得することを意味します。システムは、スレッドが他のスレッドと同じCPU時間を取得することを保証しません。これは、あるスレッドが別のスレッドが終了するのを待っている可能性があるため、遅延している可能性があることを意味します。

スレッドがすべてのノートを準備し、「開始」コマンドのみを提供するように、1つのスレッドで複数のノートを再生できない場合は、むしろ見てください。

スレッドで行う必要がある場合は、少なくともスレッドを再利用します。その場合、作品全体にあるノートほど多くのスレッドを持つ必要はありませんが、同時に再生されるノートの最大数と同じ数のスレッドだけが必要です。


「1つのスレッドで複数のノートを演奏することが可能です。そのため、スレッドはすべてのノートを準備し、「開始」コマンドのみを提供します。」-これも私の最初の考えでした。マルチスレッドについての解説(例が殺到している場合、私は時々不思議programmers.stackexchange.com/questions/43321/...プログラマは設計時に迷うの多くをリードしません)。私は、大量のスレッドの必要性を最終的に主張する大きな、事前のプロセスに懐疑的です。シングルスレッドソリューションを熱心に探すことをお勧めします。
user1172763 14

1

実用的な答えは、それはあなたのアプリケーションのために働くとあなたの現在の要件は、あなたが間違ったことをやっていない:)満たしているかどうかということです、あなたが意味するならば、答えはノーである「このスケーラブル、効率的な、再利用可能なソリューションです」。設計は、さまざまな状況(長いメロディー、より多くの同時ノート、さまざまなハードウェアなど)で真実である場合とそうでない場合があるスレッドの動作について多くの仮定を行います。

別の方法として、タイミングループとスレッドプールの使用を検討してください。タイミングループは独自のスレッドで実行され、ノートを演奏する必要があるかどうかを継続的にチェックします。これは、システムの時間をメロディーが開始された時間と比較することにより行われます。通常、各ノートのタイミングは、メロディのテンポと一連のノートから非常に簡単に計算できます。新しいノートを再生する必要があるため、スレッドプールからスレッドを借用play()し、ノートで関数を呼び出します。タイミングループはさまざまな方法で機能しますが、最も単純なのは、CPU使用率を最小限に抑えるための短いスリープ(おそらくコード間の)を伴う連続ループです。

この設計の利点は、スレッドの数が同時ノートの最大数+ 1(タイミングループ)を超えないことです。また、タイミングループは、スレッドの遅延によって引き起こされる可能性のあるタイミングのずれからユーザーを保護します。第三に、メロディのテンポは固定されておらず、タイミングの計算を変更することで変更できます。

余談ですが、この関数note.play()は非常に貧弱なAPIであるため、他のコメンターに同意します。適切なサウンドAPIを使用すると、ノートをより柔軟な方法でミックスおよびスケジュールできます。とは言うものの、時には私たちが持っているものと一緒に暮らす必要があります:)


0

それがあなたが使用しなければならないAPIであると仮定して、単純な実装として私にはうまく見えます。他の答えは、なぜこれがあまり良いAPIではないのかをカバーしているので、私はそれについてそれ以上話していない。アプローチでは多数のスレッドを使用しますが、スレッド数が数十個である限り、最新のPCでは問題になりません。

実行可能であれば(ユーザーがキーボードを押すのではなくファイルから再生するなど)、遅延を追加する必要があります。したがって、スレッドを開始すると、システムクロックの特定の時間までスリープし、適切なタイミングでメモの再生を開始します(メモ、場合によってはスリープが早期に中断されることがあるため、必要に応じてクロックを確認し、スリープします)。スリープが終了したときにOSがスレッドを継続するという保証はありませんが(リアルタイムオペレーティングシステムを使用しない限り)、スレッドを開始してタイミングを確認せずに再生を開始する場合よりもはるかに正確である可能性が非常に高くなります。

次に、少し複雑になりますが、あまり複雑ではない次のステップで、上記の待ち時間を短縮できるようにするために、スレッドプールを使用します。つまり、スレッドがノートを終了しても、終了するのではなく、新しいノートが再生されるのを待ちます。そして、ノートの演奏を開始するように要求する場合、まずプールから空きスレッドを取得し、必要な場合にのみ新しいスレッドを追加しようとします。もちろんこれには、現在の発火して忘れるアプローチの代わりに、いくつかの簡単なスレッド間通信が必要になります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.