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

第43回 非同期処理の呼び出し

2021/8/22

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

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

VBで非同期に処理を実行させる代表的な方法は Async(読み方:Async=エイシンク) と Await(読み方:Await=アウェイト) を使うことです。

Async・Awaitのすごいところは、これがフレームワークの機能ではなく、Visual Basicという言語の機能であり、簡単に使えるということです。

Visual Basic 言語の機能なので、Async・Await を記述するとコンパイラーは本来プログラマーが考慮する必要があることを自動的に組み込んでコンパイルを行ってくれます。

Asyncはメソッドやラムダ式の定義に付加します。プロパティやコンストラクターには付けることができません。Async自体は単なる目印で、特別な機能はありません。

Asyncが付けられているメソッドを「非同期メソッド」と呼ぶことがあります。ここでいうメソッドにはラムダ式で定義されたSub,Functionも含んでいます。

次のプログラムはAsyncを使ってメソッドを定義する例です。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Function DoSomethingAsync As Task

    '内容は割愛します。通常は Await を使ったプログラムを記述します。
    
End Function

ラムダ式の場合は、次のようになります。

VB2012 VB2013 VB2015 VB2017 VB2019

Dim doSomethingAsync = Async Function()
                           '内容は割愛します。通常は Await を使ったプログラムを記述します。
                       End Function

これらの非同期メソッドは中で Await を使ったプログラムを記述することが想定されています。

そのためAsyncで定義しているメソッド内 Await が使われていないと次のような警告が表示されます。

BC42356 この Async メソッドには 'Await' 演算子がないため、同期で実行されます。ブロック不可の API 呼び出しを待つには 'Await' 演算子を、バックグラウンド スレッドに CPU 主体の操作をするには 'Await Task.Run(...)' を使用することを検討してください。

これはエラーではないので、この警告を無視してプログラムを実行することはできますが、それだと、Asyncを使っている意味がないのでAwaitを使わない場合は、定義から Async を外しましょう。

Asyncを付けたメソッドには次の制約が発生します。

Clickイベントなどの自動生成されるイベントハンドラやエントリーポイントとなるMainメソッドにも必要であれば自分で Async の記述を追加することができます。

 

2.Await

2-1.Awaitによる非同期処理

Await は Async で定義されている非同期メソッド内でのみ使用できます。

処理がAwait に到達すると、そのAwaitがついている処理の実行が完了するまで非同期メソッドの実行が中断されます。

ここに最大のポイントがあって、処理が Await に到達すると、完了を待たずに即座に呼び出し元に制御が返り、呼び出し元の続きの処理が実行されます。つまり、Await を起点に、Awaitの処理の実行完了を待って続きを実行する流れと、Awaitの処理の完了を待たずに直ちに呼び出し元の次の処理を実行する流れとに処理が分かれます。これでそれぞれの流れは非同期で実行されるわけです。

この2つの流れはお互い別々に実行され、一方が実行中の場合でも、他方が同時に実行される可能性があります。

メモ メモ  - 分割されない場合もあります

この説明は代表的な場合の説明です。Awaitしている処理が即座に終了する場合など2つに分割されずにAwaitの下の処理が実行されてから、呼び出し元に戻る場合もあります。「Awaitするとマルチスレッドになる」と考えるのは、たいていの場合正解ですが、常に正解ではありません。

「Await」という英語は 何かを待つ という意味です。つまり、Awaitがついている処理の完了を待つという意味だと思うのですが、一方で、完了を待たないで即座に呼び出し元に戻るフローもあるので、なかなか誤解を招きそうな名前だなと思います。

 

もっと詳しく処理の流れを見てみましょう

①では、AsyncTestメソッドが呼ばれると、DownloadAsyncメソッドが呼ばれる。

②では、DownloadAsyncメソッドが呼び出されたので、このメソッドが上から順番に実行されます。この段階では通常の同期処理と同じです。

③に実行が到達すると、ここで Await が使用されているため、Awaitしている処理(GetByteArrayAsync)の実行が完了するまで、DownloadAsyncメソッドは続き(=⑤の部分)を実行しません。

プログラムの制御はAwaitしている処理(GetByteArrayAsync)の終了を待たずに即座に呼び出し元である AsyncTest に戻り、④が実行されます。

⑤の部分は、③の部分(Awaitしている処理)の終了後に実行されます。

④と⑤の処理はお互いに非同期で同時に実行される可能性があります。Awaitしている処理や④・⑤の処理にかかる時間によって実際に同時に実行されるかどうかは場合によります。

 

このプログラムには Task や Task(Of T) はまったく登場しませんが、Async/Awaitはタスクベースの非同期処理を使ってこの動作を実現しています。③のGetByteArrayAsyncの処理はタスクであり、⑤の部分は、③の処理の後続タスクです。③.ContinueWith(⑤) ということです。

 

 

