Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第44回 非同期処理の作成

2021/8/30

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 × 対象外です。
VB2008 Visual Basic 2008 × 対象外です。
VB2005 Visual Basic 2005 × 対象外です。
VB.NET 2003 Visual Basic.NET 2003 × 対象外です。
VB.NET 2002 Visual Basic.NET (2002) × 対象外です。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

 

1.非同期処理の作成

インテリセンスに<待機可能>と表示される処理は、非同期で実行できるということはこれまでに説明しました。そして、Async/Awaitを使うことで、この非同期処理を簡単に扱えるようになることも説明しました。

今回は、この非同期処理を自分で作成する方法を説明します。まず簡単に概要を紹介しましょう。

自分で、<待機可能>な非同期処理を作成する方法は3つあります。

 

方法1は、Asyncを使った非同期メソッドの戻り値を Task または Task(Of T)にするということです。

次の例は、非同期でテキストファイルを検索する非同期メソッドです。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Function SeachFileAsync(fileName As String, searchKeyword As String) As Task(Of Integer)

    Dim allText As String = Await IO.File.ReadAllTextAsync(fileName)

    Dim position As Integer = allText.IndexOf(searchKeyword)

    Return position

End Function

この方法は、何かの既にある 待機可能な処理を使って、自分自身も待機可能にするという方法です。何か既存の待機可能な処理を利用することが前提になります。

 

方法2は、Async/Awaitに依存せずに、Task または Task(Of T)を返すメソッドを自作することです。戻り値が Task または Task(Of T)であればそれだけで待機可能になります。これはAwaitできる処理をゼロから自作する最も簡単な方法です。

 

方法3は、一定の要件を満たすクラスを戻り値にすることです。GetAwaiterメソッドの実装などいくつかの要件を満たすクラスを戻り値にするメソッドは待機可能になります。

 

この後、方法1と方法2についてもう少し詳しく説明します。方法3は一般的ではなく、使いこなすのは難しく、しかも、使用する必要はまずないため説明は割愛します。

 

2.タスクを生成する非同期メソッドの作成

2-1.戻り値のない非同期処理

Asyncで付きで定義した非同期メソッドはSubで定義するか、戻り値がTask または Task(Of T)のFunctionで定義する必要があります。

戻り値がない場合、 Sub で定義することは意味がなく、使いにくくなるので推奨されていません。Sub で良いと感じたら Sub ではなく Task を返す Function にしてください。。

Sub で定義した非同期メソッドを Task を返すFunctionに書き換えるのはとても簡単です。

次の例はシンプルな(説明用でしか使い道のない)非同期メソッドです。1秒待機した後にメッセージを非同期で表示します。

非同期メソッドなので名前の末尾に Async をつけています。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Sub TestAsync()

    Await Task.Delay(1000)

    Debug.WriteLine("1秒経ちました。")

End Sub

この例は特に戻り値がないので Sub で宣言しています。動作はしますがこれは非推奨です。

これをTaskを返すFunctionに書き換えるには次のようにするだけです。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Function TestAsync() As Task

    Await Task.Delay(1000)

    Debug.WriteLine("1秒経ちました。")

End Function

文字通り Sub を Function に変更し、戻り値として As Task を付けています。

何も Return していないのですが、Async/Await の効果で 自動的に Task が戻り値に設定されます。

戻り値に設定されるタスクはAwaitしている処理ではなく、Awaitより下にある処理です。この例では Debug.WriteLine がタスクとして戻り値に設定されます。

この処理は戻り値が Task なのでこれ自体が <待機可能> になります。ですから、呼び出し側ではAwaitを使って次のように呼び出すことができます。

VB2012 VB2013 VB2015 VB2017 VB2019


Await TestAsync()

Awaitが付いているので、この呼び出しはAsyncで定義されたメソッド内にしか記述できない点に注意してください。

 

戻り値がTaskのメソッドをAwaitを付けて呼び出すと戻り値を受け取ることはできないので、次のようにTask型の戻り値が返ってくる前提のプログラムはエラーになります。

VB2012 VB2013 VB2015 VB2017 VB2019

'この例はエラーです。
Dim t = Await TestAsync()

 

