🖨️

手軽に始めるマルチスレッドプログラミング

2022/12/11に公開

スレッドとは

大雑把に言えばプログラムが処理する単位のようなものです。普通に作成したプログラムは一つのスレッドで処理されるため、重い処理を実施すると前々回前回の例のように描画が止まったりします。
そこで、重い処理を別スレッドで実施することで描画の停止を防いだり複数の処理を同時に実施することで処理効率を上げる方法がマルチスレッドになります。

マルチスレッドあれこれ

マルチスレッド処理自体は.NetFramework初期の頃から実装可能でした。それらを順に紹介いたしいます。

Thread

.NetFramework1.xの頃に実装された方法で一番原始的な方法となります。この当時はラムダ式といったものも無く、戻り値の受け取りが困難だった等あったこともあり、難易度の高いものであったと聞きます。

ThreadPool

スレッドの生成・破棄はかなりコスト(メモリや処理にかかる時間)がかかる為、使用したThreadを再利用するように考えられたのがThreadPoolです。DBのコネクションプールのようなイメージととらえればわかりやすいかと思います。

BackgroundWorker

.NetFramework2で登場したマルチスレッドの実装を簡易化した仕組みです。
プログラムのサンプルは前々回を参考にしてください。
BackgroundWorkerはマルチスレッドプログラミングの入門にはもってこいでしたが、スレッドの前処理・実処理・プログレス処理・後処理が分離されてしまい、繋がりが分かり辛くなってしまうという問題点もありました。

Task

Async/Awaitを使用しないTaskは.NetFramework4から登場しました。Taskは指定した処理を非同期で実施する仕組みです。その為ループの中で重いメソッドをTask実行すると、ループはスグに抜け、バックグラウンドで重い処理が順次処理されます。
以下のプログラムを実施してみてください。

  • Form1にボタン・プログレスバー・ラベルを一個ずつ配置
C#サンプルソース
Form1.cs
using System;
using System.Runtime.CompilerServices;
using System.ComponentModel;

public class Form1
{
    private MAX_COUNT = 100;

    private void Form1_Load(object sender, EventArgs e)
    {
        ProgressBar1.Hide();
        Label1.Hide();
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        try
	{
            Button1.Enabled = false;
            ProgressBar1.Maximum = MAX_COUNT;
            ProgressBar1.Value = 0;
            ProgressBar1.Step = 1;
            Label1.Text = string.Empty;

            // 処理実施
            ProgressBar1.Show();
            Label1.Show();
            for (var i = 1; i <= MAX_COUNT; i++)
	    {
                var wk = i;
                Task.Run(() => WaitProcess(wk));

                ProgressBar1.PerformStep();
                Label1.Text = $"({ProgressBar1.Value}/{MAX_COUNT})";
            }
        }
        finally
	{
            ProgressBar1.Hide();
            Label1.Hide();
            Button1.Enabled = true;

        }
    }

    private void WaitProcess(int i)
    {
        // 重い処理
        Threading.Thread.Sleep(500);

        Debug.WriteLine(i);
    }
}
VB.Netサンプルソース
Form1.vb
Imports System.ComponentModel
Public Class Form1
    Private MAX_COUNT = 100

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
        ProgressBar1.Hide()
        Label1.Hide()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Try
            Button1.Enabled = False
            ProgressBar1.Maximum = MAX_COUNT
            ProgressBar1.Value = 0
            ProgressBar1.Step = 1
            Label1.Text = String.Empty

            '*** 処理実施
            ProgressBar1.Show()
            Label1.Show()
            For i = 1 To MAX_COUNT
                Dim wk = i
                Task.Run(Sub()
                             WaitProcess(wk)
                         End Sub)

                ProgressBar1.PerformStep()
                Label1.Text = String.Format("({0}/{1})", ProgressBar1.Value, MAX_COUNT)
            Next

        Finally
            ProgressBar1.Hide()
            Label1.Hide()
            Button1.Enabled = True

        End Try
    End Sub

    Private Sub WaitProcess(i As Integer)
        '*** 重い処理
        Threading.Thread.Sleep(500)

        Debug.WriteLine(i)
    End Sub
End Class

