[C#]ざっくりマルチスレッド(非同期処理)

C#のマルチスレッド(非同期処理)に関して、いつものように「ざっくり」説明。

C#のマルチスレッドは、何種類か書き方があるが、主に4つのパターンがある。

  1. Threadでデリゲートを動かす。
    基本。でも、.NetFramework4以降では使わないと思う。
  2. ThreadPoolで1をもう少し効率よくする。
    1の方法だと、スレッド数が多くなったりすると、場合によっては逆に遅くなる。
    なのでThreadPoolを使うことで効率アップできるが、書き方がめんどいし、なんだか分からなくなる。
  3. Taskクラスに1とか2の中身をお任せ (.Net Framework4)
    1の感覚で裏ではThreadPoolを使ってくれる。
    .NetFramework4以降はこれだけ覚えてればよい。
  4. async await で、シンプル非同期処理 (.Net Framework4.5)
    シンプルな記述で非同期処理を書ける。GUIアプリケーションでInvokeでスレッドセーフな書き方が簡潔になる。

んで、とりあえずちゃんと覚えるべきは、3の方法かな~っと。

3が理解できたら、使えるポイントで4のasync, awaitを書くと、便利だなーって思うときがある。

1.は覚える必要は無いけど、歴史として軽く知っておくとよい。(.NETFramework4未満な場合はがんばるしかない。)

2.は仕組みは知る必要があるけど、書き方はもう覚えなくていいじゃないかな・・・。

なので、めんどくさい方やお時間無い方は、Taskとスレッドセーフの辺りだけかいつまめばよいかと!

Thread

で、まずは歴史として軽く覚えておくべきThreadについて、軽く。
以下の例はコンソールアプリケーションで、Thread使った場合。

public void Run()
{
	Thread thread = new Thread(
		() => {
			for(int i = 0; i < 5; i++)
			{
				Console.WriteLine("別スレッドだよ");
				Thread.Sleep(1000);
			}
		});
	thread.Start();

	for (int i = 0; i < 5; i++)
	{
		Console.WriteLine("本体スレッドだよ");
		Thread.Sleep(1000);
	}
	Console.ReadKey();
}

んで、結果。
mt1

呼び出した側の本体のスレッドと、Threadクラスで作った別スレッドの2本が同時に動きます。

Threadの引数はデリゲートなので、実行させたい関数を持たせるか、匿名メソッドでその場でメソッドを書く。
今回はラムダ式でその場で処理書いています。

つまり、マルチスレッドは、デリゲートに覚えさせた関数を別スレッドで実行させる仕組み。
だから、デリゲートはざっくりとでも理屈を覚えとかないとね。
デリゲートの記事は前に書いているので目を通した方がよいかも。
デリゲートとはなんぞや

これが基本のマルチスレッド(非同期処理)になる。

ThreadPool(スレッドプール)

スレッドの作成は割と重い処理です。

環境によると思うんですが、スレッドを作成するのに1秒掛かる場合もあります。

つまり1秒かからない処理をマルチスレッドで処理すると下手すると遅くなるという残念な事になります。

そこで、使われるのがスレッドプール(Thread Pool)

スレッドプールはスレッドをいくつか事前に立ち上げといて、使いまわすことで、スレッド作成の時間を短縮させることができる。

今回はコードは割愛。

Task

そして、.NetFramework4でTaskクラスができ、ThreadやThreadPoolを中で上手くゴニョゴニョしてくれるようになった。

さっきの、ThreadをTaskで書きなおすと以下の様になる。

public void Run()
{
	Task task = new Task(() =>
	{
		for (int i = 0; i < 5; i++)
		{
			Console.WriteLine("別スレッドだよ");
			Thread.Sleep(1000);
		}
	});
	task.Start();

	for (int i = 0; i < 5; i++)
	{
		Console.WriteLine("本体スレッドだよ");
		Thread.Sleep(1000);
	}
	Console.ReadKey();
}

まぁ、ほとんど変わらないよね。

今後はThreadでなくTaskで書くべき。

ちなみに、Taskを走らせるには、Taskを作ってStartを呼ぶ方法と、もういきなりStartさせる方法がある。上記は前者。

特に理由がないなら、いきなりStartさせる方法がよさげ。

その場合、以下の2つがある。下の方がより細かな設定ができるが、とりあえずRun使っとけばよい。

Task task1 = Task.Run(() => { Console.Write("Run"); });
Task task2 = Task.Factory.StartNew(() => { Console.Write("StartNew"); });

 

ここまでは簡単ですね。

まぁ、引数もないし、投げっぱなしでOKなので

ThreadSafe(スレッドセーフ)

で、マルチスレッドには、スレッドセーフという概念が存在する。

スレッドセーフとは、複数のスレッドが同時に実行しても問題が発生しないこと。みたいな感じ。

どういうことかというと、

int count = 0;
Task task = new Task(() =>
{
	//count に +10 を5回で計50を追加する
	for (int i = 0; i < 5; i++)
	{
		int tmpCount = count;
		Console.WriteLine("別スレッドだよ");
		count = tmpCount + 10;
	}
});
task.Start();

//countから-5の5回で計-25を引く
for (int i = 0; i < 5; i++)
{
	int tmpCount = count;
	Console.WriteLine("本体スレッドだよ");
	count = tmpCount - 5;
}

task.Wait();	//別スレッドの処理を待つ

//合計は50-25で25?
Console.WriteLine(count);

Console.ReadKey();

これを実行すると25が返ってきそうだが、50だったり40だったり25だったりと、実行するごとに値が違う場合がある。(上記コードで再現しない場合もあり。)

2つのスレッドが同時に同じ値(count)を更新するので、値を更新している間に別スレッドで更新されてしまい値があやふやになる。
(tmpCountに無理やり入れたりしてるのは、あやふやになりやすいようにわざと意味のない代入をしています。)

こいうった状況を、スレッドセーフでない。という。

んで、どうするかというと、一番一般的な方法が、ロック(lock)という方法で更新の際に、countの値を固定する方法を使います。

Object lockObj = new Object();
int count = 0;

Task task = new Task(() =>
{
	//count に +10 を5回で計50を追加する
	for (int i = 0; i < 5; i++)
	{
		Thread.Sleep(i);
		lock (lockObj)
		{
			int tmpCount = count;
			Console.WriteLine("別スレッドだよ");
			count = tmpCount + 10;
		}
	}
});
task.Start();

//countから-5の5回で計-25を引く
for (int i = 0; i < 5; i++)
{
	Thread.Sleep(5 - i);
	lock (lockObj)
	{
		int tmpCount = count;
		Console.WriteLine("本体スレッドだよ");
		count = tmpCount - 5;
	}
}

task.Wait();

//合計は50-25で25?
Console.WriteLine(count);

lockObjという、何でもないオブジェクトのインスタントを作成。

lockでlockObjを持っている間は、同じlockObjeを持っている他のlockは待機となる。

複数のメソッドをまたいで使用される場合は、privateの変数などにlockObjを持たせるとよいかも。
このようにマルチスレッドは、その記述がスレッドセーフであるかを意識する必要がある。

この部分に気を付けていないと、「ごく稀に、値がおかしい」などのバグが発生する。
(ゲームのように複数の敵やキャラをマルチスレッドで動かし、お互いが干渉しあうような場合などは、頻繁に起こりそうですね。)

async と await

さて、最後にasyncとawaitを軽くご説明。

asyncとawaitは、非同期処理を意識せずに、自然な形で書けるようになる。

まず、適当にWindows Form を作成。

ボタンとラベルを配置。

ボタンをクリックしたら重い処理(ここでは5秒待つ)を行った後ラベルを更新する。

using System.Windows.Forms;

namespace asynawaitTest
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();
		}

		private void button1_Click(object sender, EventArgs e)
		{
			var start = DateTime.Now;

			//何らかの重い処理
			System.Threading.Thread.Sleep(5000);

			var ts = DateTime.Now - start;

			this.label1.Text = "OK:" + ts.TotalSeconds + "Sec";
		}
	}
}

