サーチ…


備考

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

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

マルチスレッド・アプリケーションは、ユーザーの入力に対する応答性が高く、開発者が作業負荷の増加に応じてスレッドを追加できるため、スケーラビリティも簡単です。

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

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

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

ThreadSystem.Threading名前空間のメインクラスです。その他のクラスには、 AutoResetEventInterlockedMonitorMutex 、およびThreadPoolます。

System.Threading名前空間に存在する代理人の中には、 ThreadStartTimerCallbackTimerCallbackなどがありWaitCallback

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

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

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

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));
        }
    }
}

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

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));
        }
    }
}

明示的なタスクParallism

    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()
            );

      }

第2のスレッドの作成と起動

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

using System.Threading;

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

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

パラメータでスレッドを開始する

using 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);
    }
}

プロセッサごとに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);
    }
}

同時にデータを読み書きするのを避ける

場合によっては、スレッドが同時にデータを共有したい場合があります。これが起こるときには、コードを認識して間違っている可能性のある部分をロックすることが重要です。 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);
        }
    }
}

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;
    }
}

デッドロック(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スレッドで待機するようにして、workerthreadが完了するようにします。

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
}

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

デッドロック(リソースの保持と待機)

デッドロックは、2つ以上のスレッドがお互いが完了するのを待っているとき、または永遠に待つような方法でリソースを解放するときに発生します。

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

次のコード例でbutton1をクリックすると、アプリケーションは前述のデッドロック状態になり、ハングします

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がそのロックに関して頑固であり、スレッド1が降伏しようとしているので、thread2が常に優先されることに注意してください。また、スレッド1は、リソースAをロックした後の作業をやり直す必要があります。したがって、2つ以上の降伏するスレッドでこのアプローチを実装するときには注意してください.2つのスレッドが作業の最初の部分を続けてお互いに結果を出す場合に起こる状態、いわゆるライブロックを実行するリスクがあります、繰り返し始めます。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow