C#におけるプロセス/スレッド/マルチスレッド/AppDomain完全ガイド

C#におけるプロセス/スレッド/マルチスレッド/AppDomain完全ガイド

C#における並列処理与非同期処理は、現代のアプリケーション開発において不可欠な技術です。本稿では、プロセス、スレッド、マルチスレッド、Task、そしてAppDomainについて詳細に解説します。

1. Taskの基本操作

Taskクラスには複数の待機メソッドが用意されています。Task.Waitは単一のタスクの完了を待機し、Task.WaitAllは複数のタスク全ての完了を待機します。一方、Task.WaitAnyは、複数のタスクのうちいずれか 하나が完了するまで待機します。

非同期メソッドの宣言では、戻り値としてTask、Task<T>、またはvoidを指定します。await式を使用してTaskを待機する場合、そのTaskの実行が完了するまで、await以降のコードは実行されません。

Taskの利点は、複数のタスクがそれぞれ異なるスレッドで実行でき、前方のタスクの完了を待たずに次のタスクを開始できる点です。TaskはThreadを基盤として実装されており、非同期処理を通じて同時に複数の処理を実行することが可能です。

2. 非同期とマルチスレッドの違い

非同期処理は同時に複数のタスクを実行するための処理形態であり、マルチスレッドはその実装手段の一つです。両者の概念を混同しないことが重要です。

Task.runは新しいスレッドを起動してTaskを実行しますが、async/awaitキーワード自体は新しいスレッドを作成しません。これらはタスク処理の構文的なSugarであり、実際の処理はスレッドプール内のスレッドで実行されます。

3. スレッドの詳細

3.1 スレッドの作成手順

新しいスレッドを作成するには、以下の4つの手順に従います。まず、スレッドで実行するメソッドを定義します。次に、System.Threading名前空間をインポートし、Threadクラスのインスタンスを生成します。この時点でスレッドは作成されますがまだ実行されていません最後に、Startメソッドを呼び出してスレッドの実行を開始します。

3.2 前景スレッドと背景スレッド

.NETアプリケーションには、前景スレッドと背景スレッドの2種類があります。アプリケーションが終了するには、すべての前景スレッドの実行が完了する必要があります。背景スレッドは、前景スレッドが完了すれば自動的に終了し、アプリケーションの終了を阻止しません。

.NETでは、Threadクラスを使用して作成したスレッドはデフォルトで前景スレッドです。ThreadPoolやTaskを使用して作成したスレッドは背景スレッドとして動作します。

前景スレッドが使用中の場合、アプリケーションのウィンドウを閉じてもプロセスが終了しない場合があります。これが某些なWinFormアプリケーションで全てのウィンドウを閉じてもタスクマネージャーにプロセスが,依然として表示される原因です。

3.3 スレッドの操作

using System;
using System.Threading;

class ThreadSample
{
    public static void ExecuteWorker()
    {
        Console.WriteLine("Worker thread started");
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine($"Processing item {i}");
            Thread.Sleep(500);
        }
        Console.WriteLine("Worker thread completed");
    }

    public static void Main()
    {
        Thread worker = new Thread(ExecuteWorker);
        worker.Start();
        
        Console.WriteLine("Main thread continues");
        worker.Join();
        Console.WriteLine("All work completed");
    }
}

3.4 スレッドの優先順位

Thread.Priorityプロパティを使用して、スレッドの優先順位を設定できます。優先順位が高いスレッドは、より多くのCPU時間を受け取る可能性があります。

class PriorityDemo
{
    private volatile bool running = true;

    public void Stop() { running = false; }

    public void RunCounter()
    {
        long count = 0;
        while (running) count++;
        Console.WriteLine($"{Thread.CurrentThread.Name}: {count}");
    }
}

static void TestPriority()
{
    var sample = new PriorityDemo();
    
    Thread highPriority = new Thread(sample.RunCounter);
    highPriority.Name = "HighPriority";
    highPriority.Priority = ThreadPriority.Highest;

    Thread lowPriority = new Thread(sample.RunCounter);
    lowPriority.Name = "LowPriority";
    lowPriority.Priority = ThreadPriority.Lowest;

    highPriority.Start();
    lowPriority.Start();

    Thread.Sleep(1000);
    sample.Stop();
    
    highPriority.Join();
    lowPriority.Join();
}

