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

第42回 マルチスレッド

2021/8/16

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

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.同期処理と非同期処理

何か処理を実行している間に、別の処理を実行することを非同期処理と呼びます。

これに対して、1つの処理が終わってから次の処理を実行することを同期処理と呼びます。

VBでは通常のプログラム実行は同期処理で行われます。

VB2012のときに導入されたキーワード Async (読み方:Async=エイシンク) と Await (読み方:Await=アウェイト)を使用することで非同期処理も簡単に記述できます。しかも、ただ簡単に記述できるだけではなく、非同期処理を実行したいほとんどの場合でこのキーワードを使うのが最適です。

これを使うと、何かのダウンロードや巨大なファイルの圧縮やディスクの読み書きなどしている間に別の処理を実行させることができるようできるようになり効率的です。

UIのあるアプリケーションでは、何か時間がかかる処理をやっていたとしてもユーザーが操作できるようにしないと使いにくいので、同期処理が好ましくないことがしばしばあります。もし、Webブラウザーが通信中は操作不能になるようだったらどんなに扱いにくいか想像してみてください。

それに同期処理はコンピューターのリソースを効率的に使えなくしてしまいます。時間がかかる処理というのはたいてい何かを待っているだけで、コンピューターのCPUやメモリなどには余力があることがほとんどです。これを別の処理にうまく活用できないのはもったいないということです。

 

まずは同期処理でファイルをダウンロードする例を紹介します。

次のプログラムはHttpClientクラスのGetByteArrayAsyncメソッドを使って約15MBあるファイルををインターネットでダウンロードします。サンプルなのでダウンロードするものは何でもよいのですが、ここではMicrosoftが公開しているMDACという古いプログラムのインストーラーをダウンロードしています。実行するにはAsyncTestメソッドを呼び出してください。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()

    Download()

    Debug.WriteLine("ダウンロード完了")

End Sub

Private Sub Download()

    '▼ダウンロード
    'MDAC 2.8 SDK 約15MBをダウンロードします。
    'ダウンロードした内容は変数 content に格納されます。
    Dim url As String = "https://download.microsoft.com/download/9/a/1/9a1256c9-d301-4fdc-93b9-370c5b2f9827/mdac28sdk.msi"

    Dim client As New Net.Http.HttpClient
    Dim content As Byte() = client.GetByteArrayAsync(url).Result

    '▼保存
    Dim fileName As String = "C:\temp\mdac28sdk.msi.download"
    IO.File.WriteAllBytes(fileName, content)

End Sub

AsyncTestメソッド内では、まずDownloadを呼び出して、Downloadの処理が終わったら「ダウンロード完了」と表示します。同期処理なので、Downloadメソッドの実行完了して(つまり、実際にダウンロードが完了して)からこのメッセージが表示されます。

これを非同期版に書き換えると次のようになります。

VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()

    DownloadAsync()

    Debug.WriteLine("ダウンロード中です。")

End Sub

Private Async Sub DownloadAsync()

    '▼ダウンロード
    'MDAC 2.8 SDK 約15MBをダウンロードします。
    'ダウンロードした内容は変数 content に格納されます。
    Dim url As String = "https://download.microsoft.com/download/9/a/1/9a1256c9-d301-4fdc-93b9-370c5b2f9827/mdac28sdk.msi"

    Dim client As New Net.Http.HttpClient
    Dim content As Byte() = Await client.GetByteArrayAsync(url)

    '▼保存
    Dim fileName As String = "C:\temp\mdac28sdk.msi.download"
    IO.File.WriteAllBytes(fileName, content)

    Debug.WriteLine("ダウンロード完了")  

End Sub

いくつか変更点があります。最も重要なAsyncとAwaitは赤字で示しました。非同期処理するメソッドは名前の最後に Async を付けることになっているのでDownloadメソッドの名前をDownloadAsyncに変更しています。この名前は機能とは関係がないので、Downloadのままでも非同期で動作します。もう1つ小さいポイントですが、最初に紹介した同期版ではGetByteArrayAsyncメソッドをResultプロパティとともに使っていましたが、非同期版ではResultはなくなっています。この変更の理由はあとでゆっくり説明することにします。

