ひきぷろのプログラミング日記

プログラミングの日記です。

(C#) マルチスレッド対応の int 型

マルチスレッドで値を操作しても内容が壊れない int 型を作ってみました。

class MtInt {
	private int _value;
	private object _valueLock;

	public MtInt() {
		_value = 0;
		_valueLock = new object();
	}

	public MtInt(int i) {
		_value = i;
		_valueLock = new object();
	}

	public static implicit operator int(MtInt mtInt) {
		return mtInt._value;
	}

	public static MtInt operator +(MtInt mtInt, int i) {
		mtInt._Add(i);
		return mtInt;
	}

	public static MtInt operator -(MtInt mtInt, int i) {
		mtInt._Add(-i);
		return mtInt;
	}

	public static MtInt operator *(MtInt mtInt, int i) {
		mtInt._Mul(i);
		return mtInt;
	}

	public static MtInt operator /(MtInt mtInt, int i) {
		mtInt._Div(i);
		return mtInt;
	}

	public static MtInt operator ++(MtInt mtInt) {
		mtInt._Add(1);
		return mtInt;
	}

	public static MtInt operator --(MtInt mtInt) {
		mtInt._Add(-1);
		return mtInt;
	}

	private void _Add(int value) {
		lock (_valueLock) {
			_value += value;
		}
	}

	private void _Mul(int value) {
		lock (_valueLock) {
			_value *= value;
		}
	}

	private void _Div(int value) {
		lock (_valueLock) {
			_value /= value;
		}
	}
}

マルチスレッドプログラムの経験が乏しいので、もしかしたら根本的に何か間違っている可能性もあります。今のところ、問題なく使えそうだと思っています。
演算子オーバーロードを全て定義しているわけではないので、使いやすくするためには追加する必要が出てきそうです。
また、ロックの範囲が狭すぎるので、用途によっては使いにくいかもしれないです。

implicit operator int がミソで、これを定義しておくと暗黙的に int 型に変換されます。

public int GetValue() {
	return new MtInt();
}

というようなイメージで、戻り値の型が int で定義されているメソッドでも、そのまま MtInt 型の値を返すことができます (MtInt::_value が勝手に返されます)。

次のようなコードで実行確認しました。

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Threading;
using System.Diagnostics;

namespace WindowsFormsApplication1 {
	public partial class Form1 : Form {
		private MtInt _mtInt = new MtInt();

		public Form1() {
			InitializeComponent();
		}

		// ボタンが押された時
		private void button1_Click(object sender, EventArgs e) {
			long time = Benchmark(() => {
				// スレッド 10 個を作成して、すぐに実行する
				List<Thread> threads = new List<Thread>();
				for (int i = 0; i < 10; i++) {
					Thread thread = new Thread(new ThreadStart(() => {
						for (int n = 0; n < 10000; n++) {
							_mtInt++;
						}
					}));
					thread.Start();
					threads.Add(thread);
				}

				// スレッド 10 個の終了を待つ
				foreach (Thread thread in threads) {
					thread.Join();
				}

				// _mtInt 変数の内容を表示する
				// ↓ ここは自動的に変換されないのか...
				Console.WriteLine("value: {0}", (int)_mtInt);
			}, 1);

			// 実行時間を表示する
			Console.WriteLine("time: {0}", time);
		}

		// 引数で渡したメソッドの実行時間を計る
		private static long Benchmark(Action action, int interval) {
			GC.Collect();
			Stopwatch sw = Stopwatch.StartNew();
			for (int i = 0; i < interval; i++) {
				action.Invoke();
			}
			sw.Stop();
			return sw.ElapsedMilliseconds;
		}
	}
}

上記のまま実行すると出力コンソールに

value: 100000

というように表示されますが、 _mtInt 変数の型を int に変えると

value: 94055
(毎回、表示される値は異なります)

というような値が表示されます。

追記

足し算・インクリメント・デクリメントのような簡単な操作をする場合は、 lock を使わずに System.Threading.Interlocked クラスの機能を使うと軽くなるようです。
MtInt クラスの _Add() メソッドを、 Interlocked クラスを使って修正すると次のようになります。

private void _Add(int value) {
	Interlocked.Add(ref _value, value);
	//lock (_valueLock) {
	//	_value += value;
	//}
}