2-2.Awaitできる処理の見分け方

Await は何にでも付けられるわけではありません。

Await できる処理はインテリセンスに <待機可能> と表示されるので簡単に見分けられます。

この位置に <待機可能> と表示されるものには Await を付けることができます。

戻り値がTask または Task(Of T)のメソッドはすべて<待機可能>です。自作の処理を待機可能にしたい場合には戻り値がTask または Task(Of T)であるメソッドを作成することになります。詳細は後述します。

<待機可能> が表示されない処理もたくさんあります。残念ながらAwaitをどんな命令や処理にでも付けられるというわけではありません。もし、非同期で実行したい機能が <待機可能> でないなら、Task.Run など Task を返すメソッドで囲んでみることを検討しましょう。

 

待機可能なメソッドは名前の末尾に Async を付けるルールになっていますので、これも目印になります。

ルールに従うと、 XXX という同期実行するメソッド(=つまり、通常のメソッド)にAwaitに対応した別ものがある場合、XXXAsync という名前になります。Asyncがついている非同期版のメソッドしか存在しない場合もあります。

ただ、この命名ルールは機能上の要件ではないため第三者が作った待機可能なメソッドの名前の末尾に Async がついている保証はありません。

また、マイクロソフト製のメソッドでも、Async/Awaitが登場する前に作られたメソッドで、名前の末尾に Async がついているものがあります。これは Async と付いてい入るけれども<待機可能>ではありません。

 

2-3.Awaitによる戻り値の取得

戻り値が Task のメソッドに Await を付けて呼び出した場合、その式は値を生成しません。

たとえば、Task.Delay メソッドは 戻り値が Task ですから次のように記述することは可能です。(ただし、この例は機能的には意味がありません。TAsk.Delayの機能については後で取り上げます。)

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim t As Task = Task.Delay(1000)

Task を返すので、このDelayメソッドは待機可能です。Await を付けると戻り値のTask は受け取れなくなります。

次の例はビルドエラーとなり「式は値を生成しません。」と表示されます。実際にこの例を試してみる場合は Async で定義したメソッド内のみAwaitが使用できるということもも思い出してください。つまり、この例はAsyncで定義された非同期メソッド内に記述する前提です。

VB2012 VB2013 VB2015 VB2017 VB2019


'この例はエラーです。
Dim t As Task = Await Task.Delay(1000)

Await を付けるのであれば戻り値を受け取らないで次のように呼び出します。

VB2012 VB2013 VB2015 VB2017 VB2019


'この例は正常に動作します。
Await Task.Delay(1000)

タスクの面倒は Await が見るから、プログラマーは気にするなということなのだと思います。

 

戻り値が Task(Of T)のメソッドの場合、Awaitを付けて呼び出すと、その式の値は T 型になります。

たとえば、Task.FromResultメソッドは Task(Of T)を返します。Awaitを使わないで次のように記述することは可能です。

Task.FromResultは単純な値を返すタスクを生成するメソッドです。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim t As Task(Of Integer) = Task.FromResult(5)

Await を付けると値は Task(Of T)ではなくなりますから、次の記述はエラーになります。

VB2012 VB2013 VB2015 VB2017 VB2019


'この例はエラーです。
Dim t As Task(Of Integer) = Await Task.FromResult(5)

Await を付けると式の値は Task(Of T) の T の型になりますから、次のように呼び出すことになります。

VB2012 VB2013 VB2015 VB2017 VB2019


'この例は正常に動作します。
Dim t As Integer = Await Task.FromResult(5)

この例では t の値は 5 になります。

 

3.待機可能な処理の呼び出し戦略

3-1.Awaitの効果

待機可能なメソッドは非同期実行される可能性があります。(逆に言うとだから<待機可能>なのです。)

待機可能なメソッドにAwaitを付けて呼び出すか、付けないで呼び出すかは、プログラムの都合に合わせて使い分けるのが良い方策です。

Await を付けると何が起こるのか、Await の効果を表にまとめました。

この表では「分岐」「待機」「戻り値の透過」という用語を使用していますが、これはこの記事内でだけ通用する独自のものです。

分岐
Await を付けてメソッドを呼び出すと、
その処理が完了する前に、呼び出し元に制御が戻る場合があります。その場合、結果としてプログラムの流れが2つに分割され、それぞれ非同期で実行されます。
待機
Await を付けてメソッドを呼び出すと、
Await より下の行は Await の処理が完了を待ってから実行されます。
(<待機可能>なメソッドは非同期で実行され得るので、Await しないと完了を待たないで先に進みます。)
戻り値の透過
Await を付けてメソッドを呼び出すと、
そのメソッドの戻り値の受け取り方が変わります。戻り値が Task のメソッドからは戻り値を受け取れなくなります。戻り値が Task(Of T)のメソッドからは、型 T の値を受け取ります。

 