Awaitを使わないで呼び出せば本来の戻り値である Task を受け取ることができます。戻り値のTaskを使って状態を確認したり、待ち合わせをしたりという処理を記述することもできます。

たとえば、次の例では呼び出し側はこの処理の完了を待ちます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim t As Task = TestAsync()

t.Wait()

もっと簡単に次のように書くこともできます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


TestAsync().Wait()

ところが、非同期メソッドが Sub で定義されている場合は、呼び出し側はタスクを受け取れないので、タスクを使って待ち合せたり、何かタスクの制御をするということが一切できなくなります。

これが Sub が非推奨である理由です。

メモ メモ  - Return なしでの戻り値

本文中にさらっと書きましたが、Async/Await の効果でReturnしていないのに戻り値が設定されるんです。普段は戻り値を設定するには Return だということが頭に染みついているプログラマーも多いので、戸惑うかもしれません。ご注意ください。

 

2-2.戻り値のある非同期処理

非同期メソッドが実行する非同期処理自体に戻り値がある場合は、その非同期メソッドの戻り値をTask(Of T)で宣言します。

T は 非同期処理自体の戻り値の型です。

たとえば、次の非同期メソッドは、テキストファイル内で文字列を検索し、ヒットした位置を数値で返します。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Function SeachFileAsync(fileName As String, searchKeyword As String) As Task(Of Integer)

    Dim allText As String = Await IO.File.ReadAllTextAsync(fileName)

    Dim position As Integer = allText.IndexOf(searchKeyword)

    Return position

End Function

この例では、デフォルトの文字コードを使用するので、正常に検索するには対象のテキストファイルがUTF-8で保存されている必要があります。

メソッドの定義は As Task(Of Integer) となっていますが、Return しているの値は Integer です。Async/Awaitの効果で自動的に Task(Of Integer)が戻り値になるわけです。

このメソッドの宣言を As Integer に書き換えると、Visual Studio は自動的に As Task(Of Integer)に変更します。

ここで戻り値となるタスクは、やはりAwait より下にある部分です。つまり、Dim Position As Integer の行と Return position の行がタスク化されて戻り値になります。

Task(Of Integer)が戻っていくので、この非同期メソッドは<待機可能>であり、呼び出し元は次のように Await 付きでこの処理を呼び出すことができます。

VB2012 VB2013 VB2015 VB2017 VB2019

Dim position As Integer = Await SeachFileAsync("C:\temp\sample.html", "レムス")

If position > 0 Then
    Debug.WriteLine($"レムスは{position}文字目にあります。")
    'Debug.WriteLine("レムスは" & position & "文字目にあります。") '←VB2013以前の場合
Else
    Debug.WriteLine("レムスはありませんでした。")
End If

このプログラム中には Task や Task(Of T)がまったく記述されていないことに注意してください。仕組みとしてはもちろんタスクを使って実現される機能ですが、プログラムでは完全に隠蔽されています。

Await を使用しているので、この呼び出し元のメソッドにも Async を付ける必要があります。

 

Await を付けないで Task(Of T)の戻り値をそのまま受け取ることもできます。

VB2012 VB2013 VB2015 VB2017 VB2019

Dim t As Task(Of Integer) = SeachFileAsync("C:\temp\sample.html", "レムス")

t.ContinueWith(Sub(baseTask As Task(Of Integer))
                   Dim position As Integer = baseTask.Result
                   If position > 0 Then
                       Debug.WriteLine($"レムスは{position}文字目にあります。")
                       'Debug.WriteLine("レムスは" & position & "文字目にあります。") '←VB2013以前の場合
                   Else
                       Debug.WriteLine("レムスはありませんでした。")
                   End If
               End Sub)

見比べてみると Await を付けて呼び出す方が簡潔に書けて、タスクのことを何も記述しないで済みます。

Awaitを使わないで真の戻り値である Task(Of Integer) を受け取る方は、少し小難しいプログラムになってしまいますが、タスクを直接操作できるので、タスクを使った処理を行いたい場合には優れています。

非同期メソッドを使えば、どちらでもお好みの方で呼び出すことができるというわけです。

 

2-3.頭の体操

さて、1つ問題です。少し考えてみてください。