メッセージの位置も変更しています。DownloadAsyncメソッドは非同期で実行されるため、呼び出し側のAsyncTestメソッドは、DownloadAsyncメソッドの実行がダウンロードが完了しなくても次の行へ処理を進めます。そのためAsyncTest内のメッセージを「ダウンロード中です。」にしています。

Awaitから下は、ダウンロード完了後に実行されるため、「ダウンロード完了」のメッセージはこの位置で表示します。

ただし、この例は簡易的に作成しているので、Awaitの行が何も待つ必要がなく一瞬で完了する場合は、「ダウンロード完了」と表示されてから「ダウンロード中です。」と表示される場合もあります。また、ダウンロードの完了を待たずにプログラムを終了させた場合は、ダウンロードは中断されます。

 

以上、簡単に処理の流れを説明しましたが、Async/Awaitを使うことで何が起こっているのか、どう使うべきかこれから順を追って説明することにします。

残念ながら Async と Await を付けただけで魔法のように非同期になるわけではなく、この技術を理解するのはなかなか難しいものです。まずは、タスクを使ったマルチスレッドについて理解しておく必要があります。AsyncとAwaitについてはいったん忘れてマルチスレッドから話を始めることにしましょう。

Async/Awaitについては次回に説明します。

 

2.マルチスレッド

2-1.スレッド

1つの命令を実行して、それが終わったら次の命令を実行すると…という流れのことを「スレッド」と呼びます。

最初から最後まで同期処理するアプリケーションは1つのスレッドで実行できることになります。

下記の図は処理を1つのスレッド上で処理が順番に実行されていく様子を模式的に表したものです。

 

非同期処理は、ある処理をやっているのとは別のところで(あるいは待っているのとは別のところで)、別の処理も順次実行していくということですから、複数のスレッドを使って実現されることが多いです。複数のスレッドを使うプログラミングをマルチスレッドと呼びます。

下記の図はマルチスレッドの処理を模式的に表しています。線が2つに分かれており、2つのスレッドが非同期(=お互い関係せずに)で動作しています。

コンピューターはCPUのコアやOS・フレームワークの支援を受けて処理を同時に実行できるようになっているので、複数の処理を同時に動かした方が効率がよくなる場合があります。

本来、コンピューターはCPUのコアの数だけ同時に処理を実行できます。1つのコアを分割して使うハイパースレッディングという技術を考えても、2021年現在通常のコンピューターで同時に実行可能な処理は8程度。かなり高性能なマシンを導入しても数十というところです。

しかし、OSや.NETの処理系がスレッドを抽象化してリソースとして管理しており、Windowsのタスクマネージャーでスレッド数を確認すると私の4コアのマシンでは2065個のスレッドが稼働していました。

このようにスレッドは二重三重に抽象化され、管理されているため、気が付かないうちに最初とは違うスレッドでプログラムが実行されている可能性もあります。同期処理しか実行していないつもりでも複数のスレッドを使っていることもありますから、マルチスレッドと非同期処理はイコールではありません。

Await は多くの場合、新しいスレッドを開始して、アプリケーションをマルチスレッドにします。Awaitで実行する処理によってはスレッドは生成されない場合もあります。

発展 発展学習  -  GPU

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

古典的にはコンピュータの中で演算処理を実行するのは CPU (中央演算処理装置)で、2021年現在でも、CPUがコンピューターの処理の中心であることは間違いありませんが、近年では画像処理用の演算装置であるGPUの用途をもう少し広げた演算に使うソリューションも主にクラウド(Azure,AWS,GCPなど)で登場しています。GPUはもともと並列処理が得意であることから、GPUを活用するソリューションでは物理的な同時処理数としてはCPUを凌駕します。

 

2-2.タスク

VBでは処理を非同期で実行するためにいくつかの方法があります。2021年時点で主流なのは、メソッド単位で非同期で実行する処理を表現するものです。ここでいうメソッドにはラムダ式でによる Sub や Function も含んでおり、手軽さの理由でラムダ式が活用されるケースがほとんどです。

もともとメソッドは意味のある処理の単位なので、これがそのまま非同期処理を表すのは直感点です。