上記プログラムを実行しButton1をクリックすると、一瞬プログレスバーが表示され、すぐに消えButton1が使用可能になり、その後もイミディエイトウィンドウには引き続き1~100まで順に出力されているのが確認できます。
このように、手軽に非同期処理を実施できる仕組みがTaskになります。

Task(Async/Await)

.NetFramework4.5から登場したのがAsync/Awaitです。Async修飾子を含むメソッドには一つ以上Awaitを含む必要があり、Awaitを含むTask処理を実施後続きの処理を実施するようにする仕組みです。Asyncを含むメソッドは非同期メソッドとも呼ばれるようですが、決してメソッドが非同期(別スレッド)で実施されるわけではないことに注意してください。その為、Asyncを含むメソッド内ではコントロール等に通常通りアクセスできます。
上記のように、Await付きでコールされたTaskは非同期処理ながら同期処理のように動作し、なおかつTaskは別スレッドで実施されるため、メインスレッドでは引き続き別処理を行えます。
以下のプログラムを実施してみてください。

・Form1にボタン・プログレスバー・ラベルを一個ずつ配置

C#サンプルソース
Form1.cs
using System;
using System.Runtime.CompilerServices;
using System.ComponentModel;

public class Form1
{
    private MAX_COUNT = 100;

    private void Form1_Load(object sender, EventArgs e)
    {
        ProgressBar1.Hide();
        Label1.Hide();
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        try
	{
            Button1.Enabled = false;
            ProgressBar1.Maximum = MAX_COUNT;
            ProgressBar1.Value = 0;
            ProgressBar1.Step = 1;
            Label1.Text = string.Empty;

            // 処理実施
            ProgressBar1.Show();
            Label1.Show();
            for (var i = 1; i <= MAX_COUNT; i++)
	    {
                var wk = i;
                await Task.Run(() => WaitProcess(wk));

                Threading.Thread.Sleep(500);
                ProgressBar1.PerformStep();
                Label1.Text = $"({ProgressBar1.Value}/{MAX_COUNT})";
            }
	}
        finally
	{
            ProgressBar1.Hide();
            Label1.Hide();
            Button1.Enabled = true;

        }
    }

    private void WaitProcess(int i)
    {
        // 重い処理
        Threading.Thread.Sleep(500);

        Debug.WriteLine(i);
    }
}
VB.Netサンプルソース
Form1.vb
Imports System.ComponentModel
Public Class Form1
    Private MAX_COUNT = 100

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
        ProgressBar1.Hide()
        Label1.Hide()
    End Sub

    Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Try
            Button1.Enabled = False
            ProgressBar1.Maximum = MAX_COUNT
            ProgressBar1.Value = 0
            ProgressBar1.Step = 1
            Label1.Text = String.Empty

            '*** 処理実施
            ProgressBar1.Show()
            Label1.Show()
            For i = 1 To MAX_COUNT
                Dim wk = i
                Await Task.Run(Sub()
                                   WaitProcess(wk)
                               End Sub)

                Threading.Thread.Sleep(500)
                ProgressBar1.PerformStep()
                Label1.Text = String.Format("({0}/{1})", ProgressBar1.Value, MAX_COUNT)
            Next

        Finally
            ProgressBar1.Hide()
            Label1.Hide()
            Button1.Enabled = True

        End Try
    End Sub

    Private Sub WaitProcess(i As Integer)
        '*** 重い処理
        Threading.Thread.Sleep(500)

        Debug.WriteLine(i)
    End Sub
End Class

Taskのサンプルとの違いはButton1_Clickイベントの前に『Async』、Task.Runの前に『Await』が追加されているのみです。
コチラを実施するとTaskのサンプルと異なり、プログレスの動きとイミディエイトへの出力が同期されており、処理が完了してからButton1が有効にり、同期処理のように動作していることが分かります。

マルチスレッド処理の注意点

BackgroundWorker、Taskの登場により比較的身近になったマルチスレッドプログラムですが、シングルスレッド処理では意識しなかった問題点も多々発生します。
代表的な注意点としてはデッドロック、他スレッドで作成した変数の書き換え、ワーカースレッドからのコントロールへのアクセス等です。
また、同時実行の制御をキチンとしないと再現性の難しい不具合を含む可能性がありますのでご注意ください。
マルチスレッドプログラミングに挑戦する際はスレッドセーフやlock等のキーワードをしっかり押さえていくようにしてください。

Discussion