次のプログラムで DoSomethingAsyncメソッドの戻り値の値は何でしょうか?

VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Function DoSomethingAsync() As Task(Of Integer)

Await Task.Run(Function() 123)

End Function

どのように呼び出しても答えは同じですが、たとえば、次のように呼び出す場合を考えてみてください。

VB2012 VB2013 VB2015 VB2017 VB2019

Dim result = Await DoSomethingAsync

Debug.WriteLine(result) 

 

DoSomethingAsyncメソッドは、123を返す処理をAwaitしているだけの短いプログラムです。

このメソッド自体の戻り値は Task(Of Integer)となっています。

前述したように、 この場合、戻り値となるタスクは、Await より下にある部分です。ところが、Awaitより下には何も記述されていませんから、既定の値を返すだけの何の処理もないタスクが返されることになります。既定のというのは Integer の場合 0 のことですから、つまり「0を返すタスク」が返されることになります。

そうすると、Await DoSomethingAsync は、DoSomethingAsync の戻り値である「0を返すタスク」から戻り値の 0 をを取り出すので、result は 0 になるというわけです。

では、この場合、123 の方を戻り値にするにはどうしたらよいでしょうか?

次のようにします。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function DoSomethingAsync() As Integer

Return 123

End Function

(もはや非同期メソッドではありませんが、説明の連続性を重視して名前の末尾の Async は付けたままにしておきました)

タスクとして返したいのなら次の通りです。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function DoSomethingAsync() As Task(Of Integer)

Return Task.Run(Function() 123)

End Function

単純な値を返すタスクは FromResult メソッドで作ることもできます。次のプログラムでも同じ意味です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function DoSomethingAsync() As Task(Of Integer)

Return Task.FromResult(123)

End Function

 

Async/Awaitで記述するには?

この処理は本質的にAsync/Awaitでの記述になじみません。Async/Awaitは、何かAwaitして処理することがあって、それの後続タスクがReturnされる仕組みです。単純に123を返すという処理は、何の後続タスクもないし、その前処理となるタスクもないのでAsync/Awaitになじまないというわけです。

 

3.タスクを生成する通常のメソッドの作成

3-1.戻り値のない非同期処理

非同期実行したい処理を実行するTaskを作成して、そのTaskを戻り値とするだけで、そのメソッドは<待機可能>になります。

たとえば、次の処理はデバッグ出力にメッセージを非同期で出力します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function OutputMessage(message As String) As Task

    Dim t = Task.Run(Sub() Debug.WriteLine(message))

    Return t

End Function

これだけでインテリセンスのヒントにもちゃんと<待機可能>と表示されます。

ポイントは実施したい処理に戻り値がない場合でも、メソッドはそれを実行する Task を戻り値にする必要があるという点です。

Await はここで戻ってきた Task を自動的に処理してくれる仕組みなのです。

 

メッセージを表示するだけだと処理がすぐ終わってしまうので、指定されたフォルダーの中から、サイズが大きいファイルを順に10個取り出す処理を非同期で実行するようにしてみましょう。ここでは取り出されたファイルをデバッグ出力します。

このサンプルでは、使用している EnumerationOptionsクラスが .NET Core 2.1以上から導入されたクラスのため、Visual Studio 2017以上でフレームワークを.NET Core 2.1以上または.NET Core 5以上にしないと動作しません。

VB2017 VB2019

Private Function Find10LargestFiles(folderPath As String) As Task

    Dim t = Task.Run(Sub()
                         Dim folder As New IO.DirectoryInfo(folderPath)
                         Dim options As New IO.EnumerationOptions()
                         options.IgnoreInaccessible = True 'アクセスできないファイルは無視します。
                         options.RecurseSubdirectories = True 'サブフォルダーも検索対象にします。

                         Dim largestFiles = From file In folder.EnumerateFiles("*", options)
                                            Order By file.Length Descending
                                            Take 10

                         Debug.WriteLine(largestFiles.Count & " 個見つかりました。")

                         For Each largeFile In largestFiles
                             Debug.WriteLine($"{largeFile.FullName} {largeFile.Length / 1024 / 1024:0.0} MB")                          
                         Next

                     End Sub)

    Return t

End Function