このアプローチでは、非同期処理を表すメソッドのことを タスク と呼び、Sub を Taskクラス、Function を Task(Of T)クラスを使って表現します。

これを「タスクベースの非同期パターン」(TAP = Task based Asynchrounous Pattern)と呼びます。

参考

非同期プログラミングのパターン | Microsoft Docs

冒頭で紹介したAsync/Awaitもこのタスクベースの非同期パターンを活用します。

雑談 雑談  -Threadクラスはレガシーです

もともと .NET Framework 1.0 (2002年)  のころからスレッドを制御するためのその名も Thread (スレッド) というクラスがありました。その後 .NET Framework 4 (2010年) のときに、より便利な Taskクラス/Task(Of T)クラスが導入されたため、今でもThreadクラスを使うことはまずありません。

 

Taskを使って非同期処理を定義して、実行するもっとも簡単な方法は TaskクラスのRunメソッド(読み方:Run=ラン)を使うことです。

次の例は Debug.WriteLine をタスクとして(つまり、非同期で)実行します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim t As Task = Task.Run(Sub() Debug.WriteLine("Action"))

Task.Run は生成したタスクを戻り値として返します。タスクを生成して実行して後は放置するいわゆる「撃ちっぱなし」の場合は、この戻り値のTaskを受け取っても使い道はありませんが、タスクから戻り値を受け取ったり、タスクが実行中なのか完了しているのかなどステータスを確認したり、タスク完了時に何か後続処理を実施したりなど、タスクを制御する場合はこの戻り値のTaskを使用することになります。

ここでははじめてのTaskの例なので、わかりやすいように型を明示していますが、次のよう型推論に任せる方が一般的です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim t = Task.Run(Sub() Debug.WriteLine("Action"))

 

タスク自体に戻り値がある場合は、Task.Runの戻り値はTask(Of T)になります。型 T はタスクの戻り値の型です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim t1 As Task(Of Integer) = Task.Run(Function() 1 + 2)
Dim t2 As Task(Of String) = Task.Run(Function() "Hello!")

この例でも型推論を使って次のように書く方が一般的です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim t1 = Task.Run(Function() 1 + 2)
Dim t2 = Task.Run(Function() "Hello!")

 

タスクから戻り値を受け取るには Task(Of T)のResultプロパティを使用します。

次のように使います。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim t1 As Task(Of Integer) = Task.Run(Function() 1 + 2)
Dim t2 As Task(Of String) = Task.Run(Function() "Hello!")

Dim result1 As Integer = t1.Result
Dim result2 As String = t2.Result

でも、ちょっと考えてみてください。タスクは非同期で実行されているので、処理が完了してからでないと戻り値が取り出せないはずです。中には数分かかるような長い非同期タスクもあるかもしれません。

そこで、Resultプロパティは、タスクがまだ完了していない場合、タスクの完了を待ちます。数分かかるタスクに対し、実行直後にResultプロパティを使用すると、Resultプロパティの行で数分間待機することになります。使いどころを考えないと非同期で処理する意味がなくなってしまいます。

 

2-3.後続タスク

タスクの処理が終わったら、次の後続処理を実行したいという場合は、ContinueWithメソッド(読み方:ContinueWith=コンティニューウィズ)を使用します。

通常はプログラムは同期で実行されるので、1つ目の処理が終わったら、次の処理が実行される…という具体で、「後続処理」ということをあまり気にしませんが、タスクは非同期で実行されるので、いつ終了するのかはタイミングしたいです。

そこで、タスクがいつ終了してもその後続を実行する ContinueWith を使います。

次のように使用します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim t1 As Task = Task.Run(Sub() Debug.WriteLine("最初の処理"))

Dim t2 As Task = t1.ContinueWith(Sub(baseTask As Task) Debug.WriteLine("後続処理"))

Dim t3 As Task = t2.ContinueWith(Sub(baseTask As Task) Debug.WriteLine("さらにその後続処理"))

この例では、最初のタスクが Task.Run で開始されます。その後 ContinueWith で次のタスクが設定されます。最初のタスクが終了すると自動的にこのタスクも実行されます。さらに、その後でもう1回 ContinueWith で別のタスクを紐づけています。

