また、具体的な課題が含まれていないご意見募集型の質問で失礼します。
####昔話
Windows 3.1 や Mac OS System 4 など昔のパソコンのOSはノンプリエンプティブ・マルチタスク(以降、協調的マルチタスクと呼びます)でした。
これらのOSではアプリケーションが自発的にCPUを開放しないとタスクが切り替わらず、一つでもCPU の開放のしかたが悪いプログラムがあると、OSごと凍りついてしまうものでした。そのことが不評で、今や全面的にプリエンプティブ・マルチタスクのOSに置き換えられました。
####帰ってきた協調的マルチタスク
今、マルチスレッドのアーキテクチャが抱えるC10K問題に対応するために node.js、 nginx+lua などイベント駆動型やコルーチン型(=協調的マルチタスク)で処理する処理系が出始めました。そして、従来の一つのリクエストを一つのスレッドが処理するプリエンプティブなマルチスレッド処理系に比べて高い性能が出るということが定説になっています。
####また戦いが
その状況の中で、最近のWebでの議論を見ていると、Linux のマルチスレッドが非常に軽量・高速で協調的マルチタスクじゃなくてもマルチスレッドで十分性能が出るというベンチマーク結果が出てきているようです(※1)。そうなると、協調的マルチタスクの処理系はまたなくなってしまうのでしょうか。
前述の昔話において、パソコンのOSがプリエンプティブマルチタスクに置き換えられるときも、OSの実装者は「プリエンプティブマルチタスクなんて必要ない。プログラマが時間のかかる処理を実装するときに処理の途中経過を報告したり、中断したりする機能をちゃんと入れてれば、協調的マルチタスクのほうが効率が高いに決まっている」と執拗に粘ったのですが、世の中の流行には逆らえず(というよりも質の悪いプログラムの多さに負けて)、負けてしまいました。
今回もプリエンプティブマルチタスク(マルチスレッド派)対協調的マルチタスク(イベント駆動型やコルーチン派)の戦いが始まっているように思います。これを第二次プリエンプティブ戦争と呼びましょう。
####協調的マルチタスク派
(いくつか回答いただいて、自分の好みの問題とC10K問題が直結していないことに気が付き、書き直しました)
私は個人的にはシングルスレッドイベント駆動型でコーディングするのが好きで、c10K問題を理由にマルチスレッドプログラミングのパラダイムを駆逐して欲しいくらいに思っています。たとえば、DBへの問い合わせが長い時間かかる場合に、ユーザからの要求やシステムのシャットダウンに対応して問い合わせを中断できるようにする場合、マルチスレッドで割り込みを気にしながら順次的に書くよりも自分で状態遷移を管理するほうが簡単ではないでしょうか。マルチスレッドの場合、
(DBへの問い合わせを中止できるようなドライバがあるかどうかはおいておいて)
javascript
1class 問い合わせ { 2 do() { 3 try { 4 DBへの問い合わせ 5 } catch (CancelSignal cs) { 6 if (微妙なタイミング判定) { 7 問い合わせ中止処理 8 } 9 } 10 } 11 onCancel() { 12 問い合わせ実行中スレッドへ CancelSignal を送信 13 } 14}
となり、問い合わせの終了とキャンセルシグナルの入力との微妙なタイミング判定(排他制御)が必要ですが、シングルスレッドであれば、
javascript
1class 問い合わせ { 2 do() { 3 DBへの問い合わせ((event) => this.handler(event)); 4 } 5 handler (event) { 6 ... 7 } 8 onCancel() { 9 問い合わせ中止処理 10 } 11}
と書けて、シングルスレッドなので、handler 呼び出しと onCancel の呼び出しは早い者勝ちとなるわけです。
そもそも、OSレベルでは多重入力はイベント駆動で実装されているわけで、そのイベント駆動をそのままコーディングできるシングルスレッドイベント駆動プログラミングって気持ち良くないですか?
####質問:シングルスレッドプログラミングは好きですか?残ると思いますか?
いくつか回答いただいて、シングルスレッドプログラミングとC10K問題が直結していないことに気が付きましたが、質問としてはC10K問題で復権したシングルスレッドプログラミングをどう思うかというポイントに変更して続けさせていただきたいと思います。
JavaScript のコールバックは嫌いですか? lua などコルーチンの yield って懐かしくないですか?マルチスレッドプログラミングって難しくないですか?
####補足1:マルチコアについて
それほど性能がCPUに依存するアプリが少ないのではないかとは思いますが、、CPU依存が強い場合でもワーカプロセスに負荷分散することで、プログラム自身はシングルスレッド・協調的マルチタスクで書くことが可能だと思います。特に並列演算で問題が解決するような場合は、ワーカプロセス側で GPUをつかったり、SIMD命令を使うなど、マルチコア以上に並列度をあげる工夫が必要ではないかと考えます。この場合は、マルチスレッドプログラミングというよりも並列処理プログラミングになってくると思います。となると、やはりマルチスレッドプログラミングは不要なのではないかと思えてきます。
####補足2:イベント駆動型プログラミングの順次的記述
回答でイベント駆動型プログラミングのデメリットとしてサブルーチンのネスト構造などモジュール化がやりにくいという指摘がありました。 Javascript では、サブルーチンの呼び出し後の継続処理をコールバック関数(というかクロージャ)に記述するという技法で回避し、非同期でありながらモジュール化を行うことに成功しています。しかし、この技法はコールバック地獄と呼ばれており、モジュール化には成功しましたが、順次処理を記述すると、ネストがどんどん深くなるという欠点がありました。次世代 Javascript である es6 では、この問題を Promise という技法でさらに回避しております。
ここで、普通に順次的に記述したプログラムと Promiseを使って非同期処理可能でありながら順次的に記述したプログラムを比較してみます。
普通の順次的記述
Javascript
1function main() { 2 let x = 'default value of x'; 3 let y = 'default value of y'; 4 let z; 5 try { 6 z = getValuesForZ(x); 7 z = z.map(item => processItem(item)); 8 if (z.includes(processItem(x))) { 9 y = processY(y); 10 } 11 console.log("x = " + x + ", y= " + y + ", z= " + z); 12 } catch (e) { 13 console.log(e); 14 } 15} 16 17function getValuesForZ(arg) 18{ 19 return ['value1', 'value2'].concat(arg); 20} 21 22function processItem(item) { 23 return 'processed:' + item; 24} 25 26function processY(y) { 27 return processYnest(y); 28} 29 30function processYnest(y) { 31 return 'processed in nested function:' + y; 32} 33 34main()
Promise を使った非同期処理可能な順次的記述
Javascript
1function main() { 2 Promise.resolve({ 3 x: 'default value of x', 4 y: 'default value of y' 5 }) 6 .then(getValuesForZ()) 7 .then(state => { 8 return Promise.all(state.z.map(item => Promise.resolve(item).then(processItem()))) 9 .then(z => Object.assign(state, {z})); 10 }) 11 .then(state => { 12 return Promise.resolve(state.x).then(processItem()).then(r => { 13 if (state.z.includes(r)) { 14 return Promise.resolve(state).then(processY()); 15 } 16 return state; 17 }); 18 }) 19 .then(state => { 20 console.log("x = " + state.x + ", y= " + state.y + ", z= " + state.z); 21 }) 22 .catch(e => { 23 console.log(e); 24 }); 25} 26 27function getValuesForZ() 28{ 29 return state => Object.assign(state, {z:['value1', 'value2'].concat(state.x)}); 30} 31 32function processItem() { 33 return item => 'processed:' + item; 34} 35 36function processY() { 37 return state => { 38 return Promise.resolve(state.y) 39 .then(processYnest()) 40 .then(y => Object.assign(state,{y})); 41 }; 42} 43 44function processYnest() { 45 return y => 'processed in nested function:' + y; 46} 47 48main()
普通版は 34行に対して、Promise 版は48行と行数は増えてしまいましたが、処理順にプログラムを書くことができて、かつ、サブルーチンによるモジュール化もできていると思います。
モジュール化のみそは、サブルーチンが演算結果を返すのではなく、演算を行う関数を返すことです。このようにすることで、呼び出し側は非同期にその関数を呼び出すことができるわけです。
プログラムの詳細な説明は避けますが、順次処理、条件判定、繰り返し処理、サブルーチンのネスト呼び出しを含める例となっています。プログラムの処理内容には何の意味もありません。また、両方を比較しやすいように非同期処理は入っていませんが、Promise版の方ではいくらでも非同期処理を挿入できますが、普通版では非同期処理を挿入することは不可能です。
※1の参考:
TCP/IP - Solving the C10K with the thread per client approach
「サーバ書くなら epoll 使うべき」は、今でも正しいのか
いずれも、協調的マルチタスクに比べれば性能は出ないと思いますが、「協調的マルチタスクがないと1万コネクションはできないのかというとそうではない」という主張だと思います。桁が上がって10万コネクションになれば、やはり協調的マルチタスクが必要なのかもしれません。
回答2件
あなたの回答
tips
プレビュー