このプログラムではDirectoryInfoクラスのEnumerateFilesメソッドを使ってファイルを検索します。EnumerateFilesメソッドは戻り値としてFileInfoのコレクションを返します。第1引数に "*" を指定しているのですべてのファイルが検索対象となります。

第2引数にEnumerationOptionsを設定することで、検索の動作を細かく設定します。具体的に設定している項目は2つで、IgnoreInaccessibleプロパティをTrueに設定することでアクセスできないファイルを無視するようにします。この設定がないと、検索範囲内にアクセスできないファイルが存在した場合に例外が発生します。また、RecurseSubdirectoriesプロパティもTrueにすることでサブディレクトリーも検索対象に含めるようにします。

 

Taskを返しているので、このメソッドは<待機可能>になります。次のようにAwait を使って呼び出すことができます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Sub TestAsync()

    Await Find10LargestFiles("C:\Program Files")

End Sub

この処理は指定するフォルダーによっては少し時間がかかると思うので、コンソールアプリケーションで試してみる場合は、非同期処理が終わる前にアプリケーションが終了してと思います。そうならないように、実際に動作させる場合は、Console.ReadLine などでプログラムが終わらないように間を持たせてください。

たとえば、次のような感じです。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Sub Main(args As String())
    TestAsync()
    Console.WriteLine("出力ウィンドウのデバッグ出力を確認して下さい。")
    Console.ReadLine()
End Sub

 

3-2.戻り値のある非同期処理

非同期処理が何らかの戻り値を返す場合、その値の型を型パラメーターにしたTask(Of T)を作成して、それを戻り値とするメソッドを作ると<待機可能>になります。たとえば、Integer型の値を返す非同期処理であれば、Task(Of Integer)を使用します。

単純なたし算の例を紹介します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function Compute(x As Integer, y As Integer) As Task(Of Integer)

    Dim t = Task.Run(Function() x + y)

    Return t

End Function

下記はポイントを図で示したものです。

 

呼び出し側は、Await を使っている場合、Integer を直接戻り値として受け取ることができます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Sub AsyncTest()

    Dim result As Integer = Await Compute(2, 3)

    Debug.WriteLine($"計算結果は{result}です。")
    'Debug.WriteLine("計算結果は" & result & "です。") '←VB2013以前の場合

End Sub

Await を使わない場合は、本来の戻り値であるTask(Of Integer)を受け取ります。

Awaitの力で極力 Task の存在が隠蔽されるというわけです。

さきほど紹介したサイズの大きい10個のファイルを検索する処理を書き換えて、見つけた10個のファイルを戻り値で返すようにすると次のようになります。

VB2017 VB2019

Private Function Find10LargestFiles(folderPath As String) As Task(Of IEnumerable(Of IO.FileInfo))

    Dim t = Task.Run(Function()
                         Dim folder As New IO.DirectoryInfo(folderPath)
                         Dim options As New IO.EnumerationOptions()
                         options.IgnoreInaccessible = True 'アクセスできないファイルは無視します。
                         options.RecurseSubdirectories = True 'サブフォルダーも検索対象にします。

                         Dim largestFiles = From file In folder.EnumerateFiles("*", options)
                                            Order By file.Length Descending
                                            Take 10

                         Return largestFiles

                     End Function)

    Return t

End Function

呼び出し側は、Awaitと組み合わせることで戻りを IEnumerable(Of IO.FileInfo)として受け取ることができます。

つまり、FileInfoのコレクションということなので、戻りをそのままFor Eachして検索で見つかったファイルを取り出すことができます。

VB2015 VB2017 VB2019

Private Async Sub TestAsync()

    Dim files = Await Find10LargestFiles("C:\Program Files")

    For Each file In files
        Debug.WriteLine($"{file.FullName} {file.Length / 1024 / 1024:0.0} MB")
    Next

End Sub

 

 

4.Async/Awaitによる非同期処理の注意点

4-1.変数の共有

ここまでAsync/Awaitを使った非同期処理について説明してきました。どれも簡単なサンプルで紹介しているので、非同期処理がごく簡単に扱えるように思えたかもしれません。それは間違いです。

