.NETプログラミング研究 第26号 †
.NET Tips †
.NETのマルチスレッドプログラミング その8 †
.NETのマルチスレッドプログラミングについて、あれやこれやと書いてきましたが、主なところは一通り紹介しきったのではないかと思いますので(それ以上に書く方も読む方も飽きたでしょう)、今回でひとまず終わりにしたいと考えています。今回は今まで紹介していなかった小ネタをできるだけ紹介するつもりですが、紹介し切れなかった事柄については、後々DOBON.NETにアップする予定です。
ReaderWriterLockの使い方 †
ReaderWriterLockは、複数スレッドからの共有リソースへのアクセスを同期するために使用します。Monitorを使用しての同期と違い、ReaderWriterLockは読み込みアクセスに対して複数のスレッドに許可を与え、書き込みアクセスに対しては一つのスレッドにしか許可しません。よって、共有リソースへの書き込みが頻繁で、書き込みはめったになく、かつ短時間で終了するようなケースではReaderWriterLockは有効な手段となります。
スレッドが読み込みのためのロック(リーダーロック)を取得する時は、ReaderWriterLock.AcquireReaderLockメソッドを、書き込みのためのロック(ライタロック)を取得する時は、ReaderWriterLock.AcquireWriterLockメソッドを呼び出します。
以下にReaderWriterLockを使用した簡単な例を示します。このコードのReaderWriterLockを使っている箇所を削除すると同期が取れず、大変なことになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| |
Private Shared rwl As New ReaderWriterLock
Private Shared resource As String = "0123456789"
Public Shared Sub Main()
Dim i As Integer
For i = 0 To 199
If i Mod 2 = 0 Then
Dim t As New Thread( _
New ThreadStart(AddressOf ReadFromResource))
t.Start()
Else
Dim t As New Thread( _
New ThreadStart(AddressOf WriteToResource))
t.Start()
End If
Next i
Console.ReadLine()
End Sub
Private Shared Sub ReadFromResource()
rwl.AcquireReaderLock(Timeout.Infinite)
Console.WriteLine(resource)
rwl.ReleaseReaderLock()
End Sub
Private Shared Sub WriteToResource()
rwl.AcquireWriterLock(Timeout.Infinite)
Dim s As String = resource.Substring(0, 1)
resource = resource.Substring(1)
Thread.Sleep(1)
resource += s
rwl.ReleaseWriterLock()
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| |
private static ReaderWriterLock rwl = new ReaderWriterLock();
private static string resource = "0123456789";
public static void Main()
{
for (int i = 0; i < 200; i++)
{
if (i % 2 == 0)
{
(new Thread(new ThreadStart(ReadFromResource))).Start();
}
else
{
(new Thread(new ThreadStart(WriteToResource))).Start();
}
}
Console.ReadLine();
}
private static void ReadFromResource()
{
rwl.AcquireReaderLock(Timeout.Infinite);
Console.WriteLine(resource);
rwl.ReleaseReaderLock();
}
private static void WriteToResource()
{
rwl.AcquireWriterLock(Timeout.Infinite);
string s = resource.Substring(0, 1);
resource = resource.Substring(1);
Thread.Sleep(1);
resource += s;
rwl.ReleaseWriterLock();
}
|
リーダーロックを取得中に共有リソースへ書き込む必要が生じたときは、ReaderWriterLock.UpgradeToWriterLockメソッドを使ってライタロックへアップグレードすることができます。(ReaderWriterLock.UpgradeToWriterLockメソッドを呼び出したら、ReaderWriterLock.DowngradeFromWriterLockメソッドによりリーダーロックを復元します。)
ReaderWriterLock.ReleaseLockメソッドでもロックを解放できますが、この時はスレッドがロックを取得した回数に関係なく、すぐにロックを解放します。ReleaseLockメソッドが返すLockCookieオブジェクトをRestoreLockメソッドに使うことにより、解放したロックを復元することもできます。このことを利用すれば、ロックを一時的に解放することができます。
さらに、WriterSeqNumプロパティで現在のライタシーケンス番号を覚えておき、それをAnyWritersSinceメソッドに使うことにより、その間にライタロックを取得したスレッドがあるか調べることができます。もしライタロックを取得したスレッドがあれば、共有リソースの内容が変更されたと判断できます。これを利用すれば、例えば、リーダーロック解放前に共有リソースの内容をローカル変数などに保存しておき、その後再びリーダーロックを取得してから共有リソースの内容が変更されていれば共有リソースを読み込み、変更されていなければ保存してある内容を使用するということができます。
次にUpgradeToWriterLock、DowngradeFromWriterLockメソッド、ReleaseLockメソッド、RestoreLockメソッド、WriterSeqNumプロパティ、AnyWritersSinceメソッドの具体的な使用例を示します。UpAndDownGradeメソッドでUpgradeToWriterLock、DowngradeFromWriterLockメソッドを、ReleaseAndRestoreメソッドでReleaseLockメソッド、RestoreLockメソッド、WriterSeqNumプロパティ、AnyWritersSinceメソッドを使っています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
| |
Private Shared rwl As New ReaderWriterLock
Private Shared resource As String = "0123456789"
Public Shared Sub Main()
Dim i As Integer
For i = 0 To 199
If i Mod 2 = 0 Then
Dim t As New Thread( _
New ThreadStart(AddressOf UpAndDownGrade))
t.Start()
Else
Dim t As New Thread( _
New ThreadStart(AddressOf ReleaseAndRestore))
t.Start()
End If
Next i
Console.ReadLine()
End Sub
Private Shared Sub UpAndDownGrade()
rwl.AcquireReaderLock(Timeout.Infinite)
Console.WriteLine(resource)
Dim lc As LockCookie = rwl.UpgradeToWriterLock(Timeout.Infinite)
Dim s As String = resource.Substring(0, 1)
resource = resource.Substring(1)
Thread.Sleep(1)
resource += s
rwl.DowngradeFromWriterLock(lc)
rwl.ReleaseReaderLock()
End Sub
Private Shared Sub ReleaseAndRestore()
rwl.AcquireReaderLock(Timeout.Infinite)
Dim resourceValue As String = resource
Dim seqNum As Integer = rwl.WriterSeqNum
Dim lc As LockCookie = rwl.ReleaseLock()
Thread.Sleep(10)
rwl.RestoreLock(lc)
If rwl.AnyWritersSince(seqNum) Then
Console.WriteLine("({0})", resourceValue)
resourceValue = resource
End If
Console.WriteLine(resourceValue)
rwl.ReleaseReaderLock()
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
| |
private static ReaderWriterLock rwl = new ReaderWriterLock();
private static string resource = "0123456789";
public static void Main()
{
for (int i = 0; i < 200; i++)
{
if (i % 2 == 0)
{
(new Thread(new ThreadStart(UpAndDownGrade))).Start();
}
else
{
(new Thread(new ThreadStart(ReleaseAndRestore))).Start();
}
}
Console.ReadLine();
}
private static void UpAndDownGrade()
{
rwl.AcquireReaderLock(Timeout.Infinite);
Console.WriteLine(resource);
LockCookie lc = rwl.UpgradeToWriterLock(Timeout.Infinite);
string s = resource.Substring(0, 1);
resource = resource.Substring(1);
Thread.Sleep(1);
resource += s;
rwl.DowngradeFromWriterLock(ref lc);
rwl.ReleaseReaderLock();
}
private static void ReleaseAndRestore()
{
rwl.AcquireReaderLock(Timeout.Infinite);
string resourceValue = resource;
int seqNum = rwl.WriterSeqNum;
LockCookie lc = rwl.ReleaseLock();
Thread.Sleep(10);
rwl.RestoreLock(ref lc);
if (rwl.AnyWritersSince(seqNum))
{
Console.WriteLine("({0})", resourceValue);
resourceValue = resource;
}
Console.WriteLine(resourceValue);
rwl.ReleaseReaderLock();
}
|
スレッドタイマの使い方 †
.NET Frameworkで用意されているタイマには、System.Windows.Forms.Timer(Windowsタイマ)、System.Threading.Timer(スレッドタイマ)、System.Timers.Timer(サーバータイマ)の3種類があります。Windowsフォームアプリケーションの開発ではWindowsタイマがよく使われますが、これはOSのタイマ機能を使用しているため、スレッドでメッセージを処理しなければタイマは発生せず、長い処理によりブロックされる恐れがあります。これに対してサーバータイマとスレッドタイマはワーカースレッドにより処理されますので、その心配がありません。
サーバータイマについてはヘルプによると、「サーバー ベースのタイマは、サーバー環境での実行用に最適化された従来のタイマを強化したものです。」とのことです。Visual Studio .NETのツールボックスの「コンポーネント」タブにあるのはこれです。
スレッドタイマは、システムが提供するスレッドプールを使用したタイマです。イベントの代わりにコールバックメソッドを使用します。軽量で、簡単なタイマ処理が必要な時に便利です。(これらのタイマの違いに関する詳細は、ヘルプの「サーバー ベースのタイマの概説」等をご覧ください。)
スレッドタイマThreading.Timerを作成するには、コンストラクタで実行するメソッドへのTimerCallbackデリゲート、メソッドで使用される状態オブジェクト、最初に発生する時間、発生する時間間隔を渡します。最初に発生する時間、発生する時間間隔を変更するには、Changeメソッドを使います。タイマをキャンセルするには、Timer.Dispose関数を呼び出します。なお、Dispose関数を呼び出せるように、Threading.Timerオブジェクトへの参照は保持しておくべきです。
下のコードでは、TimerState.Tickメソッドをスレッドタイマにより、定期的に実行します。まず0.1秒後に1秒間隔で実行し、TimerState.Tickメソッドが5回呼び出されると、0.5秒間隔で実行するように変更します。そして10回呼び出されたところで、タイマを破棄します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| |
Class TimerState
Private Count As Integer = 0
Public ThreadTimer As System.Threading.Timer
Public Sub Tick(ByVal state As Object)
Dim ts As TimerState = CType(state, TimerState)
Count += 1
Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})", _
System.Environment.TickCount, Count)
If Count = 5 Then
ts.ThreadTimer.Change(500, 500)
Console.WriteLine("Changed")
Else
If Count = 10 Then
ts.ThreadTimer.Dispose()
ts.ThreadTimer = Nothing
Console.WriteLine("Disposed")
End If
End If
End Sub
End Class
Class MainClass
Public Shared Sub Main()
Dim ts As New TimerState
ts.ThreadTimer = New System.Threading.Timer( _
New TimerCallback(AddressOf ts.Tick), ts, 100, 1000)
Console.ReadLine()
End Sub
End Class
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| |
class TimerState
{
private int Count = 0;
public System.Threading.Timer ThreadTimer;
public void Tick(object state)
{
TimerState ts = (TimerState) state;
Count++;
Console.WriteLine("システム起動後の経過時間:{0}ミリ秒({1})",
System.Environment.TickCount, Count);
if (Count == 5)
{
ts.ThreadTimer.Change(500, 500);
Console.WriteLine("Changed");
}
else if (Count == 10)
{
ts.ThreadTimer.Dispose();
ts.ThreadTimer = null;
Console.WriteLine("Disposed");
}
}
}
class MainClass
{
public static void Main()
{
TimerState ts = new TimerState();
ts.ThreadTimer =
new System.Threading.Timer(
new TimerCallback(ts.Tick), ts, 100, 1000);
Console.ReadLine();
}
}
|
上記コードの結果は例えば次のようになります。
システム起動後の経過時間:8315236ミリ秒(1)
システム起動後の経過時間:8316218ミリ秒(2)
システム起動後の経過時間:8317219ミリ秒(3)
システム起動後の経過時間:8318220ミリ秒(4)
システム起動後の経過時間:8319222ミリ秒(5)
Changed
システム起動後の経過時間:8319723ミリ秒(6)
システム起動後の経過時間:8320223ミリ秒(7)
システム起動後の経過時間:8320724ミリ秒(8)
システム起動後の経過時間:8321225ミリ秒(9)
システム起動後の経過時間:8321726ミリ秒(10)
Disposed
参考:
スレッドセーフなコレクションを使う †
System.Collections名前空間にあるコレクションクラス(ArrayList、Queueなど)のインスタンスメンバは、スレッドセーフである保障がありません。複数のスレッドがコレクションの内容を読み取る限りにおいては問題ありませんが、コレクションの内容が変更されうる場合は、コレクションにアクセスするすべてのスレッドでその結果は保障されません。(ただしHashtableクラスは単一の書き込み操作に関してはスレッドセーフが保障されています。)
コレクションをスレッドセーフにするためには、コレクションのSynchronizedメソッドによりスレッドセーフラッパーを作成し、そのラッパーを通じてコレクションにアクセスするようにします。コレクションがスレッドセーフであるか確かめるには、IsSynchronizedプロパティがtrueかどうか調べます。
次にSynchronizedメソッドを使用した例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| |
Dim al As New ArrayList
Dim syncdAl As ArrayList = ArrayList.Synchronized(al)
Dim syncdAl2 As ArrayList = _
ArrayList.Synchronized(New ArrayList)
Console.WriteLine("al.IsSynchronized = {0}", _
al.IsSynchronized)
Console.WriteLine("syncdAl.IsSynchronized = {0}", _
syncdAl.IsSynchronized)
Console.WriteLine("syncdAl2.IsSynchronized = {0}", _
syncdAl2.IsSynchronized)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| |
ArrayList al = new ArrayList();
ArrayList syncdAl = ArrayList.Synchronized(al);
ArrayList syncdAl2 = ArrayList.Synchronized(new ArrayList());
Console.WriteLine("al.IsSynchronized = {0}",
al.IsSynchronized);
Console.WriteLine("syncdAl.IsSynchronized = {0}",
syncdAl.IsSynchronized);
Console.WriteLine("syncdAl2.IsSynchronized = {0}",
syncdAl2.IsSynchronized);
|
出力結果;
al.IsSynchronized = False
syncdAl.IsSynchronized = True
syncdAl2.IsSynchronized = True
コレクションの列挙はスレッドセーフな処理ではありませんので、コレクションが同期されていても別のスレッドがコレクションの内容を変更することがあり得るため、その結果例外をスローするかもしれません。列挙処理をスレッドセーフに行うには、列挙処理中コレクションをlock(VB.NETではSyncLock)などによりロックします。
lockでコレクションをロックする時は、そのコレクション自身ではなく、コレクションのSyncRootプロパティを使用したほうがよいでしょう。(スレッドセーフな独自のラッパーを作成する時にも、派生クラスでSyncRootプロパティが使用されます。)
次にコレクションの列挙処理をスレッドセーフに行う例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| |
Private Shared syncdAl As ArrayList = _
ArrayList.Synchronized(New ArrayList)
Public Shared Sub Main()
Dim i As Integer
For i = 0 To 99
syncdAl.Add(i)
Next i
Dim t As New Thread(New ThreadStart(AddressOf MyThread))
t.Start()
Thread.Sleep(500)
syncdAl.RemoveAt(0)
Console.ReadLine()
End Sub
Private Shared Sub MyThread()
SyncLock syncdAl.SyncRoot
Dim i As Integer
For Each i In syncdAl
Console.Write(i)
Thread.Sleep(10)
Next i
End SyncLock
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| |
private static ArrayList syncdAl =
ArrayList.Synchronized(new ArrayList());
public static void Main()
{
for (int i = 0; i < 100; i++)
{
syncdAl.Add(i);
}
Thread t = new Thread(new ThreadStart(MyThread));
t.Start();
Thread.Sleep(500);
syncdAl.RemoveAt(0);
Console.ReadLine();
}
private static void MyThread()
{
lock(syncdAl.SyncRoot)
{
foreach (int i in syncdAl)
{
Console.Write(i);
Thread.Sleep(10);
}
}
}
|
ArrayクラスのようにSynchronizedメソッドがないコレクションでは、上記と同じように、lockを使用することによりスレッドセーフにすることができます。
参考:
Interlockedの使い方 †
以前「スレッドの同期」にて、「競合状態」の説明をしました。競合状態は「x++」のような単純な変数のインクリメントでも発生します。一見一回の操作のみで行われるように見える変数のインクリメントも実は、「変数の値を取得する → 1つ足す → 結果を変数に格納する」という幾つかの操作により行われており、あるスレッドが変数に1足して結果を格納する前に、別のスレッドが1足してしまうということが起こりうるのです。
lockステートメントによる同期により競合状態を防いでインクリメントを行うには、次のようにします。
1
2
3
| | SyncLock Me
x += 1
End SyncLock
|
1
2
3
4
| | lock (this)
{
x++;
}
|
実は.NET Frameworkではより優れた方法として、Interlocked.Incrementメソッドが用意されています。Interlocked.Incrementメソッドは、先に説明した変数インクリメントの一連の操作を、分割不可能な操作として一度に行ってくれるため、競合状態の発生する隙を与えません。(このように操作が分割不可能であることを「アトミック」と呼びます。)
上記のコードをInterlocked.Incrementメソッドで書き直すと、次のようになります。
1
| | System.Threading.Interlocked.Increment(x)
|
1
| | System.Threading.Interlocked.Increment(x);
|
Interlockedクラスの4つの静的メソッドIncrement、Decrement、Exchange、CompareExchangeメソッドを使うことにより、整数のインクリメント、デクリメント、さらに変数への値の設定(変数の値の交換)、変数の比較と値の設定を分割不可能な操作として実行できます。
参考:
コメント †