前述したように、各スレッドは独自のリソースを持ちますが、コード領域は共有されるため、各スレッドは同じ関数を実行できます。これによって発生する可能性がある問題は、複数のスレッドが関数を同時に実行し、データの混乱や予測不可能な結果を引き起こすため、この状況を回避する必要があります。
C# は、コードのセクションを相互排他セクション (クリティカル セクション) として定義できるキーワード ロックを提供します。相互排他セクションでは、一度に 1 つのスレッドのみが実行を開始でき、他のスレッドは待機する必要があります。 C# では、キーワード lock は次のように定義されます。
ロック(式)ステートメントブロック
式は追跡するオブジェクトを表し、通常はオブジェクト参照です。
Statement_block は、相互に排他的なセクションのコードです。このコードは一度に 1 つのスレッドでのみ実行できます。
以下に、lock キーワードの典型的な使用例を示します。lock キーワードの使用法と目的については、コメントで説明します。
例は次のとおりです。
コード
システムを使用する;
System.Threading を使用します。
名前空間 ThreadSimple
{
内部クラス アカウント
{
整数バランス;
ランダム r = 新しいランダム();
内部アカウント(int 初期値)
{
バランス = 初期;
}
内部 int 引き出し(int 金額)
{
if (残高 < 0)
{
//残高が0未満の場合は例外をスローする
throw new Exception("マイナス残高");
}
//次のコードは、現在のスレッドがバランスの値を変更する前に完了することが保証されています。
// 他のスレッドがこのコードを実行してバランスの値を変更することはありません
//したがって、残高の値は 0 未満にはなりません
ロック(これ)
{
Console.WriteLine("現在のスレッド:"+Thread.CurrentThread.Name);
//lockキーワードの保護がない場合は、if条件判定実行後である可能性があります。
//別のスレッドが、balance=balance-amount を実行し、balance の値を変更しました。
//この変更はこのスレッドには表示されないため、if 条件が成立しなくなる可能性があります。
//ただし、このスレッドは Balance=balance-amount を実行し続けるため、残高は 0 未満になる可能性があります
if (残高 >= 金額)
{
Thread.Sleep(5);
残高 = 残高 - 金額;
返金額。
}
それ以外
{
return 0 // トランザクションが拒否されました。
}
}
}
内部 void DoTransactions()
{
for (int i = 0; i < 100; i++)
撤退(r.Next(-50, 100));
}
}
内部クラスのテスト
{
静的な内部 Thread[] スレッド = 新しい Thread[10];
パブリック静的 void Main()
{
アカウント acc = 新しいアカウント (0);
for (int i = 0; i < 10; i++)
{
スレッド t = 新しい Thread(new ThreadStart(acc.DoTransactions));
スレッド[i] = t;
}
for (int i = 0; i < 10; i++)
スレッド[i].Name=i.ToString();
for (int i = 0; i < 10; i++)
スレッド[i].Start();
Console.ReadLine();
}
}
}Monitor クラスがオブジェクトをロックする
複数のスレッドがオブジェクトを共有する場合、一般的なコードと同様の問題が発生するため、ここでは System.Threading のクラス Monitor を使用する必要があります。 , Monitor は、スレッドがリソースを共有するためのソリューションを提供します。
Monitor クラスはオブジェクトをロックでき、スレッドはロックを取得した場合にのみオブジェクトを操作できます。オブジェクト ロック メカニズムにより、混乱を引き起こす可能性がある状況では、一度に 1 つのスレッドのみがこのオブジェクトにアクセスできるようになります。 Monitor は特定のオブジェクトに関連付ける必要がありますが、静的クラスであるため、オブジェクトの定義に使用できません。また、そのメソッドはすべて静的であり、オブジェクトを使用して参照することはできません。次のコードは、Monitor を使用してオブジェクトをロックする方法を示しています。
...
キュー oQueue=new Queue();
...
Monitor.Enter(oQueue);
......//oQueue オブジェクトは現在のスレッドによってのみ操作できるようになりました
Monitor.Exit(oQueue);//ロックを解放する
上に示したように、スレッドが Monitor.Enter() メソッドを呼び出してオブジェクトをロックすると、そのオブジェクトはそのスレッドによって所有されます。他のスレッドがこのオブジェクトにアクセスしたい場合、そのオブジェクトが Monitor.Exit() を使用するまで待つことしかできません。ロックを解除する方法。スレッドが最終的にロックを確実に解放するようにするには、try-catch-finally 構造体のfinally コード ブロックに Monitor.Exit() メソッドを記述します。
Monitor によってロックされたオブジェクトについては、それに関連するいくつかの情報がメモリに保存されます。
1 つは現在ロックを保持しているスレッドへの参照です。
2 つ目は準備キューで、ロックを取得する準備ができているスレッドを格納します。
3 番目は待機キューで、現在オブジェクトのステータスが変更されるのを待機しているキューへの参照を保持します。
オブジェクト ロックを所有するスレッドは、ロックを解放する準備ができると、Monitor.Pulse() メソッドを使用して待機キューの最初のスレッドに通知します。これにより、オブジェクト ロックが解放されると、スレッドは準備キューに転送されます。 、準備キュー内 スレッドはすぐにオブジェクト ロックを取得できます。
以下は、lock キーワードと Monitor クラスを使用してスレッドの同期と通信を実装する方法を示す例です。これは、プロデューサーとコンシューマーの典型的な問題でもあります。
このルーチンでは、プロデューサー スレッドとコンシューマー スレッドが交互に実行され、プロデューサーが数値を書き込み、コンシューマーが即座にそれを読み取って表示します (プログラムの本質はコメントで紹介されています)。
使用されるシステム名前空間は次のとおりです。
システムを使用する;
System.Threading を使用します。
まず、操作対象のオブジェクトのクラス Cell を定義します。このクラスには、ReadFromCell() と WriteToCell という 2 つのメソッドがあります。コンシューマ スレッドは ReadFromCell() を呼び出して cellContents の内容を読み取って表示し、プロデューサ プロセスは WriteToCell() メソッドを呼び出して cellContents にデータを書き込みます。
例は次のとおりです。
コード
パブリッククラスのセル
{
int cellContents; //Cell オブジェクトの内容
bool ReaderFlag = false; // ステータスフラグ、trueの場合は読み取り可能、falseの場合は書き込み中
public int ReadFromCell()
{
lock(this) // Lock キーワードは何を保証しますか? 前回の lock の概要を読んでください。
{
if (!readerFlag)//現在読み取れない場合
{
試す
{
//WriteToCell メソッドで Monitor.Pulse() メソッドが呼び出されるのを待ちます
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("消費: {0}",cellContents);
リーダーフラグ = false;
//readerFlag フラグをリセットして、消費行動が完了したことを示します
Monitor.Pulse(this);
//WriteToCell()メソッドを通知(このメソッドは別スレッドで実行され待機中)
}
cellContents を返します。
}
public void WriteToCell(int n)
{
ロック(これ)
{
if(リーダーフラグ)
{
試す
{
Monitor.Wait(this);
}
catch (SynchronizationLockException e)
{
//同期メソッド(Enterを除くMonitorクラスのメソッドを参照)が非同期コード領域で呼び出された場合
Console.WriteLine(e);
}
catch (ThreadInterruptedException e)
{
//スレッドが待機状態の場合は中止する
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("生成: {0}",cellContents);
リーダーフラグ = true;
Monitor.Pulse(this);
// 別のスレッドで待機中の ReadFromCell() メソッドに通知します
}
}
}
プロデューサ クラス CellProd とコンシューマ クラス CellCons は以下で定義されています。どちらも ThreadRun() メソッドを 1 つだけ持っているため、Main() 関数でスレッドに提供される ThreadStart プロキシ オブジェクトがスレッドへの入り口として機能します。
パブリック クラス CellProd
{
Cell cell; // 操作対象のセルオブジェクト
intquantity = 1 // プロデューサが生成する回数、1 に初期化されます。
public CellProd(セルボックス, intリクエスト)
{
//コンストラクタ
セル = ボックス;
数量 = リクエスト;
}
public void ThreadRun()
{
for(int ルーパー=1; ルーパー<=数量; ルーパー++)
cell.WriteToCell(looper); //プロデューサーは操作オブジェクトに情報を書き込みます。
}
}
パブリック クラス CellCons
{
細胞細胞;
整数 = 1;
public CellCons(セルボックス、intリクエスト)
{
//コンストラクタ
セル = ボックス;
数量 = リクエスト;
}
public void ThreadRun()
{
int valReturned;
for(int ルーパー=1; ルーパー<=数量; ルーパー++)
valReturned=cell.ReadFromCell();//コンシューマは操作オブジェクトから情報を読み取ります
}
}
次に、以下の MonitorSample クラスの Main() 関数で、プロデューサとコンシューマとして 2 つのスレッドを作成し、CellProd.ThreadRun() メソッドと CellCons.ThreadRun() メソッドを使用して同じスレッドに対して操作を実行する必要があります。セルオブジェクトを操作します。
コード
パブリッククラスMonitorSample
{
public static void Main(String[] args)
{
int result = 0; //フラグビット。0 の場合はプログラムにエラーがないことを意味します。1 の場合はエラーがあることを意味します。
セル cell = new Cell( );
//以下では cell を使用して CellProd クラスと CellCons クラスを初期化します。生成時間と消費時間は両方とも 20 回です。
CellProd prod = 新しい CellProd(cell, 20);
CellCons cons = new CellCons(cell, 20);
スレッドプロデューサー = new Thread(new ThreadStart(prod.ThreadRun));
スレッドコンシューマー = new Thread(new ThreadStart(cons.ThreadRun));
//プロデューサー スレッドとコンシューマー スレッドの両方が作成されましたが、実行は開始されていません。
試す
{
プロデューサー.Start( );
Consumer.Start( );
プロデューサー.Join();
Consumer.Join();
Console.ReadLine();
}
catch (ThreadStateException e)
{
//スレッドの状態により要求された操作を実行できない場合
Console.WriteLine(e);
結果 = 1;
}
catch (ThreadInterruptedException e)
{
//スレッドが待機状態の場合は中止する
Console.WriteLine(e);
結果 = 1;
}
//Main()関数は値を返しませんが、次のステートメントは実行結果を親プロセスに返すことができます
環境.ExitCode = 結果;
}
}
上記のルーチンでは、Monitor.Pulse() を待つことで同期が行われます。まず、プロデューサは値を生成しますが、同時にコンシューマはプロデューサから生成が完了したことを通知する「パルス」を受け取るまで待機状態になります。その後、コンシューマは消費状態に入ります。そして、プロデューサーはコンシューマーが完了するのを待ち始めます。Monitor.Pulese() によって発行された「パルス」が操作の後に呼び出されます。
その実行結果は単純です。
生産: 1
消費:1
プロデュース:2
消費:2
生産: 3
消費: 3
...
...
生産数:20
消費: 20
実際、この単純な例は、マルチスレッド アプリケーションで発生する可能性のある大きな問題を解決するのに役立ちました。スレッド間の競合を解決する基本的な方法を理解していれば、それをより複雑なプログラムに適用するのは簡単です。
-