マルチスレッドで並列処理して快適に

2023年2月7日

こんにちは、ノムノムです。

今回はマルチスレッドをやってみたいと思います。

マルチスレッド

マルチスレッドとは、処理を並行して進めることで複数の処理を同時に行うことができることを指します。

マルチスレッドを使うとメインの画面処理を行っている裏で別の処理を動かすことができます。

つまり、画面で実行ボタンを押したら処理が終わるまで固まって止まってしまうことを回避したりできます。

マルチスレッドの使い方

ここではマルチスレッドについて説明させていただきます。

Task.Runを使ってマルチスレッドを行う

単純にマルチスレッドを行うにはTask.Run関数を使用します。

Task.Run(() =>
{
    //処理・・・
});

とても簡単にできます。これはラムダ式を使用しています。

ラムダ式

ラムダ式とは、匿名メソッドのことを指します。
その場所でしか使用されない処理などを関数名の命名なしに利用することができます。

プログレスバーダイアログを作ってみよう

メインスレッドなどで重たい処理を実行すると処理が終わるまで画面が固まってしまいます。
俗にいうハングアップの状態です。

プログレスバーを作成してもハングアップ状態だとキャンセルボタンが押せません。

そこで重たい処理を別のスレッドで行い、メインスレッドはプログレスバーの制御を行うことでキャンセルボタンを押せるようにできます。

サンプルソース

プログレスバー

まずはプログレスバーのコントロールを設定します。

プログレスバーとボタン(キャンセル用)を置いただけです。

フォームのプロパティを以下のように設定します。

プロパティ名設定値
ControlBoxFalse
MaximizeBoxFalse
MinimizeBoxFalse
StartPositionCenterScreen
TopMostTrue

それでは中身をソースは以下のようにしました。

public partial class dlgProgress : Form
{
    //キャンセルプロパティ
    public bool Cancel { get; set; }

    public dlgProgress()
    {
        InitializeComponent();
    }

    private void dlgProgress_Load(object sender, EventArgs e)
    {
        //初期設定
        Cancel = false;
        this.progressBar1.Value = 0;
    }

    private void btnCancel_Click(object sender, EventArgs e)
    {
        //キャンセルボタンを押した時プロパティの値を変える
        Cancel = true;
    }

    public void SetProgressValue(int prg)
    {
        //プログレスバーの値をセットする
        if (prg > 100) prg = 100;
        if (prg < 0) prg = 0;

        this.progressBar1.Value = prg;
    }

}

特筆すべき点は特にないと思われます。

呼出しフォームの内容

dlgProgress dlgPrg = null;
int _prg = 0;
bool _complete = false;

private void button1_Click(object sender, EventArgs e)
{
    StartMultThreading();
}

/// <summary>
/// マルチスレッド起動
/// </summary>
private void StartMultThreading()
{
    //制御変数初期化
    _prg = 0;
    _complete = false;

    //プログレスバーを作成
    dlgPrg = new dlgProgress();
    this.Enabled = false;
    dlgPrg.Show();

    //別スレッド起動
    OtherThreadAsync();

    //タイマー起動
    this.timer1.Enabled = true;

}

/// <summary>
/// 別スレッドの処理
/// </summary>
public void OtherThreadAsync()
{

    Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            if (dlgPrg.Cancel == true)
            {
                break;
            }

            _prg = i;

            System.Threading.Thread.Sleep(200);
        }

        //処理完了
        _complete = true;
    });


}

private void timer1_Tick(object sender, EventArgs e)
{
    //プログレスバーの値を変更する
    dlgPrg.SetProgressValue(_prg);

    if (_complete == true)
    {
        dlgPrg.Close();
        this.Enabled = true;
        this.timer1.Enabled = false;
        if (dlgPrg.Cancel == false)
        {
            MessageBox.Show("処理が完了しました。");
        }
        else
        {
            MessageBox.Show("処理がキャンセルされました。");
        }
    }
}

順に説明していきます。

StartMultThreading関数でマルチスレッドを開始する前の準備をしています。
なぜタイマーを起動しているかというとマルチスレッドですので要は非同期で処理が実行されるということです。つまり、Task.Runの処理が終わるまでそこで止りません。

変数、関数などは同じメモリ空間を使用しているので利用できます。
なのでタイマーを使って刻一刻と変わる変化を変数を使って状況確認をしているのです。

OtherThreadAsync関数でTask.Runを使って非同期処理(マルチスレッド)を行います。
処理は適当です。1から100までカウントアップするだけの内容です。
キャンセルが押されていたら中断します。

タイマーイベントで、現在の進捗状況、キャンセルが押されたか、正常終了したかを判断して処理を完了します。

今回、プログレスバーダイアログをShowで呼び出しているため、呼び出し元の画面も触れてしまうのでthis.Enabled=Falseにして画面をロックしています。完了時にはちゃんとTrueに戻しましょう。

デッドロックに注意

例えばメインスレッドと別スレッドがあるとします。

上記の図のようにフラグAが有効だったらフラグBを有効にするメインスレッド、フラグBが有効だったらフラグAを有効にする別スレッドこうなった場合、永久に処理が終わりません。

メインスレッド、別スレッドともに処理がロックされてアプリが死んでしまっている状態つまりデッドロックです。こうなるともうお手上げです気をつけましょう。

まとめ

今回のプログレスバーは、なるべく複数の処理で同じプログレスバーを利用できかつそれなりに簡単に作れて勉強がてら覚えやすいように簡素にしました。

プログレスバーを利用すれば、処理の状況をユーザに教えることができるし、今アプリは処理を頑張っているんだよ。っていうのを表現できて便利です。

ぜひやってみてください。

これは僕ができるだけ理解しやすいようにかみ砕いたサンプルになっています。できれば改版してもっと使いやすい汎用性の高いものを作ってみてはいかがでしょうか?

ここまで読んで頂いてありがとうございます。