非同期で処理が実行されるということはそれ自体が難しい問題を秘めています。第42回のマルチスレッドの説明で、マルチスレッドの難しいポイントして下記の4つを取り上げました。

Async/Awaitを使うとどうなるか見てみましょう。

 

非同期で処理が実行されるのに同じ変数を使うということは本質的にプログラムの難易度を挙げます。

次のプログラムで CountUpAsyncメソッドは変数 count に1を10000回 たします。

AsyncTestメソッドはこれを2回呼び出していますが、結果として 変数 count の値はいくつになるか考えてみてください。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()
    Dim t1 = CountUpAsync()
    Dim t2 = CountUpAsync()

    't1 と t2 が終わるまで待機します。
    Task.WaitAll(t1, t2)

    Debug.WriteLine("結果=" & count)

End Sub

Private count As Integer = 0

Private Async Function CountUpAsync() As Task

    Await Task.Run(Sub()
                       'count に +1 を 10000回実行します。
                       For i As Integer = 0 To 9999
                           count += 1
                       Next
                   End Sub)

End Function

20000 になる場合もありますが、16895などの値になることもあります。2つの非同期処理が同時に変数にアクセスするので、値の取得と更新のタイミングが重なるとうまく更新できない場合があるということです。これはAsync/Awaitを使わないタスクベースの非同期処理でも同様で、第42回で詳しく説明しています。

手軽な解決方法は 第42回で説明したのと同様 SyncLock を使うことです。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()
    Dim t1 = CountUpAsync()
    Dim t2 = CountUpAsync()

    't1 と t2 が終わるまで待機します。
    Task.WaitAll(t1, t2)

    Debug.WriteLine("結果=" & count)

End Sub

Private count As Integer = 0
Private countUpLocker As New Object

Private Async Function CountUpAsync() As Task

    Await Task.Run(Sub()
                       'count に +1 を 10000回実行します。
                       For i As Integer = 0 To 9999
                           SyncLock countUpLocker
                               count += 1
                           End SyncLock
                       Next
                   End Sub)

End Function

 

4-2.コレクション

コレクションの列挙中にコレクションの内容を変更することはできません。Async/Await を使った処理でもこの原則が変わることはありません。次の例では高確率で例外が発生します。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()
    Dim items As New List(Of String)

    'Windowsフォルダー配下の全ファイルのフルパスをコレクションに追加します。
    items.AddRange(IO.Directory.GetFiles("C:\Windows"))

    'Program Filesフォルダー配下の全ファイルのフルパスをコレクションに非同期で追加します。
    FindFilesAsync(items, "C:\Program Files")

    For Each item In items
        Debug.WriteLine(item)
    Next '←高確率で InvalidOperationException が発生します。

End Sub

Private Async Function FindFilesAsync(items As List(Of String), path As String) As Task

    Await Task.Run(Sub()
                       'pathのフォルダー配下の全ファイルのフルパスをコレクションに追加します。
                       items.AddRange(IO.Directory.GetFiles(path))
                   End Sub)

End Function

ただし、VBのコンパイラーは賢いので、このプログラムが何かおかしいぞということを感じ取って警告を表示してくれます。

FindFilesAsync(items, "C:\Program Files") の部分に緑の波線が表示され、次のように警告されます。

BC42358 この呼び出しは待機されなかったため、現在のメソッドの実行は呼び出しの完了を待たずに続行されます。呼び出しの結果に Await 演算子を適用することを検討してください。

警告に書かれているようにずばり Await を適用するか、または、戻り値に Wait メソッドを適用するなどすれば、列挙中にアイテムが追加されることはなくなります。

初級講座第42回で、タスクベースの非同期処理でこの問題を説明した時は、コレクションのコピー機能を利用して回避する方法やSyncLockについても説明しており、Async/Awaitを使っている上記の場合でもそれらの回避方法は同じです。

 

4-3.例外

Await しているタスクで例外が発生した場合、Await はその例外を呼び出し元に再スローしてくれます。そのため、呼び出し元のTry~Catchで例外の発生を検出することができます。Awaitなしでタスクを実行している場合は、単にTry ~ Catch しただけでは例外を捕捉できないので、この点は大きな違いです。