ContinueWithはRunと同様に引数に実行したい処理をラムダ式などで指定します。そのラムダ式に引数があり、上記の例では baseTask As Task として宣言しています。この引数には親タスクが渡されてくるので、親タスクの状態を確認したい場合などに利用できます。

なお、上記の例はわかりやすいように型を明示していますが、すべて型推論が可能ですので、慣れてきたプログラマーは次のように書くことが多いと思います。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim t1 = Task.Run(Sub() Debug.WriteLine("最初の処理"))

Dim t2 = t1.ContinueWith(Sub(baseTask) Debug.WriteLine("後続処理"))

Dim t3 = t2.ContinueWith(Sub(baseTask) Debug.WriteLine("さらにその後続処理"))

さらに、Taskを生成するたびに変数に代入する必要はないので、メソッドチェーンを使って次のように書くことを好むプログラマーが大半です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Task.Run(Sub() Debug.WriteLine("最初の処理")).
    ContinueWith(Sub(baseTask) Debug.WriteLine("後続処理")).
    ContinueWith(Sub(baseTask) Debug.WriteLine("さらにその後続処理"))

 

念のためもう1点補足しておきます。ここでは、例をシンプルにするために Debug.WriteLine するだけの特に意味のない処理を記述しています。これだけの処理であれば ContinueWith を使わずに1つのタスクで次のように済ませてしまうのが現実的です。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Task.Run(Sub() 
             Debug.WriteLine("最初の処理")
             Debug.WriteLine("後続処理")
             Debug.WriteLine("さらにその後続処理")
         End Sub)

 

2-4.待ち合わせ

1つのタスクが終了するのを待つには Wait メソッド を使用します。また、前日したように戻り値があるタスクではResultプロパティを使うと終了を待ってから戻り値を取得できます。

この他に、Taskクラスの共有メソッドには複数のタスクの終了を待つ機能があります。

Task.WaitAll メソッドは、引数に指定したすべてのタスクが完了するまで待機します。

次の例では非同期でファイルの検索を2つ実行し、両方終了するまで待機します。それぞれのタスクにはContinueWithで後続タスクを指定しており、それぞれ検索終了後結果を表示します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim windir = Environment.GetFolderPath(Environment.SpecialFolder.Windows)

'Windowsフォルダー直下から notepad.exe を検索します。
Dim t1 = Task.Run(Function()
                      Return IO.Directory.EnumerateFiles(windir, "notepad.exe").FirstOrDefault("なし")
                  End Function).
                  ContinueWith(Sub(t) Debug.WriteLine("検索結果1:" & t.Result))

'Windowsフォルダー直下から calc.exe を検索します。
Dim t2 = Task.Run(Function()
                      Return IO.Directory.EnumerateFiles(windir, "calc.exe").FirstOrDefault("なし")
                  End Function).
                  ContinueWith(Sub(t) Debug.WriteLine("検索結果2:" & t.Result))


'両方の検索が終了するまで待機します。
Task.WaitAll(t1, t2)

Debug.WriteLine("タスク t1 と t2 の両方が完了しました。")

WaitAll メソッドにはタスクの終了を待つ限度をタイムリミットとして指定する機能もあります。

また、複数のタスクのうち、どれか1つが完了するのを待つWaitAnyメソッドもあります。

 

3.マルチスレッドの注意点

3-1.変数の共有

以上、ごく簡単にTask/Task(Of T)を使ってマルチスレッドでプログラムを動作させる方法を説明しました。この例だけ見ると特に難しいことはなく、簡単にマルチスレッドが実現できるように見えると思いますが、実際にはいろいろと複雑な問題があります。その一端を紹介しておきます。

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

AsyncTestメソッドはこの、CountUpを2つのスレッドで非同期に呼び出します。2つのスレッドが実行するので、結果は20000になりそうですが、たいていの場合、20000になりません。私の環境で試しに実行してみると20000になることもありますが、19494などの値になることもありました。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()
    Dim t1 = Task.Run(Sub() CountUp())
    Dim t2 = Task.Run(Sub() CountUp())

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

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

End Sub

