「C# で 2 つのスレッドを使用して 1 ~ 100 を交互に出力する 5 つの方法」は、.NET エンジニアのマルチスレッド面接でよくある質問の 1 つで、主に C# 構文とマルチスレッドへの習熟度を調査します。この記事では、この面接の質問を 5 つの方法で実装します。
方法 1: ミューテックスまたはロックを使用する
このアプローチには、Mutex またはロック オブジェクトを使用して 2 つのスレッドを同期することが含まれます。一方のスレッドは偶数の出力を担当し、もう一方のスレッドは奇数の出力を担当します。スレッドはタスクを実行する前に共有ミューテックスまたはロック オブジェクトをロックし、各スレッドがタスクを実行するときに 1 つのスレッドだけが共有リソースにアクセスできるようにします。コードは以下のように表示されます。
class Program
{
static Mutex mutex = new Mutex();
static int count = 1;
static void Main(string[] args)
{
Thread t1 = new Thread(PrintOddNumbers);
Thread t2 = new Thread(PrintEvenNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.ReadLine();
}
static void PrintOddNumbers()
{
while (count <= 100)
{
mutex.WaitOne();
if (count % 2 == 1)
{
Console.WriteLine("Thread 1: " + count);
count++;
}
mutex.ReleaseMutex();
}
}
static void PrintEvenNumbers()
{
while (count <= 100)
{
mutex.WaitOne();
if (count % 2 == 0)
{
Console.WriteLine("Thread 2: " + count);
count++;
}
mutex.ReleaseMutex();
}
}
}
方法 2: AutoResetEvent を使用する
AutoResetEvent は、スレッドが別のスレッドから実行継続の信号を送信されるまで待機できるようにするスレッド同期メカニズムです。一方のスレッドは奇数の出力を担当し、もう一方のスレッドは偶数の出力を担当します。1 つのスレッドが印刷を終了すると、別のスレッドを起動して実行を継続するように信号を送ります。
class Program
{
static AutoResetEvent oddEvent = new AutoResetEvent(false);
static AutoResetEvent evenEvent = new AutoResetEvent(false);
static int count = 1;
static void Main(string[] args)
{
Thread t1 = new Thread(PrintOddNumbers);
Thread t2 = new Thread(PrintEvenNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.ReadLine();
}
static void PrintOddNumbers()
{
while (count <= 100)
{
if (count % 2 == 1)
{
Console.WriteLine("Thread 1: " + count);
count++;
evenEvent.Set();
oddEvent.WaitOne();
}
}
}
static void PrintEvenNumbers()
{
while (count <= 100)
{
if (count % 2 == 0)
{
Console.WriteLine("Thread 2: " + count);
count++;
oddEvent.Set();
evenEvent.WaitOne();
}
}
}
方法 3: モニターを使用する
Monitor は、Mutex と同様の C# の同期メカニズムです。一方のスレッドは奇数の出力を担当し、もう一方のスレッドは偶数の出力を担当します。スレッドはタスクを実行する前に共有 Monitor オブジェクトをロックし、各スレッドがタスクを実行するときに 1 つのスレッドだけが共有リソースにアクセスできるようにします。
class Program
{
static object lockObj = new object();
static int count = 1;
static void Main(string[] args)
{
Thread t1 = new Thread(PrintOddNumbers);
Thread t2 = new Thread(PrintEvenNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.ReadLine();
}
static void PrintOddNumbers()
{
while (count <= 100)
{
lock (lockObj)
{
if (count % 2 == 1)
{
Console.WriteLine("Thread 1: " + count);
count++;
}
}
}
}
static void PrintEvenNumbers()
{
while (count <= 100)
{
lock (lockObj)
{
if (count % 2 == 0)
{
Console.WriteLine("Thread 2: " + count);
count++;
}
}
}
}
}
方法 4: セマフォを使用する
セマフォは、複数のスレッドが共有リソースに同時にアクセスできるようにする同期メカニズムです。一方のスレッドは奇数の出力を担当し、もう一方のスレッドは偶数の出力を担当します。スレッドは、セマフォを取得した後にのみ各スレッドが共有リソースにアクセスできるようにするために、タスクを実行する前にセマフォを待機します。
class Program
{
static Semaphore semaphore = new Semaphore(1, 1);
static int count = 1;
static void Main(string[] args)
{
Thread t1 = new Thread(PrintOddNumbers);
Thread t2 = new Thread(PrintEvenNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.ReadLine();
}
static void PrintOddNumbers()
{ //注意 这里是99,否则会出现101
while (count <= 99)
{
semaphore.WaitOne();
if (count % 2 == 1)
{
Console.WriteLine("Thread 1: " + count);
count++;
}
semaphore.Release();
}
}
static void PrintEvenNumbers()
{
while (count <= 100)
{
semaphore.WaitOne();
if (count % 2 == 0)
{
Console.WriteLine("Thread 2: " + count);
count++;
}
semaphore.Release();
}
}
}
方法 5: タスクと async/await を使用する
C# では、Task キーワードと async/await キーワードを使用して、2 つのスレッド間で実行を簡単に切り替えることができます。一方のスレッドは奇数の出力を担当し、もう一方のスレッドは偶数の出力を担当します。スレッドは、async/await を使用して非同期タスクの完了を待ってからタスクを実行し、各スレッドが非同期タスクの完了後にのみ共有リソースにアクセスするようにします。
class Program
{
static int count = 1;
static void Main(string[] args)
{
Task.Run(PrintOddNumbers);
// 这里改成这个也可以
// var thread1 = new Thread(PrintOddNumbers);
Task.Run(PrintEvenNumbers);
Console.ReadLine();
}
//如果用Thread改成同步方法
static async Task PrintOddNumbers()
{
while (count <= 100)
{
if (count % 2 == 1)
{
Console.WriteLine("Thread 1: " + count);
count++;
//如果用Thread这里改成 Thread.Sleep(1);
await Task.Delay(1);
}
}
}
static async Task PrintEvenNumbers()
{
while (count <= 100)
{
if (count % 2 == 0)
{
Console.WriteLine("Thread 2: " + count);
count++;
await Task.Delay(1);
}
}
}
その効果は以下の5つです。
上記の 5 つの方法にはそれぞれ長所と短所があり、アプリケーションのシナリオや要件によっては、どれも絶対に最適というわけではありません。以下に 5 つのメソッドの簡単な比較と説明を示します。 1. ManualResetEventWaitHandle を使用します。このメソッドの実装は比較的単純ですが、スレッドは共有リソースに相互に排他的にアクセスする必要があるため、パフォーマンスのボトルネックが発生します。さらに、ManualResetEventWaitHandle を使用するには、WaitOne メソッドと Set メソッドを頻繁に呼び出す必要があるため、アプリケーションの応答性が低下する可能性があります。2. AutoResetEventWaitHandle を使用する: このメソッドは実装が比較的簡単で、AutoResetEventWaitHandle を使用するとパフォーマンスのボトルネックを回避できます。ただし、依然として WaitOne メソッドと Set メソッドを頻繁に呼び出す必要があるため、アプリケーションの応答性が低下する可能性があります。3. ロックを使用する: ロックを使用すると、同時に 1 つのスレッドのみが共有リソースにアクセスできるため、パフォーマンスのボトルネックを回避できます。ただし、ロックはスレッドのデッドロックやパフォーマンスの問題を引き起こす可能性があるため、使用には注意が必要です。4. セマフォを使用する セマフォ: この方法により、パフォーマンスのボトルネックが回避され、複数のスレッドが共有リソースに同時にアクセスできるようになります。セマフォでは、複数のライセンスを設定して同時スレッドの数を制御することもできます。ただし、セマフォを使用するとコードが複雑になる可能性があるため、使用には注意が必要です。5. Task と async/await を使用する: この方法ではパフォーマンスのボトルネックを回避でき、Task と async/await を使用するとコードがより簡潔で理解しやすくなります。ただし、タスク間でコンテキストを頻繁に切り替える必要があるため、追加のメモリと CPU オーバーヘッドが発生する可能性があります。要約すると、どの方法を選択するかは、アプリケーションの要件とプログラマの個人的な好みによって異なります。アプリケーションのパフォーマンスを向上させる必要がある場合は、ロックまたはセマフォを使用する必要があります。アプリケーションがより簡潔でわかりやすいコードを必要とする場合は、タスクと async/await を使用する必要があります。
検査の知識ポイント
1. C# プログラミング言語と構文: マルチスレッド プログラムを実装するには、スレッドの作成と管理、共有リソースへのアクセスと同期などの知識を含む、C# プログラミング言語と構文に精通している必要があります。2. マルチスレッド プログラミング: マルチスレッド プログラミングとは、複数のスレッドを同時に実行するプログラミング モデルを指し、アプリケーションのパフォーマンスと応答性を向上させることができます。マルチスレッド プログラミングでは、スレッドの同期、共有リソースへのアクセス、スレッド間の通信などの問題を考慮する必要があります。3. スレッド同期メカニズム: スレッド同期メカニズムとは、共有リソースにアクセスするために複数のスレッドを制御するために使用されるメカニズムを指します。一般的に使用されるスレッド同期メカニズムには、ロック、セマフォ、イベントなどが含まれます。4. 非同期プログラミング: 非同期プログラミングとは、スレッドをブロックせず、タスクの完了後にスレッドに通知するプログラミング モデルを指します。これにより、アプリケーションの応答性とパフォーマンスが向上します。非同期プログラミングでは、非同期および await キーワード、Task や Task<T> などの型、および async メソッドや await メソッドなどの概念に精通している必要があります。