3-2.待機可能な処理を Await なしで呼び出す

Await を付けないと、<待機可能>な処理の完了を待たずに処理は進みます。

Await を付ける場合と、付けない場合とで発生する違いを、 Task.Delayメソッド(読み方:Delay=ディレイ) を題材にもう少し詳しく確認してみましょう。

Delayメソッドは一定時間何もしないメソッドです。時間はミリ秒で指定します。

このメソッドは <待機可能> なメソッドです。メソッドが待機可能であるということは、そのメソッド自体に処理を非同期で実行する機能が組み込まれているということにご注意ください。(だから、<待機可能>なんです。)

次のプログラムではMessageメソッドを定義し、その中で Await なしで Delay を呼び出しています。

3秒後にメッセージを表示することを意図したプログラムですが、結論から言うとメッセージは3秒経たなくても即座に表示されます。

何が起こっているのかじっくり見てみましょう。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Message()

    Task.Delay(3000)

    '↓このメッセージは3秒経つ前に表示されます。
    Debug.WriteLine("3秒経ちました。")

End Sub

Task.Delayの呼び出しはAwait なしなので、Message メソッドは処理を順番に実行しようとします。Task.Delayメソッドは<待機可能>なので、この中では非同期処理が行われます。つまり、Task.Delay内部で処理の流れが2つに分割され非同期実行はされますが、Messaegメソッド はそんなことお構いなしに1つずつ処理を実行していくというわけです。

図示すると次のようになります。Task.Delay の内部で処理が2つに分かれます。1つは3秒待機する処理です。もう1つは、Task.Delayが即座に結果を返すので、続き(Debug.WriteLine)を実行しようとする Messageメソッド の流れです。

このように分割されて一方では3秒間待機し、もう一方では次の命令を実行するため「3秒経ちました。」というメッセージは3秒を待たずに表示されるわけです。

言葉遊びすると、<待機可能>な処理を Await で待機しなかったため、即座に処理が進行したということです。

 

 

待機可能な処理を、この例のように呼び出すことは無意味なことがほとんどです。実際にこの例でも3秒待つという処理は実際上何の効果もありません。

そこで、VBはこのような呼び出しを次のように警告する場合があります。

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

通常はこの警告を見たら、戻り値を受け取るか、Await を付けるか、Waitメソッドなどを呼び出すかします。レアケースではありますが、意図通りということであれば、この警告を無視しても大丈夫です。

Await と Waitメソッドについてはすぐ下でも紹介します。

 

 

3-3.待機可能な処理を Await を付けて呼び出す

Await を付けると、<待機可能>な処理が完了するまで続きの実行が待たされます。

今度は Await を付けてTask.Delayを呼び出してみましょう。次の例ではAwaitを使うために、メソッドの定義にAsyncも追加し、メソッドの名前も Message から MessageAsyncに変更しています。

Await には処理が完了するまで待機する効果があるので、この場合は Task.Delayの実行が完了するまで、Messageメソッド は処理を進めずに待ちます。

そのため、3秒経ってからメッセージが表示されるようになります。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Async Sub MessageAsync()

    Await Task.Delay(3000)

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

End Sub

 

メモ メモ  - ContinueWithでもできます

Taskの機能であるTask.ContinueWith (読み方:ContinueWith=コンティニューウィズ)を使っても同じことができます。このくらいシンプルなプログラムであればAwaitではなくContinueWithを好むプログラマーもいるかと思いますが、ContinueWithだとUI要素へのアクセスにひと工夫必要なのに対し、Awaitは普通にUI要素を操作できるという利点があります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Message()

    Task.Delay(3000).ContinueWith(Sub(t) Debug.WriteLine("3秒経ちました。"))

End Sub

 

ところが、Await や 囲み記事のContinueWith を使った途端、プログラムはマルチスレッドを前提にする必要があり、注意すべきことが増えますので、非同期実行ではなくても構わない場合にはAwaitやContinueWithを使用したくないと思うときもあります。つまり、同期実行したいということです。

 

3-4.待機可能な処理を 同期実行する

Await を使うことで、<待機可能>な処理の完了を待ってから次に進むことはできるようになりますが、Awaitには分岐機能もあるため、この待っている間に呼び出し元の処理が先に進んでしまいます。

完了を待機しつつ、呼び出し元に制御を返さないようにするには WaitメソッドかResultプロパティを使用するのが一般的です。この方法では<待機可能>なメソッドは完全に同期処理のように振る舞います。