Private count As Integer = 0

Private Sub CountUp()

    'count に +1 を 10000回実行します。
    For i As Integer = 0 To 9999
        count += 1
    Next

End Sub

 

なぜこのようなことが起こるのか考えてみましょう。

count += 1 のように一見シンプルな処理でもコンピューターはいくつかのステップを踏みます。まず、countの現在の値を取得して、定数1をロードして、2つを合計して、その結果を変数countに書き込むという具合です。

2つのスレッドでこれらの処理を同時に実行しているため、おかしなことが起こります。たとえば、スレッド1とスレッド2で同時に値を取得することがあるかもしれません。スレッド1ではその取得した値に+1します、スレッド2でも同じです。そうすると2つのスレッドでそれぞれ +1 をしたのに、結果として +2ではなく、+1しかされないということが起こります。

単純なたし算ですらこの難しさです。マルチスレッドの難しさの一端が伝わったでしょうか?

VBにSyncLock ~ End SyncLock (読み方:SyncLock=シンクロック)ステートメントを使用すると、その間の処理は1つのスレッドでしか実行できなくなるのでこの現象を防げます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Sub AsyncTest()
    Dim t1 = Task.Run(Sub() CountUp())
    Dim t2 = Task.Run(Sub() CountUp())

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

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

End Sub

Private count As Integer = 0
Private countUpLocker As New Object '←追加

Private Sub CountUp()

    'count に +1 を 10000回実行します。
    For i As Integer = 0 To 9999
        SyncLock countUpLocker '←追加
            count += 1
        End SyncLock '←追加
    Next

End Sub

SyncLockステートメントは、プログラムの中で複数使用できるので、区別するためにObject型の変数を指定することになっています。同じObject型の変数が指定されているSyncLock~End SyncLockは実行できるスレッドは1つだけになります。言い換えると、SyncLock~End SyncLockのブロックがプログラムの2か所にある場合、両方が同じ変数を指定しているならば、この2つのブロックのどちらか一方を実行中のスレッドがあれば、両方のブロックを他のスレッドは実行できなくなり待たされます。両方がそれぞれ別の変数を指定しているならば、片方のブロックを実行中のスレッドがあっても、もう片方のブロックは別のスレッドが実行できる可能性があります。

 

3-2.コレクション

配列やList・Dictionaryなどのコレクションは列挙処理中に項目の追加・削除ができません。

これはマルチスレッドでなくても発生します。次のプログラムで確認できます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim items As New List(Of String)({"Apple", "Banana", "Cat", "Dog"})

For Each item In items
    If item = "Cat" Then
        items.Add("kitten") '←InvalidOperaionException
    End If
Next

通常はループの中で追加・削除等するので、すぐに問題に気が付けますが、マルチスレッドの場合、コレクションの列挙中に全然別の場所でコレクションを変更する操作を行ってエラーになる可能性があります。これはなかなか気づきにくい問題ですし、タイミングにもよるので、エラーになる場合もあればならない場合もあるという具合になります。

 

たとえば、次のプログラムは同じエラーが高確率で発生しますが、For Eachで列挙処理している場所と、項目を追加している場所が離れているため、上のシンプルな例に比べてはるかに問題があることに気が付きにくくなっています。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim items As New List(Of String)

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

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

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

実際のプログラムではもっと離れた場所に問題があるケースも多いと思うので、コレクションの列挙操作はには要注意というわけです。

 

For Each等するすべての場所で、列挙操作中にコレクションが変更されないことを確認するのはかなり大変ですが、うまい逃げ道があります。

多くのケースでは、コレクションをコピーして、コピーを列挙することで、問題を解決できます。

たとえば、次のように ToList メソッドを使うとエラーになりません。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim items As New List(Of String)({"Apple", "Banana", "Cat", "Dog"})

For Each item In items.ToList
    If item = "Cat" Then
        items.Add("kitten") '←OK
    End If
Next

なぜなら、items.ToListメソッドは、itemsをコピーした別のListを作成するメソッドなので、列挙操作はコピーに対して実行され、Addは本体に対して実行されるので、列挙しているコレクションとAddしているコレクションが別になるからです。CloneメソッドやToArrayメソッドで代用することもできます。