4. Taskの活用

Taskクラスは、非同期操作をより効率的に扱うために設計されています。Taskは内部的にThreadPoolを使用しますが、ローカルキューを採用することでスレッド間の競合を軽減しています。

4.1 Taskの作成方法

using System;
using System.Threading.Tasks;

class TaskExample
{
    public static void MethodNoParam()
    {
        Console.WriteLine("Task without parameters");
    }

    public static void MethodWithParam(object parameter)
    {
        Console.WriteLine($"Task with parameter: {parameter}");
    }

    public static string MethodWithReturn(object parameter)
    {
        return $"Result: {parameter}";
    }

    public static void Main()
    {
        // 方法1: TaskFactoryを使用(作成直後に実行)
        Task t1 = Task.Factory.StartNew(() => 
            Console.WriteLine("Task 1 started"));

        // 方法2: new Task + Start
        Task t2 = new Task(MethodNoParam);
        t2.Start();

        // パラメータ付きのタスク
        Task t3 = new Task(MethodWithParam, "Hello");
        t3.Start();

        // 戻り値付きのタスク
        Task<string> t4 = new Task<string>(MethodWithReturn, "World");
        t4.Start();
        
        Console.WriteLine($"Result: {t4.Result}");

        Task.WaitAll(t1, t2, t3, t4);
    }
}

4.2 ConfigureAwaitの詳細

ConfigureAwaitは、非同期操作の完了後に実行を継続するコンテキストを制御します。ConfigureAwait(true)はデフォルトの動作で、元の同期コンテキストに戻って継続を実行します。ConfigureAwait(false)は、コンテキストを捕捉せずにスレッドプールの任意のスレッドで継続を実行します。

UIアプリケーションでは、await後のコードがUIスレッドで実行される必要があります。しかし、ライブラリコードやASP.NET Coreでは、ConfigureAwait(false)を使用することでパフォーマンスを向上させ、デッドロックを避けることができます。

using System;
using System.Net.Http;
using System.Threading.Tasks;

class ConfigureAwaitDemo
{
    private static readonly HttpClient httpClient = new HttpClient();

    // UI層のコード(デフォルトの動作)
    public async Task<string> LoadDataForUI()
    {
        Console.WriteLine($"Before await: {Thread.CurrentThread.ManagedThreadId}");
        var data = await FetchDataAsync();
        // ここでUIを更新できる(UIスレッドで実行されるため)
        Console.WriteLine($"After await: {Thread.CurrentThread.ManagedThreadId}");
        return data;
    }

    // ライブラリコード(ConfigureAwait(false)を使用)
    private async Task<string> FetchDataAsync()
    {
        await Task.Delay(100).ConfigureAwait(false);
        // ここではUIコンテキストを捕捉しない
        return "Data loaded";
    }
}

5. ThreadPoolの概要

ThreadPoolは、スレッドの再利用を可能にする仕組みです。Task.Delayなどの非同期操作は内部的にThreadPoolを使用しています。スレッドが完了しても破棄されず、アイドル状態のままプールにとどまり、次のタスクを処理するために再利用されます。

5.1 ThreadPoolの特徴

ThreadPoolの利点は、開発者がスレッドの作成と破棄を管理しなくて済むことです。スレッドの作成には約1MBのメモリが必要ですが、ThreadPoolを使用することでメモリ使用量を削減できます。

一方、ThreadPoolには制御が難しいという欠点もあります。どのスレッドがいつ実行されるかを予測できず、タスクの完了を待機する標準的な方法が用意されていません。

5.2 ThreadPoolの使用例

using System;
using System.Threading;

class ThreadPoolDemo
{
    public static void ProcessWork(object state)
    {
        string taskName = state?.ToString() ?? "Anonymous";
        Console.WriteLine($"Task {taskName} started on thread {Thread.CurrentThread.ManagedThreadId}");
        
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine($"  {taskName}: step {i}");
            Thread.Sleep(100);
        }
        
