Thread.SleepとAsync.Sleepの違い

2017/07/08

F#の本で非同期処理について触れている個所では、Thread.SleepではなくAsync.Sleepを使って説明されていた。

この本

ところで、この2つは何が違うのか?

どうやら調べてみると、以下のような違いがあるらしい。

(1)Thread.Sleepは本当にスレッドの動作を停止するらしい

(2)Async.Sleepはスレッドの動きを停止するのではなく、指定された時間経過後に後続の処理をスレッドプールで再開する。

わかりにくいのが、Async.Sleepを使って以下のように記述した場合、

let func2 =
    async {
        (* ここで何かの処理を行う(前半) *)
        do! Async.Sleep 1000
        (* ここで何かの処理を行う(後半) *)
    }

結局のところ、前半の処理を行った後、上記であれば1秒間待ち合わせてから、後半の処理を行うことになるわけで、少なくとも表面的には、以下のようなコードと挙動に相違がない。

let func2 =
    async {
        (* ここで何かの処理を行う(前半) *)
        Thread.Sleep 1000
        (* ここで何かの処理を行う(後半) *)
    }

ただ、厳密にいえば、Async.Sleepを使った場合には、前半の処理と後半の処理とで、実行するスレッドが異なる場合があるので、スレッド毎に依存する何かを使っている場合には、アプリケーションから見える挙動に相違があるともいえる。

例えば、以下のようにすると、スレッドのIDが変わっていることが確認できる。

module  FTest
open System 
open System.Threading

let func2 =
    async {
        printf "%d\n" Thread.CurrentThread.ManagedThreadId
        do! Async.Sleep 1000
        printf "%d\n" Thread.CurrentThread.ManagedThreadId
    }

[<EntryPoint>]
let main(_) = 
    Async.Start func2
    Thread.Sleep 3600000
    0

実行結果はこうなる。




では、スレッドのIDが変わるかもしれない、ということ以外に何か相違はないのか? だとしたら、より直感的に動作してくれるThread.Sleepを使ったほうがいいのではないか? という気がしてくるが、どうやらそうではないらしい。


Thread.Sleepを使った場合は、スレッドを停止させてしまうため、.NETのスレッドプールは、ほかの処理を受け入れられるように追加でスレッドを生成するようだ。一方、Async.Sleepでは必要に応じて(つまり一定時間経過後に)タスクをスレッドプールに放り込むだけなので、スレッド数が増大することがないらしい。

だから、asyncワークフロー内で逐次的に処理されるんだったら一緒じゃねぇかと言って、非同期的なAPIを使わず同期的なAPIを使ってしまっては効率が低下するらしい。

ということで、ちょっと試してみた。

まずはThread.Sleepを使った場合。

module  FTest
open System 
open System.Threading

let func1 =
    async {
        Thread.Sleep 3600000
    }

[<EntryPoint>]
let main(_) = 
    for i = 0 to 10000 do
        Async.Start func1

    Thread.Sleep 3600000
    0

これを実行してしばらくリソースモニターを見ていると、スレッド数が増加し続けていく。




Async.Sleepを使った以下のようなコードでは、

module  FTest
open System 
open System.Threading

let func2 =
    async {
        do! Async.Sleep 3600000
    }

[<EntryPoint>]
let main(_) = 
    for i = 0 to 10000 do
        Async.Start func2

    Thread.Sleep 3600000
    0

スレッド数の増加がみられない。




上でも書いたが、なんでもこれはSleepに限らずI/Oの場合も同様らしいので、できるのであれば同期型のI/O命令を使用するのではなく、非同期型のI/O命令を使って、asyncないのdo!やlet!で待ち合わせるようにしたほうがいいらしい。そうでないと、過剰なスレッドの生成によるメモリの無駄遣いや、コンテキストスイッチの多発による性能の劣化が生じるのだという。


と、いうようなことが、.NET Flameworkの本に書いてあった。


この本


0 Comments:

コメントを投稿

Links to this post:

リンクを作成

<< Home