コピーを作ってから列挙操作を開始することになるので、本体を列挙するよりも処理が増えますが、サイズが大きくないコレクションなら念のためすべての列挙操作をコピーに対して行うようにしても良いかもしれません。

この対策が採れない場合、次の手は SyncLock です。列挙操作と変更操作の両方をSyncLock~End SyncLockで囲めば、どちらかの処理を実行中にもう片方の処理を実行するということはできなくなります。が、ここだけ見るとこれはもうマルチスレッドで処理を実行している意味がなくなってしまいます。

 

3-3.例外処理

例外処理も特殊です。

非同期で実行されている処理をTry ~ End Try で囲んでも例がはキャッチできません。

なぜならば、Try ~ End Try と 非同期で実行されている処理は別のスレッドだからです。

次の例は数値変換できない "ABC" を数値に変換する処理を別スレッドで実行するようにプログラムしています。ここでは InvalidCastException が発生します。しかし、別スレッドの例外なのでCatch はできません。

この例ではConsole.WriteLineを使っているのでコンソールアプリケーションを前提としています。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    Dim t = Task.Run(Function() CInt("ABC"))
Catch ex As Exception
    Console.WriteLine("例外発生" & ex.GetType.FullName)
End Try

この場合、通常の処理(=マルチスレッドでない処理)ならば、Catch 節の Console.WriteLine が実行されるはずですが、Visual Studio は CInt("ABC") のところで例外が発生したことを通知してデバッグモードに移行します。

デバッグなしで実行しても、Console.WriteLine のメッセージは出力されず、何事もなかったようにプログラムの残りの部分が実行されます。この例外が発生したスレッドは例外で停止しますが、非同期で実行されているためプログラムはこのスレッドを放置して先に進んでいくというわけです。(.NET Framework 4 の場合、放置するとアプリケーション全体が停止します。)

操作方法 デバッグなしで実行

デバッグなしで実行するには、ビルドして生成されたexeファイルを直接実行するか、Visual Studio の [デバッグ] - [デバッグなしで開始]をクリックします。よく設定されているショートカットは Ctrl + F5 です。

 

Taskで例外が発生していることを知る方法はいくつかあります。簡単なのはTaskのExceptionプロパティを見ることですが、非同期時で実行されているので見るタイミングに困るかもしれません。別の方法としてTaskの終了を待機するWaitメソッドやResultメソッドなどを使うことでも例外の発生を知ることができます。これらのメソッドで待機しているタスクで例外が発生するか、または既に発生していると、System.AggregateException (読み方:AggregateException=アグリゲートエクセプション)という専用の例外が発生します。

次の例をデバッグなしで実行すると 例外発生System.AggregateException と表示されます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    Dim t = Task.Run(Function() CInt("ABC"))
    t.Wait()
Catch ex As Exception
    Console.WriteLine("例外発生" & ex.GetType.FullName)
End Try

AggregateExceptionは、マルチスレッド環境下での例外を表現するための入れ物のような例外です。通常の例外クラスはその発生原因となった例外をInnerExceptionプロパティで取得できますが、AggregateExceptionの場合、マルチスレッド環境下で複数スレッドの例外を表現できるように、InnerExceptions (読み方:InnerExceptions=インナーエクセプションズ)という末尾に s が付いているプロパティがあり、これを使って実際に発生した例外を取り出せます。

次のように発生した例外を取り出せます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Try
    Dim t = Task.Run(Function() CInt("ABC"))
    t.Wait()
Catch ex As AggregateException
    For Each singleEx In ex.InnerExceptions
        Console.WriteLine("例外発生" & singleEx.GetType.FullName)
    Next
End Try

この例をデバッグなしで実行すると、例外発生InvalidCastException と表示されます。

 

マルチスレッドの例外処理はなかなか難しく、いくつかの実装パターンがあり、.NETのフレームワークにも機能があります。

 

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

WindowsフォームアプリケーションやWPFなどのGUIを持つアプリケーションでは、GUIを操作できるのはGUIを生成したスレッドだけであるというルールがあります。このように、特定のスレッドでしか特定の処理が実行できないということがあり、これをスレッドアフィニティと呼びます。