下記のプログラムを使って、Try ~ Catch で囲んだ部分で中の処理で例外が発生した場合にどうなるか確認してみましょう。

このプログラムは「通常のプログラム」「通常のラムダ式」「Awaitなしのタスクを実行」「Await付きのタスクを実行」の4パターンで例外が発生する型変換を実行します。同時には試せないで1つずつコメントを解除して実行してみます。(見やすいように色はコメント化されていないときの色にしておきました。)

VB2012 VB2013 VB2015 VB2017 VB2019

Try

    '通常のプログラム
    'Dim result = CInt("ABC")

    '通常のラムダ式 ※ () を付けて即座に呼び出しています。
    'Dim result = (Function() CInt("ABC"))()

    'Awaitなしでタスクを実行
    'Dim t = Task.Run(Function() CInt("ABC"))

    'Await付きでタスクを実行
    'Dim result = Await Task.Run(Function() CInt("ABC"))

Catch ex As Exception
    Console.WriteLine("例外発生" & ex.GetType.FullName)
End Try

 

どうなるかは Visual Studio で通常の実行(デバッグ実行)する場合と、デバッグなしで実行する場合とで変わります。

また、Try ~ Catch がない場合の反応も合わせて結果を表にまとめました。

  通常のプログラム
/通常のラムダ式
Awaitなしで
タスクを実行
Await付きで
タスクを実行
Visual Studio で
デバッグ実行
Catchの処理が実行される エラー発生個所で停止 エラー発生個所で停止
デバッグなしで実行 Catchの処理が実行される 例外を捕捉できない Catchの処理が実行される
Try~Catch なしで
Visual Studio で
デバッグ実行
エラー発生個所で停止 エラー発生個所で停止 エラー発生個所で停止
Try~Catchなしで
デバッグなしで実行
アプリケーションはエラーで終了 例外を捕捉できない アプリケーションはエラーで終了

この表で Catchの処理が実行される場合、捕捉される例外はすべて System.InvalidCastException です。

Visual Studioのデバッグ実行時に例外をどうするかはオプションで変更できるので、この表ではデフォルトの設定の場合を扱っています。なお、動作確認は .NET 6.0 プレビューで作成したコンソールアプリケーションをVisual Studio 2022 Preview 2.1 で実行することで行いました。

ポイントは2つあります。

1つ目のポイントは、通常のプログラムと Await を付けた場合の反応はほぼ同じということです。Visual Studio のデバッグ実行時の反応に違いがあることだけ押さえておけば、通常のルールを覚えておくだけでよさそうです。これはさすが言語機能として導入されたAwaitの効果だと思います。

2つ目のポイントは、Awaitなしの場合、例外を全く捕捉できていないという点です。通常はアプリケーションがエラー終了するケースでも、何事もなかったかのように振る舞うので、Awaitなしでタスクを扱う場合は、例外発生時に何が起こるか注意深く実装/テストする必要があります。

タスクから例外を検出する方法は 初級講座第42回 で説明しています。

プログラムの過程ではまず通常の機能を作ってから、例外発生時の処理を考えるとことがよくありますが、Awaitなしでタスクを扱っている場合は、はじめから例外を意識したほうが良さそうです。

 

4-4.スレッドアフィニティ

Async/Await を使ったマルチスレッドではスレッドアフィニティの面倒を見てくれるため、GUIの操作を通常のプログラムのように書くことができます。

念のためAwaitを使わないで、GUIを操作して問題が発生する例から確認しておきましょう。

次のWindowsフォームアプリケーションのプログラムでは、Buttonクリックすると3秒後にTextBox1に"OK"を表示しようとするものですが、実行すると3秒後にエラーになります。エラーになる理由はGUIを生成したスレッドと、GUIを操作するスレッドが異なるからです。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Task.Delay(3000).ContinueWith(Sub()
                                      TextBox1.Text = "OK"
                                  End Sub)

End Sub

Async/Awaitを使った非同期メソッドはVBがうまい具体に面倒を見てくれるので、このエラーは発生しません。

次のように書きます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim t = UpdateUiAsync()

End Sub

Private Async Function UpdateUiAsync() As Task
    Await Task.Delay(3000)

    TextBox1.Text = "OK"
End Function