        Console.WriteLine($"Task {taskName} completed");
    }

    public static void Main()
    {
        Console.WriteLine("Starting ThreadPool tasks...");
        
        for (int i = 0; i < 3; i++)
        {
            string taskName = $"Task-{i}";
            ThreadPool.QueueUserWorkItem(ProcessWork, taskName);
        }

        Thread.Sleep(2000);
        Console.WriteLine("All tasks queued");
    }
}

6. Processクラスの活用

System.Diagnostics.Processクラスを使用して、システムのプロセスを管理できます。プロセスの開始、停止、監視,以及进程情報の取得が可能です。

using System;
using System.Diagnostics;
using System.Linq;

class ProcessDemo
{
    public static void ListRunningProcesses()
    {
        var processes = Process.GetProcesses()
            .OrderBy(p => p.Id)
            .Take(10);

        foreach (var process in processes)
        {
            Console.WriteLine($"ID: {process.Id}, Name: {process.ProcessName}");
        }
    }

    public static void StartExternalProgram()
    {
        try
        {
            ProcessStartInfo startInfo = new ProcessStartInfo
            {
                FileName = "notepad.exe",
                Arguments = "test.txt",
                UseShellExecute = false,
                CreateNoWindow = true
            };

            using (Process p = Process.Start(startInfo))
            {
                Console.WriteLine($"Started process: {p.Id}");
                p.WaitForExit(3000);
                
                if (!p.HasExited)
                {
                    p.Kill();
                    Console.WriteLine("Process terminated");
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

7. AppDomainの詳細

AppDomain(アプリケーションドメイン)は、.NETアプリケーションにおける論理的な境界です。プロセス内のアプリケーションを分離するための仕組みであり、異なるAppDomain間のコードが互いに直接アクセスすることを防ぎます。

7.1 AppDomainの特徴

従来の.NETアプリケーションでは、実行ファイルはプロセスに直接ロードされるのではなく、AppDomainにロードされます。これにより、1つのプロセス内で複数のアプリケーションを実行でき、あるAppDomainでエラーが発生しても他のAppDomainに影響を与えられません。

7.2 AppDomainの使用例

using System;
using System.Reflection;

class AppDomainDemo
{
    public static void Main()
    {
        AppDomain current = AppDomain.CurrentDomain;
        Console.WriteLine($"Current Domain: {current.FriendlyName}");
        Console.WriteLine($"Base Directory: {current.BaseDirectory}");

        // 新しいAppDomainの作成
        AppDomain newDomain = AppDomain.CreateDomain("SecondaryDomain");
        
        newDomain.AssemblyLoad += (sender, args) =>
        {
            Console.WriteLine($"Assembly loaded: {args.LoadedAssembly.FullName}");
        };

        // 別のAppDomainでコードを実行
        newDomain.DoCallBack(() =>
        {
            Console.WriteLine("Executing in different domain");
            Console.WriteLine($"Current Domain: {AppDomain.CurrentDomain.FriendlyName}");
        });

        // AppDomainのアンロード
        AppDomain.Unload(newDomain);
        Console.WriteLine("Secondary domain unloaded");
    }
}

8. 同期と非同期の違い

同期処理は、1つの処理が完了してから次の処理を開始します。メソッドの呼び出しが結果を返すまで、呼び出し元のスレッドはブロックされます。

非同期処理では、メソッドの呼び出し後、結果を待たずに次の処理に進みます。処理の完了は、コールバック、状態ポーリング、またはawaitキーワードによって通知されます。

9. -blockingとnon-blocking

阻塞(Blocking)呼び出しは、結果が返されるまで現在のスレッドの実行を停止します。一方、非阻塞(Non-blocking)呼び出しは、即座に制御を返し、結果をポーリング하거나コールバックで受信します。

まとめ

C#における並列処理技術は、アプリケーションのパフォーマンスと応答性を向上させるために不可欠です。Threadは低レベルの制御を提供し、Taskはより高度な抽象化を提供します。ThreadPoolはスレッドの再利用による効率化し、AppDomainはアプリケーションの分離を提供します。

適切な技術を選択するには、処理の性質、要件、およびパフォーマンスへの影響を考慮する必要があります。UI応答性を維持しながらバックグラウンドで長時間実行される処理にはTaskを使用し、细腻な制御が必要な場合はThreadを使用することが推奨されます。

タグ: C# Thread Task ThreadPool async

7月10日 22:45 投稿