ねじ切り

# データの読み取りと書き込みを同時に回避する

スレッドで同時にデータを共有したい場合があります。これが発生した場合、コードを認識し、問題が発生する可能性のある部分をロックすることが重要です。 2 つのスレッドのカウントの簡単な例を以下に示します。

以下は危険な (間違った) コードです:

using System.Threading;

class MainClass 
{    
    static int count { get; set; }

    static void Main() 
    {
        for (int i = 1; i <= 2; i++)
        {
            var thread = new Thread(ThreadMethod);
            thread.Start(i);
            Thread.Sleep(500);
        }
    }

    static void ThreadMethod(object threadNumber) 
    {
        while (true)
        {
            var temp = count;
            System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
            Thread.Sleep(1000);
            count = temp + 1;
            System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
            Thread.Sleep(1000);
        }
    }
}

1、2、3、4、5 を数える代わりに、1、1、2、2、3 を数えることに気付くでしょう...

この問題を解決するには、ロックする必要があります 複数の異なるスレッドが同時に読み書きできないように、count の値。ロックとキーを追加することで、スレッドがデータに同時にアクセスするのを防ぐことができます。

using System.Threading;

class MainClass
{

    static int count { get; set; } 
    static readonly object key = new object();

    static void Main()
    {
        for (int i = 1; i <= 2; i++)
        {
            var thread = new Thread(ThreadMethod);
            thread.Start(i);
            Thread.Sleep(500);
        }
    }

    static void ThreadMethod(object threadNumber)
    {
        while (true)
        {
            lock (key) 
            {
                var temp = count;
                System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
                Thread.Sleep(1000);
                count = temp + 1;
                System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
            }
            Thread.Sleep(1000);
        }
    }
}

# 2 番目のスレッドの作成と開始

複数の長い計算を行っている場合は、コンピューターの異なるスレッドで同時に実行できます。これを行うために、新しいスレッドを作成します 別のメソッドを指すようにします。

using System.Threading;

class MainClass {
    static void Main() {
        var thread = new Thread(Secondary);
        thread.Start();
    }

    static void Secondary() {
        System.Console.WriteLine("Hello World!");
    }
}

# Parallel.ForEach ループ

速度を上げたい foreach ループがあり、出力の順序が気にならない場合は、次のようにして並列 foreach ループに変換できます。

using System;
using System.Threading;
using System.Threading.Tasks;

public class MainClass {

    public static void Main() {
        int[] Numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        // Single-threaded
        Console.WriteLine("Normal foreach loop: ");
        foreach (var number in Numbers) {
            Console.WriteLine(longCalculation(number));
        }
        // This is the Parallel (Multi-threaded solution)
        Console.WriteLine("Parallel foreach loop: ");
        Parallel.ForEach(Numbers, number => {
            Console.WriteLine(longCalculation(number));
        });
    }

    private static int longCalculation(int number) {
        Thread.Sleep(1000); // Sleep to simulate a long calculation
        return number * number;
    }
}

# シンプルで完全なスレッド化のデモ

class Program
{
    static void Main(string[] args)
    {
        // Create 2 thread objects.  We're using delegates because we need to pass 
        // parameters to the threads.  
        var thread1 = new Thread(new ThreadStart(() => PerformAction(1)));
        var thread2 = new Thread(new ThreadStart(() => PerformAction(2)));

        // Start the threads running 
        thread1.Start();
        // NB: as soon as the above line kicks off the thread, the next line starts; 
        // even if thread1 is still processing.
        thread2.Start();

        // Wait for thread1 to complete before continuing
        thread1.Join();
        // Wait for thread2 to complete before continuing
        thread2.Join();

        Console.WriteLine("Done");
        Console.ReadKey();
    }

    // Simple method to help demonstrate the threads running in parallel.
    static void PerformAction(int id)
    {
        var rnd = new Random(id);
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Thread: {0}: {1}", id, i);
            Thread.Sleep(rnd.Next(0, 1000));
        }
    }
}

