VB のたまご

作成日: 2017/10/01, 更新日: 2017/10/01


非同期処理その3(2017年10月版)


Async / Await + Task での動作変更

  •  以降の処理で登場するいくつかのクラスは、それぞれ以下の名前空間を使いますので、あらかじめインポートしておきます。 都度、各メソッドには書きませんので注意してください。
  • Imports System.Threading ' Thread.Sleep()
    Imports System.Threading.Tasks ' Task クラス
    Imports System.Collections.Concurrent ' ConcurrentXxx クラス(コレクション、ディクショナリ...)
    

  •  ここからは、Async / Await + Task の組み合わせによる扱い方を見ていきます。

  •  まずは Task を返すメソッドの作り方です。
  • ' Async / Await + Task
    
    ' Task を返すメソッドの作り方
    ' 語尾に Async と名付けて分かりやすくする(慣例であり強制ではない)
    
    ' 戻り値無し
    Public Function Test28Async() As Task
        Return Task.Delay(1000)
    End Function
    
    Public Async Function Test29Async() As Task
        Await Task.Delay(1000)
    End Function
    
    ' 戻り値あり
    Public Function Test30Async() As Task(Of Integer)
        Return Task.Run(Function() 1)
    End Function
    
    Public Async Function Test31Async() As Task(Of Integer)
        Return Await Task.Run(Function() 1)
    End Function
    

  •  前回説明した通り、戻り値あり・無しとは別に、Task の状態を管理する必要があるため、戻り値無しの処理内容であっても Task を返せるように、メソッドは Function 形式として作成します。 また、慣例としてメソッド名の語尾に Async を追加して、メソッド名を見ただけで Task を扱っているメソッドだという事が判断できるようにします。

  •  それぞれ Return で返していますが、戻り値無しかつ Async / Await の Task 返却の場合のみ(Test29Async)、Return は付けない、という書き方となります。 明示しなくても、自動的に Task が戻っていく仕組みなのでしょうが、Function と付いているメソッドなのに、Return を書いてはダメというのはちょっと落ち着かないですね。

  •  余談ですが、このとき無理やり Return を付けると、「この Async メソッドの 'Return' ステートメントは、関数の戻り値の型が 'Task' であるため、値を返すことができません。 関数の戻り値の型を 'Task(Of T)' に変更することを検討してください。」と言われてしまいます。

  •  Async / Await を付けたメソッドでよくある事例が、戻り値を返さないのだから、Task を返す必要はないのでは?という点です。
  • Public Async Sub Test32Async()
        Await Task.Run(Sub()
                           Thread.Sleep(1000)
                           Console.WriteLine("aaa")
                       End Sub)
    End Sub
    
    Public Async Function Test33Async() As Task
        Await Task.Run(Sub()
                           Thread.Sleep(1000)
                           Console.WriteLine("bbb")
                       End Sub)
    End Function
    
    Sub Main()
    
        Test32Async()
    
        Dim t = Test33Async()
        t.Wait()
    
        Console.ReadKey()
    End Sub
    

  •  これを実行すると、何の問題も無くプログラムが進みます。非同期処理と Async / Await が組み合わさって、意図した同期処理として動かすことができています。 両者の違いは、Task を戻しているか・いないかの違いだけですね。処理結果とは別に、タスクの面倒を見るか・見ないかの違いです。

  •  これは、部下に仕事を振る場面を考えると分かりやすいです。仕事をお願いして、その仕事が終わったら、依頼した側は報告してきてほしいと思いますよね。 これを元にした後続の仕事が控えているかもしれないですし。この時に、部下が報告してこなくてもいいタスクか・どうか、ということです。

  •  これを考えると、Task は返した方がいいです。自分が作ったメソッドを他の人が使うならなおさらです。メソッドの中で Await を使って待機しているのだからいいじゃん別に。 という考え方は、プログラム上はそうかもしれないですが、使う人からするとこういうメソッドは使いたくないと思うのではないでしょうか。 外部との連携を考えていない(協調性が無い)、つまり責任を持って作られていない、または放棄しているみたいで。 ちょっとひどく言いすぎたかもしれません、すみません。

  •  また、特例として、イベントハンドラだけはシグネチャ上変えることができないため、Async Sub のままでよいことになっています。 Function に変えてしまうとイベントと関連付けられなくなってしまいますからね。

  •  続いて、入れ子のタスクについてです。 Task.Run() を使った場合と、Task.Factory.StartNew() を使った場合とで、戻り値に違いがあります。タスクが入れ子になっている・いないの違いです。
  • ' Unwrap と Await
    Public Sub Test34()
    
        ' Task.Run() は、入れ子になったタスクであっても、戻り値を最低限のタスクまではがして戻す
        ' 戻り値は、Task(Of Integer)
        Dim t1 As Task(Of Integer) = Task.Run(
            Function()
                Return Task.Run(Function() 2)
            End Function)
    
        ' Task.Factory.StartNew() は、素の状態のまま返す
        ' 戻り値は、Task(Of Task(Of Integer))
        Dim t2 As Task(Of Task(Of Integer)) = Task.Factory.StartNew(
            Function()
                Return Task.Factory.StartNew(Function() 2)
            End Function)
    
    
    
        ' 1階層増やす
    
        ' 戻り値は、Task(Of Integer)
        Dim t3 As Task(Of Integer) = Task.Run(
            Function()
                Return Task.Run(Function()
                                    Return Task.Run(Function() 1)
                                End Function)
            End Function)
    
        ' 戻り値は、Task(Of Task(Of Task(Of Integer)))
        Dim t4 As Task(Of Task(Of Task(Of Integer))) = Task.Factory.StartNew(
            Function()
                Return Task.Factory.StartNew(Function()
                                                 Return Task.Factory.StartNew(Function() 1)
                                             End Function)
            End Function)
    
    End Sub
    

  •  結果に責任を持つのはいいのですが、戻り値を受け取る側からすると、一個ずつ確認するのはちょっと大変です。 このような場合に、入れ子のタスクをはがす Unwrap メソッドが用意されています。
  • ' Unwrap メソッドで、不必要なタスクを減らす
    Public Sub Test35()
    
        ' 戻り値は、Task(Of Task(Of Task(Of Integer)))
        Dim t4 As Task(Of Task(Of Task(Of Integer))) = Task.Factory.StartNew(
            Function()
                Return Task.Factory.StartNew(Function()
                                                 Return Task.Factory.StartNew(Function() 1)
                                             End Function)
            End Function)
    
        ' タスクが入れ子の場合、UnWrap メソッドを通して、1つ分のタスクをはがすことができる
        Dim t5 As Task(Of Task(Of Integer)) = t4.Unwrap()
        Dim t6 As Task(Of Integer) = t5.Unwrap()
    
        ' 入れ子のタスクにのみ対応している
        'Dim i7 As Integer = t6.Unwrap() ' 'Unwrap' は 'Task(Of Integer)' のメンバーではありません。
        Dim i8 As Integer = t6.Result
    
    End Sub
    

  •  注意点として、Unwrap メソッドが使えるのは入れ子のタスクに対してのみということです。 Task(Of Integer) のように入れ子ではないタスクに対しては使えませんので注意してください。

  •  今度は、Await を使った場合を見てみます。
  • ' Await で、不必要なタスクを減らす
    Public Async Sub Test36()
    
        ' 戻り値は、Task(Of Task(Of Task(Of Integer)))
        Dim t4 As Task(Of Task(Of Task(Of Integer))) = Task.Factory.StartNew(
            Function()
                Return Task.Factory.StartNew(Function()
                                                 Return Task.Factory.StartNew(Function() 1)
                                             End Function)
            End Function)
    
        ' タスクが入れ子の場合、Await を通して、1つ分のタスクをはがすことができる
        Dim t5 As Task(Of Task(Of Integer)) = Await t4
        Dim t6 As Task(Of Integer) = Await t5
    
        ' 最後のタスクにも対応している
        Dim i7 As Integer = Await t6 ' Task(Of Integer) から Integer へ
    
        ' こういう風にも書けるし正しいのだが、
        ' 記述ミスと誤認してしまいそう、または処理内容が把握しづらい(そんなこと無い?)
        Dim i8 As Integer = Await Await Await t4
        Dim i9 As Integer = t4.Result.Result.Result
    
        Dim i10 As Integer = t4.Unwrap().Unwrap().Result
        Dim i11 As Integer = Await t4.Unwrap().Unwrap()
    
    End Sub
    

  •  タスク変数の前に Await と付けることで、Result プロパティを取り出すことができます。入れ子タスクの場合、Unwrap メソッド同様に1つ分のタスクをはがすということですね。 また、Await の場合は、入れ子ではないタスクに対しても有効で、Result プロパティの値を取得することができます。

  •  最後に、Await・Result・Unwrap() を続けて書いた場合の比較を書きました。 Await と Result を続けて書くと作った人は分かるかもしれないですが、他の人が読んだときに把握しづらく感じないかなというのが私の感想です。 この部分は人それぞれだと思いますので、好きな書き方でいいのかもしれないですね。

  •  続いては、例外エラーの違いについてです。
  • ' 例外処理
    Public Sub Test37()
    
        Dim t = Task.Run(Sub()
                             Throw New ArgumentException("aaa")
                         End Sub)
    
        Try
            t.Wait()
        Catch ex As AggregateException
            For Each inner In ex.Flatten.InnerExceptions
                Console.WriteLine($"AggregateException")
                Console.WriteLine($"Message: {inner.Message}, Type: {inner.GetType().Name}")
            Next
        Catch ex As Exception
            Console.WriteLine($"Exception")
            Console.WriteLine($"Message: {ex.Message}, Type: {ex.GetType().Name}")
        End Try
    
        ' AggregateException
        ' Message: aaa, Type: ArgumentException
    
    End Sub
    

  •  Task 内で発生した例外エラーは複数同時発生する可能性があるため、まとめて AggregateException としてスローされることを今まで見てきました。 しかし、Await で待機すると最初に発生した1つ分の例外エラーしか補足してくれないという違いがあります。
  • Public Async Function Test38Async() As Task
    
        Dim t = Task.Run(Sub()
                             Throw New ArgumentException("aaa")
                         End Sub)
    
        Try
            Await t
        Catch ex As AggregateException
            For Each inner In ex.Flatten.InnerExceptions
                Console.WriteLine($"AggregateException")
                Console.WriteLine($"Message: {inner.Message}, Type: {inner.GetType().Name}")
            Next
        Catch ex As Exception
            Console.WriteLine($"Exception")
            Console.WriteLine($"Message: {ex.Message}, Type: {ex.GetType().Name}")
        End Try
    
        ' Task を Await すると、AggregateException ではなくなる
        ' Exception
        ' Message: aaa, Type: ArgumentException
    
    End Function
    

  •  全ての例外エラーの原因を捕捉できなくなりますので、注意が必要です。

  •  非同期処理その4(2017年10月版)に続きます。