だが、この作りには問題がある。

ボタンを押して、処理を行っている間はユーザーインターフェース側が何の処理も受け付けれずに、固まってしまう(どんな操作も受け付けられなくなる。)。

で、一般的にはどうするかというと、以下の様に別のスレッドを立ち上げることで、メインスレッドは固まらずにすむ。

private void button1_Click(object sender, EventArgs e)
{
	Task.Factory.StartNew(() =>
	{
		var start = DateTime.Now;

		//何らかの重い処理
		System.Threading.Thread.Sleep(5000);

		var ts = DateTime.Now - start;

		this.label1.Text = "OK:" + ts.TotalSeconds + "Sec";

	});
}

しかし、これでやるとlabelが更新されない。(もしくは例外で落ちる。)

これは、GUIアプリケーションの特徴で、先に説明したスレッドセーフが理由で、UIスレッド呼ばれるメインスレッド以外から、UIを変更できないという制約があるためである。

んで、どうするかっていうと、UIを変更する場合はUIスレッドにお願いする。それがInvokeと呼ばれる機能。(Invoke: 救いを求めて呼びかける。祈願する。)

private void button1_Click(object sender, EventArgs e)
{
	Task.Factory.StartNew(() =>
	{
		var start = DateTime.Now;

		//何らかの重い処理
		System.Threading.Thread.Sleep(5000);

		var ts = DateTime.Now - start;

		SafeSetLable(ts.TotalSeconds.ToString());

	});
}