# デッドロック (リソースを保持して待機)

デッドロックとは、2 つ以上のスレッドが相互に完了するのを待っているか、リソースを解放するために永遠に待機している場合に発生します。

スレッド 1 がリソース A のロックを保持し、リソース B が解放されるのを待っているときに、スレッド 2 がリソース B を保持し、リソース A が解放されるのを待っている場合、それらはデッドロックになります。

次のサンプル コードのボタン 1 をクリックすると、アプリケーションが前述のデッドロック状態になり、ハングします

private void button_Click(object sender, EventArgs e)
{
    DeadlockWorkers workers = new DeadlockWorkers();
    workers.StartThreads();
    textBox.Text = workers.GetResult();
}

private class DeadlockWorkers
{
    Thread thread1, thread2;

    object resourceA = new object();
    object resourceB = new object();

    string output;

    public void StartThreads()
    {
        thread1 = new Thread(Thread1DoWork);
        thread2 = new Thread(Thread2DoWork);
        thread1.Start();
        thread2.Start();
    }

    public string GetResult()
    {
        thread1.Join();
        thread2.Join();
        return output;
    }

    public void Thread1DoWork()
    {
        Thread.Sleep(100);
        lock (resourceA)
        {
            Thread.Sleep(100);
            lock (resourceB)
            {
                output += "T1#";
            }
        }
    }

    public void Thread2DoWork()
    {
        Thread.Sleep(100);
        lock (resourceB)
        {
            Thread.Sleep(100);
            lock (resourceA)
            {
                output += "T2#";
            }
        }
    }
}

この方法でデッドロックを回避するには、Monitor.TryEnter(lock_object, timeout_in_milliseconds) を使用して、オブジェクトがロックされているかどうかを確認します。 Monitor.TryEnter が timeout_in_milliseconds の前に lock_object のロックの取得に成功しなかった場合、false を返し、スレッドに他の保持されたリソースを解放して譲歩する機会を与え、上記のわずかに変更されたバージョンのように、他のスレッドに完了する機会を与えます。 :

private void button_Click(object sender, EventArgs e)
{
    MonitorWorkers workers = new MonitorWorkers();
    workers.StartThreads();
    textBox.Text = workers.GetResult();
}

private class MonitorWorkers
{
    Thread thread1, thread2;

    object resourceA = new object();
    object resourceB = new object();

    string output;

    public void StartThreads()
    {
        thread1 = new Thread(Thread1DoWork);
        thread2 = new Thread(Thread2DoWork);
        thread1.Start();
        thread2.Start();
    }

    public string GetResult()
    {
        thread1.Join();
        thread2.Join();
        return output;
    }

    public void Thread1DoWork()
    {
        bool mustDoWork = true;
        Thread.Sleep(100);
        while (mustDoWork)
        {
            lock (resourceA)
            {
                Thread.Sleep(100);
                if (Monitor.TryEnter(resourceB, 0))
                {
                    output += "T1#";
                    mustDoWork = false;
                    Monitor.Exit(resourceB);
                }
            }
            if (mustDoWork) Thread.Yield();
        }
    }

    public void Thread2DoWork()
    {
        Thread.Sleep(100);
        lock (resourceB)
        {
            Thread.Sleep(100);
            lock (resourceA)
            {
                output += "T2#";
            }
        }
    }
}

この回避策は、スレッド 2 が常にロックを優先するように、スレッド 2 がそのロックについて頑固であり、スレッド 1 が喜んで解放することに依存していることに注意してください。また、スレッド 1 は、リソース A をロックした後に行った作業をやり直さなければならないことに注意してください。したがって、このアプローチを複数の譲歩スレッドで実装する場合は注意してください。いわゆるライブロック (2 つのスレッドが作業の最初のビットを実行し続け、その後相互に譲歩した場合に発生する状態) に入るリスクがあるためです。 、繰り返しやり直します。

