タスク並列ということ
何故かReactive Extensionsのぐぐる検索で一時トップになってて慌てましたが少し下がってホッとしました。いあ、どう考えてもうちはトップじゃないでしょぐぐるさん!学習向けの紹介サイトならこことかこことかが分りやすくまとめていますので、まずはこちらを覗いてみることをお勧めします。
追い越し禁止?
さて表題の件ですが。私はRxを処理パイプのような感覚で理解しようしてたのですが、少し疑問がありました。「Rxはどんな単位で処理を並列化、あるいは直列化してるのか?」。もうちょっと具体的には「処理パイプの中で後から投入されたデータの処理が先に終わることはありうるのか?」ということ。
ということで実際にやってみた。
処理パイプの構成
今回の試験用処理パイプは、以下の構成になっています。
- 入力は1〜9のRange
- スケジューラはScheduler.ThreadPool
- 実行処理はコンソールへの出力(スレッド番号など)
- 処理内でランダムにSleepを入れる
この構成の部品を適当に組み合わせてパイプを構成し、試験することとします。
コード(超長いので折りたたむ)
using System; using System.Collections.Generic; using System.Concurrency; using System.Threading; using System.Linq; using System.Text; namespace ZStudy.Rx.Parallel { public static class Program { static void Main(string[] args) { // スレッドプールに対してジョブを発行するパイプ var pipe = Observable .Range(1, 9, Scheduler.ThreadPool) ; var stopwatch = new System.Diagnostics.Stopwatch(); // ■1つのパイプを実行 Console.WriteLine("#### TEST 1 ####"); stopwatch.Restart(); pipe .ConsoleWrite("1 ") .Run(); stopwatch.Stop(); Console.WriteLine("#### TEST 1 ==> time {0:00000}ms\n\n" , stopwatch.ElapsedMilliseconds); // ---> 処理は直列化されています。 // 1つのタスクが終わるまで次はスケジュールされません。 // ■2つのパイプをマージ Console.WriteLine("#### TEST 2 ####"); stopwatch.Restart(); Observable.Merge( pipe.ConsoleWrite("1 "), pipe.ConsoleWrite(" 2")) .Run(cnt => { var text = string.Format("({0:0})[--] : MERGE({1:00})", cnt, Thread.CurrentThread.ManagedThreadId); Console.WriteLine("({1:HH:mm:ss.fff}) {0}", text, DateTimeOffset.Now); }); stopwatch.Stop(); Console.WriteLine("#### TEST 2 ==> time {0:00000}ms\n\n" , stopwatch.ElapsedMilliseconds); // ---> 2つのタスクは並列実行されます。 // それぞれのタスクは直列に実行されています。 // ■2つのパイプをZip Console.WriteLine("#### TEST 3 ####"); stopwatch.Restart(); Observable.Zip( pipe.ConsoleWrite("1 "), pipe.ConsoleWrite(" 2"), (a, b) => Tuple.Create(a, b)) .Run(pair => { var text = string.Format("({0:0})[--] : ZIP({1:00}) [{2:00},{3:00}]", pair.Item1, Thread.CurrentThread.ManagedThreadId, pair.Item1, pair.Item2); Console.WriteLine("({1:HH:mm:ss.fff}) {0}", text, DateTimeOffset.Now); }) ; stopwatch.Stop(); Console.WriteLine("#### TEST 2 ==> time {0:00000}ms\n\n" , stopwatch.ElapsedMilliseconds); // ---> 2つのタスクは並列実行されます。 // それぞれのタスクは直列に実行されています。 // ■1つのパイプに2つの処理を続けて実行 Console.WriteLine("#### TEST 4 ####"); stopwatch.Restart(); pipe .ConsoleWrite("1 ") .ConsoleWrite(" 2") .Run(); stopwatch.Stop(); Console.WriteLine("#### TEST 4 ==> time {0:00000}ms\n\n" , stopwatch.ElapsedMilliseconds); // ---> 2つの処理は直列実行されます。 // 2つの処理が終わるまで次のタスクはスケジュールされません。 // ■1つのパイプに2つの処理をスケジューラ切り替えをはさんで実行 Console.WriteLine("#### TEST 5 ####"); stopwatch.Restart(); pipe .ObserveOn(Scheduler.ThreadPool) .ConsoleWrite("1 ") .ObserveOn(Scheduler.ThreadPool) .ConsoleWrite(" 2") .Run(); stopwatch.Stop(); Console.WriteLine("#### TEST 5 ==> time {0:00000}ms\n\n" , stopwatch.ElapsedMilliseconds); // ---> 2つの処理は並列実行されます。 // パイプ内で処理が追い抜かれることはありません。 Console.WriteLine("\n#### 終了しました、何かキーを押してください。 ####"); Console.Read(); } public static IObservable<int> ConsoleWrite(this IObservable<int> pipe, string name) { var rand = new Random(name.GetHashCode()); return pipe .Do(count => { var sleepTime = rand.Next(999); var title = string.Format("({0:0})[{1}]", count, name); Console.WriteLine("({0:HH:mm:ss.fff}) {1} : START({2:00}/{3:000}ms)" , DateTimeOffset.Now, title , Thread.CurrentThread.ManagedThreadId, sleepTime); Thread.Sleep(sleepTime); Console.WriteLine("({0:HH:mm:ss.fff}) {1} : END({2:00})" , DateTimeOffset.Now, title , Thread.CurrentThread.ManagedThreadId, sleepTime); }) ; } } }
TEST1:1処理だけのパイプに10個投入
実行結果
#### TEST 1 #### (01:38:22.493) (1)[1 ] : START(06/630ms) (01:38:23.132) (1)[1 ] : END(06) (01:38:23.135) (2)[1 ] : START(10/668ms) (01:38:23.803) (2)[1 ] : END(10) (01:38:23.803) (3)[1 ] : START(06/391ms) (01:38:24.194) (3)[1 ] : END(06) (01:38:24.194) (4)[1 ] : START(12/875ms) (01:38:25.069) (4)[1 ] : END(12) (01:38:25.069) (5)[1 ] : START(06/276ms) (01:38:25.345) (5)[1 ] : END(06) (01:38:25.345) (6)[1 ] : START(11/449ms) (01:38:25.794) (6)[1 ] : END(11) (01:38:25.794) (7)[1 ] : START(10/250ms) (01:38:26.044) (7)[1 ] : END(10) (01:38:26.044) (8)[1 ] : START(11/184ms) (01:38:26.228) (8)[1 ] : END(11) (01:38:26.228) (9)[1 ] : START(10/608ms) (01:38:26.836) (9)[1 ] : END(10) #### TEST 1 ==> time 04371ms
ここから読み取れるのは、
- 1処理は任意のスレッドプールに割り振られる
- スレッドプールは切り替わるが、処理の追い越しは発生しない
1パイプ内のデータは、たとえスリープが発生したとしても処理の追い越しは発生しません。全ての処理が一度にスケジュールされるのではなく、1処理が終わるたびに次の処理がスケジュールされるようです。
考えてみればこれはタスク並列、またはアクターモデルにおける重要な性格。1つの処理パイプにおいては処理が直列化されることで、並行処理にまつわるもろもろの同期処理を意識せずにロジックを組むことが出来るのです。
TEST2:2つのパイプをマージ
今度はパイプを2つ用意し、Mergeしてみました。
#### TEST 2 #### (01:38:26.852) (1)[1 ] : START(06/630ms) (01:38:26.853) (1)[ 2] : START(11/349ms) (01:38:27.203) (1)[ 2] : END(11) (01:38:27.204) (1)[--] : MERGE(11) (01:38:27.204) (2)[ 2] : START(10/506ms) (01:38:27.483) (1)[1 ] : END(06) (01:38:27.483) (1)[--] : MERGE(06) (01:38:27.484) (2)[1 ] : START(06/668ms) (01:38:27.711) (2)[ 2] : END(10) (01:38:27.711) (2)[--] : MERGE(10) (01:38:27.712) (3)[ 2] : START(11/316ms) (01:38:28.028) (3)[ 2] : END(11) (01:38:28.028) (3)[--] : MERGE(11) (01:38:28.029) (4)[ 2] : START(10/052ms) (01:38:28.084) (4)[ 2] : END(10) (01:38:28.084) (4)[--] : MERGE(10) (01:38:28.085) (5)[ 2] : START(11/758ms) (01:38:28.153) (2)[1 ] : END(06) (01:38:28.153) (2)[--] : MERGE(06) (01:38:28.154) (3)[1 ] : START(13/391ms) (01:38:28.545) (3)[1 ] : END(13) (01:38:28.545) (3)[--] : MERGE(13) (01:38:28.546) (4)[1 ] : START(12/875ms) (01:38:28.843) (5)[ 2] : END(11) (01:38:28.843) (5)[--] : MERGE(11) (01:38:28.844) (6)[ 2] : START(13/837ms) (01:38:29.422) (4)[1 ] : END(12) (01:38:29.422) (4)[--] : MERGE(12) (01:38:29.423) (5)[1 ] : START(11/276ms) (01:38:29.681) (6)[ 2] : END(13) (01:38:29.681) (6)[--] : MERGE(13) (01:38:29.682) (7)[ 2] : START(12/041ms) (01:38:29.700) (5)[1 ] : END(11) (01:38:29.700) (5)[--] : MERGE(11) (01:38:29.701) (6)[1 ] : START(10/449ms) (01:38:29.723) (7)[ 2] : END(12) (01:38:29.723) (7)[--] : MERGE(12) (01:38:29.724) (8)[ 2] : START(11/672ms) (01:38:30.150) (6)[1 ] : END(10) (01:38:30.150) (6)[--] : MERGE(10) (01:38:30.151) (7)[1 ] : START(13/250ms) (01:38:30.397) (8)[ 2] : END(11) (01:38:30.397) (8)[--] : MERGE(11) (01:38:30.398) (9)[ 2] : START(10/397ms) (01:38:30.401) (7)[1 ] : END(13) (01:38:30.401) (7)[--] : MERGE(13) (01:38:30.401) (8)[1 ] : START(12/184ms) (01:38:30.586) (8)[1 ] : END(12) (01:38:30.586) (8)[--] : MERGE(12) (01:38:30.587) (9)[1 ] : START(06/608ms) (01:38:30.795) (9)[ 2] : END(10) (01:38:30.795) (9)[--] : MERGE(10) (01:38:31.195) (9)[1 ] : END(06) (01:38:31.195) (9)[--] : MERGE(06) #### TEST 2 ==> time 04356ms
予想通り2つのパイプは平行に動作し、Mergeポイントでのみデータが直列化されます。Mergeではデータが来た順に受け付けますので、2つのパイプの処理のどちらが早いかによって受け付け順が異なることになります。
TEST3:2つのパイプをZip
Mergeあらため、Zip。ここで少し予想外な挙動です。
#### TEST 3 #### (01:41:15.755) (1)[1 ] : START(11/630ms) (01:41:15.755) (1)[ 2] : START(10/349ms) (01:41:16.105) (1)[ 2] : END(10) (01:41:16.111) (2)[ 2] : START(14/506ms) (01:41:16.385) (1)[1 ] : END(11) (01:41:16.390) (1)[--] : ZIP(11) [01,01] (01:41:16.390) (2)[1 ] : START(13/668ms) (01:41:16.618) (2)[ 2] : END(14) (01:41:16.618) (3)[ 2] : START(11/316ms) (01:41:16.935) (3)[ 2] : END(11) (01:41:16.935) (4)[ 2] : START(14/052ms) (01:41:16.988) (4)[ 2] : END(14) (01:41:16.988) (5)[ 2] : START(14/758ms) (01:41:17.059) (2)[1 ] : END(13) (01:41:17.059) (2)[--] : ZIP(13) [02,02] (01:41:17.060) (3)[1 ] : START(11/391ms) (01:41:17.454) (3)[1 ] : END(11) (01:41:17.454) (3)[--] : ZIP(11) [03,03] (01:41:17.455) (4)[1 ] : START(10/875ms) (01:41:17.747) (5)[ 2] : END(14) (01:41:17.747) (6)[ 2] : START(14/837ms) (01:41:18.330) (4)[1 ] : END(10) (01:41:18.330) (4)[--] : ZIP(10) [04,04] (01:41:18.331) (5)[1 ] : START(13/276ms) (01:41:18.585) (6)[ 2] : END(14) (01:41:18.585) (7)[ 2] : START(15/041ms) (01:41:18.607) (5)[1 ] : END(13) (01:41:18.607) (5)[--] : ZIP(13) [05,05] (01:41:18.608) (6)[1 ] : START(11/449ms) (01:41:18.627) (7)[ 2] : END(15) (01:41:18.627) (8)[ 2] : START(15/672ms) (01:41:19.057) (6)[1 ] : END(11) (01:41:19.057) (6)[--] : ZIP(11) [06,06] (01:41:19.058) (7)[1 ] : START(11/250ms) (01:41:19.300) (8)[ 2] : END(15) (01:41:19.300) (9)[ 2] : START(10/397ms) (01:41:19.308) (7)[1 ] : END(11) (01:41:19.308) (7)[--] : ZIP(11) [07,07] (01:41:19.309) (8)[1 ] : START(11/184ms) (01:41:19.493) (8)[1 ] : END(11) (01:41:19.493) (8)[--] : ZIP(11) [08,08] (01:41:19.494) (9)[1 ] : START(14/608ms) (01:41:19.698) (9)[ 2] : END(10) (01:41:20.102) (9)[1 ] : END(14) (01:41:20.102) (9)[--] : ZIP(14) [09,09] #### TEST 3 ==> time 04368ms
片方が先に到着したらそちらの処理は待機するかと思いましたが、待機せずそのまま続行しています。Zip処理では値をバッファし、両方がそろった段階でバッファから取り出す実装です。考えてみればパイプへのデータ投入は待ったなしですので、受け身の処理が基本であるRxではデータの同期はZip側で面倒見る必要があるのですね。
TEST4:1つのパイプに2つの処理
パイプに、同じ処理を2回実行させてみます。
#### TEST 4 #### (01:41:20.104) (1)[1 ] : START(11/630ms) (01:41:20.735) (1)[1 ] : END(11) (01:41:20.735) (1)[ 2] : START(11/349ms) (01:41:21.085) (1)[ 2] : END(11) (01:41:21.085) (2)[1 ] : START(14/668ms) (01:41:21.754) (2)[1 ] : END(14) (01:41:21.754) (2)[ 2] : START(14/506ms) (01:41:22.262) (2)[ 2] : END(14) (01:41:22.262) (3)[1 ] : START(15/391ms) (01:41:22.654) (3)[1 ] : END(15) (01:41:22.654) (3)[ 2] : START(15/316ms) (01:41:22.971) (3)[ 2] : END(15) (01:41:22.971) (4)[1 ] : START(11/875ms) (01:41:23.847) (4)[1 ] : END(11) (01:41:23.847) (4)[ 2] : START(11/052ms) (01:41:23.900) (4)[ 2] : END(11) (01:41:23.900) (5)[1 ] : START(12/276ms) (01:41:24.177) (5)[1 ] : END(12) (01:41:24.177) (5)[ 2] : START(12/758ms) (01:41:24.936) (5)[ 2] : END(12) (01:41:24.936) (6)[1 ] : START(15/449ms) (01:41:25.386) (6)[1 ] : END(15) (01:41:25.386) (6)[ 2] : START(15/837ms) (01:41:26.224) (6)[ 2] : END(15) (01:41:26.224) (7)[1 ] : START(13/250ms) (01:41:26.475) (7)[1 ] : END(13) (01:41:26.475) (7)[ 2] : START(13/041ms) (01:41:26.517) (7)[ 2] : END(13) (01:41:26.517) (8)[1 ] : START(11/184ms) (01:41:26.702) (8)[1 ] : END(11) (01:41:26.702) (8)[ 2] : START(11/672ms) (01:41:27.375) (8)[ 2] : END(11) (01:41:27.375) (9)[1 ] : START(12/608ms) (01:41:27.984) (9)[1 ] : END(12) (01:41:27.984) (9)[ 2] : START(12/397ms) (01:41:28.382) (9)[ 2] : END(12) #### TEST 4 ==> time 08276ms
結果は、「2つの処理が終わるまで次はスケジュールされない」です。スケジューラへの処理割振り単位は「2つの処理がまとめられて」行われています。単純に関数合成がされる範囲においては並行処理には分割されません。
TEST5:2つの処理の間にスケジューラ切り替えを挟む
今度は処理の間にObserveOnを挟んでみました。
#### TEST 5 #### (01:41:28.390) (1)[1 ] : START(15/630ms) (01:41:29.020) (1)[1 ] : END(15) (01:41:29.020) (2)[1 ] : START(15/668ms) (01:41:29.020) (1)[ 2] : START(12/349ms) (01:41:29.373) (1)[ 2] : END(12) (01:41:29.691) (2)[1 ] : END(15) (01:41:29.691) (3)[1 ] : START(15/391ms) (01:41:29.691) (2)[ 2] : START(14/506ms) (01:41:30.083) (3)[1 ] : END(15) (01:41:30.083) (4)[1 ] : START(12/875ms) (01:41:30.199) (2)[ 2] : END(14) (01:41:30.199) (3)[ 2] : START(14/316ms) (01:41:30.516) (3)[ 2] : END(14) (01:41:30.959) (4)[1 ] : END(12) (01:41:30.959) (5)[1 ] : START(12/276ms) (01:41:30.959) (4)[ 2] : START(10/052ms) (01:41:31.013) (4)[ 2] : END(10) (01:41:31.236) (5)[1 ] : END(12) (01:41:31.236) (5)[ 2] : START(11/758ms) (01:41:31.236) (6)[1 ] : START(13/449ms) (01:41:31.686) (6)[1 ] : END(13) (01:41:31.686) (7)[1 ] : START(10/250ms) (01:41:31.937) (7)[1 ] : END(10) (01:41:31.937) (8)[1 ] : START(14/184ms) (01:41:31.995) (5)[ 2] : END(11) (01:41:31.995) (6)[ 2] : START(12/837ms) (01:41:32.122) (8)[1 ] : END(14) (01:41:32.122) (9)[1 ] : START(10/608ms) (01:41:32.731) (9)[1 ] : END(10) (01:41:32.833) (6)[ 2] : END(12) (01:41:32.833) (7)[ 2] : START(14/041ms) (01:41:32.875) (7)[ 2] : END(14) (01:41:32.875) (8)[ 2] : START(10/672ms) (01:41:33.548) (8)[ 2] : END(10) (01:41:33.548) (9)[ 2] : START(11/397ms) (01:41:33.946) (9)[ 2] : END(11) #### TEST 5 ==> time 05562ms
すると、処理1と2は同時に実行されるようになります。スケジュール単位はObserveOnで区切られるため、処理の追い抜きが発生しない範囲で並行処理が行われるようになりました。
総論:タスク並列とデータ並列の使い分け
Rxに限らずアクターモデル系技術の根幹の思想は「如何に並行処理を感じさせずに非同期処理をプログラム出来るか」です。「実行は非同期化されること」「同期処理のような直感的なコードが書けること」をフレームワークが保証するからこそ、デバッグが容易な非同期処理を実装できるようになっています。
処理の流れにいつ終わるかわからない非同期処理が挟まれる場合においても、コードを分散させずに処理の流れを記述する事が出来ます。また、パイプ内の処理は直列化されますので同期を心配する必要はありません。
そのかわり、処理順序にこだわる必要が無い場合においても1パイプ内で処理が直列化されますので、CPUをぶんぶん回すような並行処理分散が行われるわけではありません。
もう一つの並行処理技術としてPLINQの実装がありますが、こちらは逆にデータ並列モデルを採用しています。大量のデータをなるべくCPUを遊ばせることなしに処理させることが目的であるため、PLINQでは最終結果に影響しない限り、処理順序に拘りません。コード記述者は処理順序が影響しない単位でデータ処理を分割することを心がけることで最大の効果を発揮する事が出来ます。
タスク並列アプローチでも適切な関数を準備すればデータ並列的な処理分散は可能なのですが、.NETのアプローチは2つの役割を明確に分離しているようで、見たところRxには2つのパイプをまとめる関数はあっても、複数のパイプに分散したり実行順序を守らなくていい処理分散を明示的に行う関数群は用意されていないようです。負荷分散に関してはPLINQが専門家ですね。
幸いどちらもAPIの見た目は似ていますし相互乗り入れも可能です。利用者はどちらか一方だけを盲目的に使うのではなく、処理の内容により適切な技術を選ぶのが.NET流の並行処理プログラミングのスタイルじゃないかと思いました。
次が遠くなるとき
技術屋とコンテンツ屋の意識の違い、大きいのは「設計が古くなることへの不安」の感じ方だと思います。
設計が古くなる、というのはソフトウェアのコードが古くなること自体もありますが、むしろ「新しい技術の取り込みがしんどくなる」事を指すことが多いです。
設計思想の理想と現実
設計思想はその時々の技術トレンドによって変わりますが、基本的には今の技術を見据えつつ今後数年に出てくるであろう新技術をなるべくなら「簡単に」取り込めることを理想とするものです。
ただ、理想は理想。アプリケーションは「世に出すことが正義」である以上、あまり設計を追い求めてもいけない。伺かの根底の設計は「世に出すこと」を優先した部分がかなり色濃く残っています。それでも当初は基本設計さえぶち壊す勢いでアジャイル開発(‥‥あー、このころはそんな言葉もありませんでしたね)が進んでいたので、「最新技術」は割と近い所に居続けました。
捨てるも地獄、進むも地獄
さて一方、「今、基本設計を全部捨てて進められるか?」、となると少し停滞状況。技術屋さんはともかく、全部捨てられたら古いコンテンツをどうするのかという問題が出る。古いものをサポートしつつ新しいものを取り込むのは、非常に骨が折れる作業です。特に、プログラムコードがコンポーネント化(部品になって入れ替えることができる仕組み)されていない部分のメンテナンス負荷は顕著になります。
バラバラ殺人事件
今の伺かの設計でコンポーネント化されていない部分は「ゴースト描画系」と「プロセス空間」です。
まず、描画系ですが。AIはうまく切り離されていますが描画に関してはプラットフォームが丸抱えしているため、簡単に拡張を行うことができません。この部分がAIと同じように何らかのコマンド通信だけで繋がっているのであれば、その先を丸ごと差し替えることでどうとでもなる部分があります。また、描画系のトレンドはまさしく日進月歩の領域であり、この分野について自力で最先端に居続けることはほぼ不可能。可能であればうまくオープンソースを取り込める下地が必要な分野です。
もう一つは「プロセス空間」。割と根本的な問題ですが、伺かは1つのプラットフォームにゴーストが複数存在する設計であるため、ゴースト同士の干渉が問題になります。SHIORIは汎用スクリプト言語を使えればかなり楽になるのですが、一方多くの汎用言語はプロセス空間より小さな単位で干渉しないような設計とはなっていません。1つのプラットフォーム内で、「知らない他人同士が作ったゴースト」が干渉せずに存在できる仕組みを構築するのは実は大変なのです。これが「デスクトップアプリが単発で数多く存在する割に、プラットフォームがほとんどない理由」といってもいいかもしれません。
今どきの設計トレンドは、「内部でゴースト毎に別プロセス空間を立ち上げる」となるでしょう。あるいは.NETであるならアプリケーションドメイン(.NETの仮想プロセス空間)で環境の干渉を封じ込める、となります。
求められる技術なのか?
ぶっちゃけるとデスクトップマスコットの分野に「生粋の技術屋」が生まれる可能性が低いんじゃないか?と。技術屋が自分で実装しちゃうと、1体完成させた時点で満足しちゃって、普通プラットフォーム展開まで考えないんでしょう。伺か以降、この分野に単発が多い理由はそんなところだと思ってます。
コンテンツは爆発だ!
なら、「伺的」分野は今後悲観なのか、というと。悲観はしてませんが楽観もしていません。中か外からやってくるかはともかく、この手の分野は突然爆発的に生まれてくる、あるいは生き返るものだと思ってます。互換とかあまり考えずに出てくるでしょう。
その時「伺か」がどうなるのか。たぶんコンバートなりなんなり、あっさり移っちゃうんじゃないかなー。技術屋もコンテンツ屋も楽しければいいんです、魂は消えないです、きっと。
少しずつ変わる道、改革を求める道、黒船がいきなり作る道、色々とあると思いますが、「窓のないゴーストの世界」はあり続ける。きっと大丈夫。
‥‥不安の話はどうなった?
「設計が古い」のは、すぐ解決する問題じゃない。伺かクラスのアプリを本気で再設計するなら、1年はフルタイムで仕事をできる位の人が中心にいないと難しい。普通に仕事で予算切るなら3〜4千万円以下にはならない。絵を含めた総予算でいえば一億の仕事です。だからこそ無責任に楽観でやってみよーなんて言うつもりはないですが、一方で技術力のある学生(またはニート‥‥はっ)が一旗揚げるにはちょうどいいコンテンツです。
なので、技術屋の不安はどーにもならないが、いつかいきなりぶっ飛んだ解決策が提示される、という結論。最新技術が遠い今の状況は、本質的には「伺か」がぶっ飛ばされるまで改善されない。ただしそれは悲観でも誰かが悪いわけでもなく、デスクトップマスコットという技術が持つ本質だと思ってます。表現の世界はいつでも大きな淘汰に晒されている。命が晒されるわけじゃなし、ネガティブにならずに、やりたいことができる船に乗っかればいいと思ってます。
そろそろ懇親会の締切り
うかべん大阪#6 9/18です。懇親会に参加予定の方は週末締切りとなりますので是非ご登録お願いします。正確には月曜日の夜に電話入れる感じです‥‥。
懇親会の方、今のところ27人の模様。50人まで行けるよ!(ってうかべん本会場のほうが40人でいっぱいの罠)
あ、それと、今回は私は完全な裏方です‥‥。よろしくっす。