たとえば、WindowsフォームアプリケーションでTaskを使ってTextBoxに値を設定しようとする次のプログラムはSystem.InvalidOperationException の例外が発生します。メッセージには次のように表示されます。

'有効ではないスレッド間の操作': コントロールが作成されたスレッド以外のスレッドからコントロール 'TextBox1' がアクセスされました。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

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

    Dim t1 = Task.Run(Sub()
                          TextBox1.Text = "OK"
                      End Sub)

End Sub

Windowsフォームの場合、この問題を回避するためにInvokeという専用のメソッドが用意されており、この例では TextBox1.Text = "OK" の行を Invoke(Sub() TextBox1.Text = "OK") に書き換えるだけでうまく動作します。

マルチスレッドの観点ではスレッドアフィニティを維持するために、SynchronizationContext クラスの機能が使用できます。このクラスのPostを呼び出すと、SynchronizationContext が生成されたスレッドでコードを実行してくれます。次のようにします

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

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

    Dim uiThread = Threading.SynchronizationContext.Current

    uiThread.Post(Sub()
                      TextBox1.Text = "OK"
                  End Sub, Nothing)

End Sub

 

3-5.その他

以上で取り上げたほかにもマルチスレッドの難しいポイントはあります。く問題になるのは複数のスレッドの待ち合わせや、スレッド間の協調、スレッドの処理を途中でキャンセルして完了を待たずに安全に終了させる方法などです。掘り下げるとまだまだこんなものではなく、熟練のプログラマーでもマルチスレッドには頭をひねります。

また、この章で取り上げた4つの注意点では、これらの簡単な回避方法は説明したものの、実際のプログラムではいろいろな要件があるため、今回紹介した回避方法では不十分なことも多々あるはずで、要件に応じた回避方法を考えることが求められます。

 

4.まとめ

今回は冒頭で Async と Await を使うと簡単に非同期処理を実行できることを紹介しました。

そして、AsyncとAwaitの説明をする前にマルチスレッドの知識があった方が良いので、TaskとTask(Of T)を使ってマルチスレッドのプログラミンを行う方法を簡単に説明しました。

最後に、下記の4点のマルチスレッドの難しさを挙げました。

 

これで準備が整ったので次回はAsync/Awaitについてもう少し説明します。

そして、マルチスレッドの難しい点がどうなるかも見てみることにします。先に言っておくとAsync/Awaitを使えば難しいことを何も考えなくてよいというわけではありません。これらが難しい理由は非同期で処理が実行されるという本質に由来するからです。そこで、難しいポイントを把握したうえでそれをAsync/Awaitではどう注意すればよいか理解しておくことが重要になります。

 

このように悩ましいことが多いので、私はマルチスレッドや非同期処理というものが好きではなく、全部同期処理でシングルスレッドで実行したいのですが、時代の流れは非同期処理にあり、HttpClientなど比較的新しいオブジェクトには、非同期処理が前提となっているものもあります。

それに、確かに待ち時間が長い処理を非同期で実行して、その間も別の処理を実行できるというのは大きな魅力です。

 

雑談 雑談  - 本当は説明するつもりではなかったんですが…

初級講座でマルチスレッドを扱うつもりはなかったのですが、Async/Awaitの簡単な説明は必要かなと思い、最初 Async/Awaitの説明を書き始めたんです。書いているうちに、さきにTaskの説明をしておかないと、説明しにくいということに気が付き、Taskの説明をしようとしたら、マルチスレッドの話のようになってしまい、ちょっと初級向けではないような気もするのですが結局「マルチスレッド」という回になってしまいました。

マルチスレッドについて詳しく学びたい方は下記の記事を見てみてください。

Async および Await を使用した非同期プログラミング - Visual Basic | Microsoft Docs

また、「プログラミングC#」という本ではマルチスレッドや非同期処理について詳しく説明されており、言語はVBではありませんが、同じ.NETなのでかなり参考になります。本格的にマルチスレッドに取り組みたい方は読んでみることをお勧めします。(下記のリンクから購入してもらうと私に紹介料が入ります。)

プログラミングC 第8版