# プロセッサごとに 1 つのスレッドを作成する

Environment.ProcessorCount 論理の数を取得します

次に、CLR は各スレッドを論理プロセッサにスケジュールします。これは、理論的には、各スレッドが異なる論理プロセッサ上にあること、すべてのスレッドが単一の論理プロセッサ上にあること、またはその他の組み合わせであることを意味します。

using System;
using System.Threading;

class MainClass {
    static void Main() {
        for (int i = 0; i < Environment.ProcessorCount; i++) {
            var thread = new Thread(Secondary);
            thread.Start(i);
        }
        
    }

    static void Secondary(object threadNumber) {
        System.Console.WriteLine("Hello World from thread: " + threadNumber);
    }
}

# タスクを使用した単純な完全なスレッド化のデモ

class Program
{
    static void Main(string[] args)
    {
        // Run 2 Tasks.  
        var task1 = Task.Run(() => PerformAction(1)));
        var task2 = Task.Run(() => PerformAction(2)));

        // Wait (i.e. block this thread) until both Tasks are complete.
        Task.WaitAll(new [] { task1, task2 });
        
        Console.WriteLine("Done");
        Console.ReadKey();
    }

    // Simple method to help demonstrate the threads running in parallel.
    static void PerformAction(int id)
    {
        var rnd = new Random(id);
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Task: {0}: {1}", id, i);
            Thread.Sleep(rnd.Next(0, 1000));
        }
    }
}

# 明示的なタスクの並列処理


   private static void explicitTaskParallism()
    {
        Thread.CurrentThread.Name = "Main";

        // Create a task and supply a user delegate by using a lambda expression. 
        Task taskA = new Task(() => Console.WriteLine($"Hello from task {nameof(taskA)}."));
        Task taskB = new Task(() => Console.WriteLine($"Hello from task {nameof(taskB)}."));

        // Start the task.
        taskA.Start();
        taskB.Start();

        // Output a message from the calling thread.
        Console.WriteLine("Hello from thread '{0}'.",
                          Thread.CurrentThread.Name);
        taskA.Wait();
        taskB.Wait();
        Console.Read();
    }

# 暗黙的なタスクの並列処理


   private static void Main(string[] args)
    {
        var a = new A();
        var b = new B();
        //implicit task parallelism
        Parallel.Invoke(
            () => a.DoSomeWork(),
            () => b.DoSomeOtherWork()
            );

      }

# パラメータ付きスレッドの開始

System.Threading の使用;

class MainClass {
    static void Main() {
        var thread = new Thread(Secondary);
        thread.Start("SecondThread");
    }

    static void Secondary(object threadName) {
        System.Console.WriteLine("Hello World from thread: " + threadName);
    }
}

# デッドロック (互いに待機している 2 つのスレッド)

デッドロックとは、2 つ以上のスレッドが相互に完了するのを待っているか、リソースを解放するために永遠に待機している場合に発生します。

2 つのスレッドが相互に完了するのを待機する典型的なシナリオは、Windows フォーム GUI スレッドがワーカー スレッドを待機し、ワーカー スレッドが GUI スレッドによって管理されるオブジェクトを呼び出そうとする場合です。このコード例では、button1 をクリックすると、ハングするプログラム。

private void button1_Click(object sender, EventArgs e)
{
    Thread workerthread= new Thread(dowork);
    workerthread.Start();
    workerthread.Join();
    // Do something after
}

private void dowork()
{
    // Do something before
    textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
    // Do something after
}

workerthread.Join() workerthread が完了するまで呼び出しスレッドをブロックする呼び出しです。textBox1.Invoke(invoke_delegate) は、GUI スレッドが invoke_delegate を処理するまで呼び出しスレッドをブロックする呼び出しですが、GUI スレッドが呼び出しスレッドの完了を既に待機している場合、この呼び出しはデッドロックを引き起こします。