private void SafeSetLable(string text)
{
	if (this.label1.InvokeRequired)
	{
		this.Invoke((MethodInvoker)delegate () { SafeSetLable(text); });
		return;
	}
	else
	{
		this.label1.Text = "OK:" + text + "Sec";
	}
}

もし、 別スレッドであれば(if(this.lable1.InvokeRequired))、メインスレッドでメソッドを実行するようにお願い(this.Invoke())する。

といった形で、非常にめんどい書き方だった。

それが、以下の様に書ける。

async private void button1_Click(object sender, EventArgs e)
{
	string text = await Task.Run(()=>
	{
		var start = DateTime.Now;

		//何らかの重い処理
		System.Threading.Thread.Sleep(5000);

		var ts = DateTime.Now - start;

		return ts.TotalSeconds.ToString();
	});

	this.label1.Text = "OK:" + text + "Sec";
}

async と頭についたメソッドの中では、await 以下の非同期処理が終わるまで待機して、終わったらメインスレッドに戻ってきて処理を続ける。

Invokeとか書かなくてもよくなっている。  いやー、便利になったよね。

Windows Form におけるasync await は、以下のサイト様の方が詳しく乗ってます。
http://kimux.net/?p=902

おまけ

実際にマルチスレッド作っててハマったので、メモ。

Taskは内部でスレッドプールとかうまく使ってくれるから、スレッドの起動にかかる時間を無くせると考えていたのですが、それは間違いで、結局スレッドの立ち上がる時間はやっぱり1秒ぐらいかかりました。

なぜかというと、当たり前ですがスレッドプールは、余っているスレッドに仕事をやらせるので、余って無ければ作るしかないからです。

んで、最初にプログラムが用意するスレッドは、デフォルトだとCPUのコア数と同じになるようで、4コアのマシンだと、事前に4つスレッドは用意されているけど、5つ目は、前の4つが終わって無ければ、スレッドを新規に作成し、遅延が発生するという訳です。

そこで使うのが事前にいくつのスレッドを立ち上げておくかを指定するSetMinThreadメソッド。

どのくらいスレッドが必要かを考えて事前に立ち上げておくと、作成の時間は無くなるということです。

このあたりは、SetMinThreadで検索すると色々と分かりやすい記事があるので、そちらを参考にしていただければと。

 

 

4 comments on “[C#]ざっくりマルチスレッド(非同期処理)

  1. すぎやん 2017年8月15日 1:30 PM

    ありがとうございます!悩みが解決しました!

    • gomokuro 2017年9月5日 10:04 AM

      お役に立てて何よりです(^^)

  2. 通りすがり 2017年12月1日 9:27 AM

    asyncの解説の前段でInvokeについて解説がありますが、InvokeRequiredは「this.label1」、
    Invokeは「this(Form)」の物をコールしています。
    この使い分けは何か意味があるのでしょうか?

    • gomokuro 2018年1月9日 4:12 PM

      ちょっとうる覚えなのですが、多分意味は特に無かった気がします。
      なんとなく、呼び出し元は各プロパティから、委託は親へthis.Invokeという書き方で覚えてたからだと思います。

Leave a Reply to すぎやん Cancel Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>