WaitメソッドとTaks または Task(Of T)のメンバー で、文字通り、処理が完了するまで同期的に待機する機能です。ResultプロパティはTask(Of T)のメンバーで、処理が完了するまで待機してから、戻り値を取得する機能です。 Task.Delay の戻り値は Task なので Waitメソッドが使用できます。

つまり、<待機可能> な処理を同期実行のように呼び出すことができて、シンプルに考えることができるようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub Message()

    Task.Delay(3000).Wait

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

End Sub

アプリケーションの一部だけを非同期にしたい場合、非同期処理と同期処理の境界でこのWaitメソッドやResultプロパティを使って非同期の世界から同期の世界へ戻りたくなることがきっとあるでしょう。

 

3-5.デッドロック

WaitメソッドやResultプロパティを使って、待機可能な処理を待機させるアプローチにはデッドロックが発生するという重要な注意点があります。

WindowsフォームアプリケーションやWPFアプリケーション、ASP.NETなど、アプリケーションの開発に使用しているフレームワークによっては、コードがどのスレッドで実行されるかが重要な場合があり、Await で停止したメソッドの実行を再開するときに、元のスレッドで実行しようとします。

ところが、WaitやResultはこの元のスレッドの実行を停止して、非同期処理の完了を待ちます。

そうすると、Awaitの処理が完了して元のスレッドで処理を実行しようとしたときに、元のスレッドが停止中なので、元のスレッドの停止中が解除されるまで、処理が停まります。お互いがお互いを待って処理を停止してしまうわけです。

次のプログラムのAsyncTestメソッドをWindowsフォームアプリケーションで実行すると、デッドロックが発生することを確認できます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()

    Debug.WriteLine("A")

    DoNotDoThisAsync().Wait()

    Debug.WriteLine("B")

End Sub

Private Async Function DoNotDoThisAsync() As Task

    Debug.WriteLine("C")

    Await Task.Run(Sub()
                       Debug.WriteLine("D")
                   End Sub)

    Debug.WriteLine("E")

End Function

出力ウィンドウのデバッグには、 A C D まで出力されます。このあと B と E はいつまで待っても出力されません。

同じプログラムをコンソールアプリケーションで実行すると A C D E B と出力され、ちゃんとプログラムは正常に実行されます。

 

コンソールアプリケーションでは問題が発生しないというところが、また、わかりにくい点です。コンソールアプリケーションで試してうまくいったプログラムをGUIに移植しても動かないかもしれないということです。

 

デッドロックの問題を解決するには、アプリケーションの設計を見直す必要があるかもしれません。

GUIのスレッドで非同期処理を実行する必要がない場合、ConfigureAwaitメソッドを引数 False で呼び出した戻り値に対してAwaitすることで、フレームワークにスレッドにこだわりがないことを伝えることができます。この場合、非同期処理を元のスレッドで実行しようとはしないため、デッドロックが発生しないようにできます。たとえば、上述の例では次のようにします。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()

    Debug.WriteLine("A")

    DoNotDoThisAsync().Wait()

    Debug.WriteLine("B")

End Sub

Private Async Function DoNotDoThisAsync() As Task

    Debug.WriteLine("C")

    Await Task.Run(Sub()
                       Debug.WriteLine("D")
                   End Sub).ConfigureAwait(False)

    Debug.WriteLine("E")

End Function

このプログラムはGUIアプリケーションで実行しても、デッドロックにならず予期した通り完了します。

これにはおまけもあって、GUIで動作させる必要がない非同期処理では、スレッドの面倒を見なくてよくなるためパフォーマンスが向上します。

ただし、スレッドアフィニティの問題(≒GUIを操作できない)が発生します。

 

このデッドロックの問題はある程度大きなプログラムでは予見が難しいので、Wait/Resultは使用するべきではないと考える人たちもいます。

そもそも Await できるように <待機可能> になっているのだから、WaitやResultではなく、素直に Await を使えばいいじゃんということです。この考え方は理にかなっているのですが、これを推し進めると一部の処理が非同期処理である場合、そこが元となって、全体が非同期処理になってしまい、同期処理であれば気にしなくてよかった様々な問題(変数の共有、コレクションの変更、例外処理、待ち合わせなど)を全体的に考慮する必要があります。それは、かなりの難易度で手間がかかると考える技術者もいるでしょう。

私自身は、WaitやResultを便利によく使いますが、うっかりデッドロックさせてしまうこともよくあります。チームで開発しているときなどはチームのポリシーに従うとよいでしょう。

ひとまずはWaitやResultを使ってみたらプログラムが応答しなくなったというようなことがあればデッドロックを疑って見てください。