これを回避するには、代わりに非ブロッキングの方法でテキストボックスを呼び出すことができます:

private void dowork()
{
    // Do work
    textBox1.BeginInvoke(new Action(() => textBox1.Text = "Some Text"));
    // Do work that is not dependent on textBox1 being updated first
}

ただし、最初に更新されるテキストボックスに依存するコードを実行する必要がある場合、これは問題を引き起こします。その場合、それを呼び出しの一部として実行しますが、これにより GUI スレッドで実行されることに注意してください。

private void dowork()
{
    // Do work
    textBox1.BeginInvoke(new Action(() => {
        textBox1.Text = "Some Text";
        // Do work dependent on textBox1 being updated first, 
        // start another worker thread or raise an event
    }));
    // Do work that is not dependent on textBox1 being updated first
}

または、まったく新しいスレッドを開始し、そのスレッドで GUI スレッドを待機させて、ワーカースレッドが完了するようにします。

private void dowork()
{
    // Do work
    Thread workerthread2 = new Thread(() =>
    {
        textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
        // Do work dependent on textBox1 being updated first, 
        // start another worker thread or raise an event
    });
    workerthread2.Start();
    // Do work that is not dependent on textBox1 being updated first
}

相互待機のデッドロックに陥るリスクを最小限に抑えるために、スレッド間の循環参照は可能な限り避けてください。下位のスレッドが上位のスレッドにのみメッセージを残し、それらを待機しないスレッドの階層では、この種の問題は発生しません。ただし、リソースのロックに基づくデッドロックに対しては依然として脆弱です。

# コメント

スレッド 他の部分とは独立して実行できるプログラムの一部です。他のスレッドと同時にタスクを実行できます。 マルチスレッド 一度に複数の操作を実行できるように、プログラムが並行処理を実行できるようにする機能です。

たとえば、フォアグラウンドで他のタスクを実行しながら、スレッドを使用してバックグラウンドでタイマーまたはカウンターを更新できます。

マルチスレッド アプリケーションは、ワークロードの増加に応じて開発者がスレッドを追加できるため、ユーザー入力に対する応答性が高く、拡張も容易です。

デフォルトでは、C# プログラムには 1 つのスレッド (メイン プログラム スレッド) があります。ただし、セカンダリ スレッドを作成して、プライマリ スレッドと並行してコードを実行するために使用できます。このようなスレッドはワーカー スレッドと呼ばれます。

スレッドの操作を制御するために、CLR はスレッド スケジューラと呼ばれるオペレーティング システムに関数を委任します。スレッド スケジューラは、すべてのスレッドに適切な実行時間が割り当てられることを保証します。また、ブロックまたはロックされているスレッドが CPU 時間をあまり消費していないことも確認します。

.NET Framework System.Threading 名前空間により、スレッドの使用が容易になります。 System.Threading は、多数のクラスとインターフェイスを提供することにより、マルチスレッドを有効にします。特定のスレッドに型とクラスを提供するだけでなく、スレッドのコレクション、タイマー クラスなどを保持する型も定義します。また、共有データへの同期アクセスを許可することで、そのサポートも提供します。

Thread System.Threading のメイン クラスです。 名前空間。その他のクラスには AutoResetEvent が含まれます 、 InterlockedMonitorMutex 、および ThreadPool .

System.Threading に存在するデリゲートの一部 名前空間インクルードThreadStartTimerCallback 、および WaitCallback .

System.Threading の列挙 名前空間には ThreadPriority が含まれます 、 ThreadState 、および EventResetMode .

.NET Framework 4 以降のバージョンでは、System.Threading.Tasks.Parallel によってマルチスレッド プログラミングがより簡単かつシンプルになります。 および System.Threading.Tasks.Task クラス、Parallel LINQ (PLINQ)、System.Collections.Concurrent の新しい並行コレクション クラス 名前空間、および新しいタスクベースのプログラミング モデルです。