やまたのおろちが酒飲んでます。あいかわらずの飲兵衛です。
その1
http://blogs.wankuma.com/esten/archive/2008/12/03/162607.aspx
ここでは、ただ首一本がガブガブ飲むだけだったんだけど、それじゃ他の首が飲ませろとうるさいので、全部の首がせーのっ!で飲めるようにして
その2
http://blogs.wankuma.com/esten/archive/2008/12/09/163043.aspx
となったわけだけれども、コメントでツッコミがあったように、これだけでは全く持って、ダメダメプログラミングなわけですw。はい、わざとです。
では、どうしようか? リファクタする?
でもその前に、まだやっておかなくちゃいけないことがあるんだわ。
やまたのおろちが飲みたい酒ツボが1つだったときにどうなるかって話。
Imports System
Imports System.Threading
Module OrochMTA
Delegate Sub DrinkingSAKE()
'酒ツボは一つ
Private sakeTubo As Integer
'酒ツボの残り
Private sakeNokori As Integer
<MTAThread()> _
Public Sub Main()
'酒ツボに酒を準備
sakeTubo = 50000
sakeNokori = sakeTubo
'本体で首が別々に酒飲みするのを身構える
Dim ts As New ThreadStart(AddressOf OrochiDrinking)
Dim workerThread As New Thread(ts)
workerThread.SetApartmentState(ApartmentState.MTA)
workerThread.Start()
workerThread.Join()
Stop
End Sub
Private Sub OrochiDrinking()
'まずは首を八本で飲んでみる
Dim headCount As Integer = 8
'酔っ払い待ち行列を首の数だけ準備
Dim yoppa(headCount - 1) As WaitHandle
For i As Integer = 1 To headCount
'飲んだ首から酔っ払いへと
yoppa(i - 1) = HeadDrinking(i).AsyncWaitHandle
Next
'全部の首が酔っ払いになるまで待機
WaitHandle.WaitAll(yoppa)
End Sub
Private Function HeadDrinking(ByVal headNo As Integer) As IAsyncResult
'指定した番号の首に酒を飲ませる
Dim myHead As New OrochiHead(headNo)
Dim gubi As New DrinkingSAKE(AddressOf myHead.Drink)
'酒飲み開始とともに、酔っ払い待ち行列へ戻す
Return gubi.BeginInvoke(Nothing, Nothing)
End Function
'おろちの首をクラス化
Private Class OrochiHead
Private meNumber As Integer
'何番目の首なのかを保存
Public Sub New(ByVal headNo As Integer)
meNumber = headNo
End Sub
'酒を飲むメソッド
Public Sub Drink()
Dim sakeZuki As New Thread( _
New ThreadStart(AddressOf DrinkSAKE)) '
sakeZuki.Start()
sakeZuki.Join()
Console.WriteLine("ぷはーっ! {0}番目の首、飲み終わり!", meNumber)
End Sub
' 酒のみスレッド
Private Sub DrinkSAKE()
Dim totalDrunk As Integer = 0
Dim nowDrink As Integer
Dim oneDrink As New Random
Dim oneBreath As New Random
'酒ツボの中の酒を飲み干す
While (sakeNokori > 0)
'一回飲むたびに息継ぎ
Thread.Sleep(oneBreath.Next(30, 60))
'一回ぐび
nowDrink = oneDrink.Next(1, 100)
'全部でどれくらい飲んだ?
sakeNokori -= nowDrink
Console.WriteLine("{0}番目の首が {1} リットル ぐびっ : 残り {2} リットル", meNumber, nowDrink, sakeNokori)
End While
End Sub
End Class
End Module
ぱっと見、これだけに見えるよね?
でも、これが落とし穴。
CPUの数が多い時、スレッドはどのCPUで管理されるかはOS次第。でもそのバラバラに動いているスレッドの処理が一箇所のアドレスにある値を読み出して書き出すわけだから、「今ある酒の量が本当に正しい」かどうか判らなくなる。これが小さなスレッドと一瞬で終わるプログラムなら気がつかないかもしれない。でも、沢山のCPU、スレッドで動いたときこの落とし穴ははっきりと結果にでてくるんだよね。
つまり、一つの酒ツボから複数の首が酒を飲むわけだけど、実際に、首の動きを制御しているのは、CPU。だから、CPUが複数あると、本当の意味で「同時に」首が酒を飲むことができるわけで、そうなると、酒ツボから飲んだ→残りこんだけ、という動きも本当の意味で「同時に」起きちゃうことになるの。そうなっちゃうと、今の酒の残り、は本当に正しい!ということがこのままだといえなくなっちゃうんだ。たとえば残り100リットルから、ある首Aが20リットル飲んで、ある首Bが10リットル飲んだ。この時、本当だったら残りは70リットルのはずなんだけど、2個のCPUがあるときに、Aの「100-20」の計算を1CPU、Bの「100-10」の計算を1CPUがそれぞれに同時にしちゃったら結果は「80」もしくは「90」として格納、これ、プログラム的には正しい動作なんだもの。見えないだけで、実はやまたのおろちの首はおもいっきり絡まってて、絡まったなりに酒を飲んだツモリになってたりしてるってことだよね。
厄介なのは、これ、必ず再現できると限らないんだ。
VisualStudioのデバッガをつかってブレークポイントを仕掛けて止めただけでも、マルチスレッドプログラミングは直列の処理に最適化されてしまうので、ステップするとうまくいっちゃったりとかする。「なんで?何が悪いの?」がさっぱり判らなくなっちゃう。
おそろしいよね、それって。
だから、スレッド間でやり取りする値、読み出したり書き出したり更新したりする時には、必ずスレッド間で同期を取れるように作ってあげないといけない。
マルチスレッドが難しいのはこの考え方があるからで、実はこれを回避するための処理クラスやステートメントが、ちゃんと用意されてたりするのね。
それが、InerLockedクラス、そして、SyncLockステートメントブロック
じゃ、こいつらを使って、やまたのおろちに行儀良くお酒を飲んでもらおうと思う。ついでに、リファクタってやつもやってみるってことで、それは次回ねw
実はまだ出てない新キャラがいるんだ、スサノオってのがwww