ASEホーム サイトマップ 交通アクセス お問い合わせ chineese english
             
                                          
最近の技術情報  一覧

                 
 


                 
 


                 
 


                 
 


                 
 


                 
 


                 
 


                 
 


                 
 


                 
 


                 
 


 
トップページ > 技術情報

.NET Framework を理解する。(第3回)

2009年11月17日

.NET
 

スレッドについて

 

マルチコアが広く広まり、今後さらにマルチスレッドの要求は増えていくと思われます。
少なくともマイクロソフト社はそう考えているようで、.NET Framework4.0 ではマルチスレッドを視野に入れたライブラリを多数投入しています。
ここでは、.NET の基本的なスレッドの扱い方について説明していきます。



System.Threading 名前空間

 

.NET では、この名前空間にマルチスレッドの為のライブラリを用意しています。
中でも、Thread クラスは、もっとも基本となるスレッド機能を提供します。


よく利用する基本メンバ
メソッド名Static/Instance内容
AbortInstanceこのスレッドインスタンスの実行を終了します。
IsAliveInstanceスレッドが実行中かどうかを取得します。
IsBackgroundInstanceバックグラウンドスレッドかフォアグラウンドスレッドかを表します。
StartInstanceスレッドの動作を開始します。
JoinInstanceこのスレッドが完了するまで待機します。
BeginCriticalRegionStatic中断すると他のスレッドに悪影響を与えるコードブロックが開始されることを通知します。
EndCriticalRegionStatic中断すると他のスレッドに悪影響を与えるコードブロックが終了することを通知します。

基本的なサンプルを以下に示します。


static void Main(string[] args)
{
    System.Threading.Thread threads
        = new System.Threading.Thread(
            new System.Threading.ThreadStart
                   (SampleThread));

    threads.Start();
    System.Threading.Thread.Sleep(100);
    Console.WriteLine("Thread is running.");
    threads.Join();
}

static void SampleThread()
{
    for (int i = 0; i < 20; i++)
    {
        Console.WriteLine(i);
        System.Threading.Thread.Sleep(500);
    }  
}


スレッドで引数を扱うには

上記サンプルは、もっとも基本的なスレッドを実行していますが、引数は無く単純な固定回数のループと表示を行うだけです。


スレッドクラスのコンストラクタでは、ThreadStart デリゲートを指定しています。このデリゲートは、以下の形で宣言されており、 引数を持つことができません。


public delegate void ThreadStart ()

.NET Framework2.0 からは、ParameterizedThreadStart デリゲートが新たに追加され、スレッドに対して引数を与えることができるようになりました。


public delegate void ParameterizedThreadStart (
	Object obj
)

引数を与える際のサンプルは以下のようになります。


static void Main(string[] args)
{
    System.Threading.Thread thread
        = new System.Threading.Thread(
            new System.Threading.ParameterizedThreadStart
                   (SampleThread));

    thread.Start(20);
    thread.Join();
}

static void SampleThread(object loopCount)
{
    int loop = (int)loopCount;
    for (int i = 0; i < loop; i++)
    {
        Console.WriteLine(i);
        System.Threading.Thread.Sleep(500);
    }
}

より安全に引数を渡して実行する

ただし、これは筆者の主観ですが、引数を元に実行し、共有変数を利用しないスレッドであれば、 スレッドを実行するオブジェクトインスタンスが別途あるほうが望ましいと考えます。


class Program
{
    static void Main(string[] args)
    {
        CountUpThread sample = new CountUpThread(20);
        System.Threading.Thread thread
            = new System.Threading.Thread(
                new System.Threading.ThreadStart
                       (sample.CountUp));

        thread.Start();
        thread.Join();
    }
}

class CountUpThread
{
    private int loops;

    public CountUpThread(int loopCount)
    {
        this.loops = loopCount;
    }

    public void CountUp()
    {
        for (int i = 0; i < this.loops; i++)
        {
            Console.WriteLine(i);
        }
    }
}

こうすることで変数の共有を抑え、バグの作りこみを避けます。


排他制御を考える

 

同時アクセス制御の問題は非常に簡単に起こりえます。
試しに以下のサンプルを用意しました。


static int result;

static void Main(string[] args)
{
    System.Threading.Thread thread1
        = new System.Threading.Thread(
            new System.Threading.ThreadStart
                   (SampleThread));

    System.Threading.Thread thread2
        = new System.Threading.Thread(
            new System.Threading.ThreadStart
                   (SampleThread));

    thread1.Start();
    thread2.Start();

    thread1.Join();
    thread2.Join();

    // 110 が出てほしい。
    Console.WriteLine(result);
}

static void SampleThread()
{
    for (int i = 0; i <= 10; i++)
    {
        int tmp = result;
        System.Threading.Thread.Sleep(1);
        result = tmp + i;
    }
}

内容としては、2つのスレッドで共有の変数「loopCount」をインクリメントして表示します。
当たり前ですが、ソースのコメントにあるような動作をすることは稀でしょう。 (Thread.Sleep でより問題が発生しやすくしていますが、綺麗にでる保障はそもそもありません)
特にこのような問題は確率的に発生する(再現性の低い)障害になりやすく、発見後のデバッグには多大な労力を強いられます。


排他制御を行う

そこで、そのような変数のアクセスには同期制御を行います。
同期制御では、C# では lock 、VB.NET では SyncLock を使います。


static int result;
static object syncObj = new object();

static void Main(string[] args)
{
    System.Threading.Thread thread1
        = new System.Threading.Thread(
            new System.Threading.ThreadStart
                   (SampleThread));

    System.Threading.Thread thread2
        = new System.Threading.Thread(
            new System.Threading.ThreadStart
                   (SampleThread));

    thread1.Start();
    thread2.Start();

    thread1.Join();
    thread2.Join();

    // 110 が出てほしい。
    Console.WriteLine(result);
}

static void SampleThread()
{
    for (int i = 0; i <= 10; i++)
    {
        lock (syncObj)
        {
            int tmp = result;
            System.Threading.Thread.Sleep(1);
            result = tmp + i;
        }
    }
}

同様の同期オブジェクトを使った方法では、Monitor クラスを利用することもできます。


static void SampleThread()
{
    for (int i = 0; i <= 10; i++)
    {
        System.Threading.Monitor.Enter(syncObj);

        int tmp = result;
        System.Threading.Thread.Sleep(1);
        result = tmp + i;

        System.Threading.Monitor.Exit(syncObj);
    }
}

複数プロセス間で排他制御を行う

上記で同期オブジェクトを利用した方法を紹介しましたが、これらは一つのプロセス内で有効です。
1個のプログラムの内部での排他制御はできますが、プログラムを2個以上起動した際の排他制御にはなりません。


複数のプロセス間で共有利用するリソース「共有メモリ」や「ファイル」等、 プログラムの外側にあるリソースを利用する際も排他制御が必要でしょう。
そのようなプログラムでは、Mutex を利用します。


static void Main(string[] args)
{
    System.Threading.Mutex mutex
        = new System.Threading.Mutex(false, "SampleMutex");

    Console.WriteLine("Waiting!");

    // ここで「SampleMutex」権限を取得するので、
    // 同名の Mutex は待ち状態になる。
    mutex.WaitOne();

    Console.WriteLine("Running!");
    mutex.ReleaseMutex();
}

Mutex の仕組みそのものは、Win32API 時代から存在しており、ゲームソフトの多重起動禁止等でよく利用されています。
その例に倣って、多重起動の禁止コードを作成します


static void Main(string[] args)
{
    bool isNotRunning = false;
    System.Threading.Mutex mutex
        = new System.Threading.Mutex(false, "SampleMutex", out isNotRunning);

    if (!isNotRunning)
    {
        Console.WriteLine("is already running!");
        return;
    }

    Console.WriteLine("Waiting!");
    mutex.WaitOne();

    Console.WriteLine("Running!");
    mutex.ReleaseMutex();
}


他のスレッド、スレッドライクな操作

 

スレッド等の並列で処理を行う仕組みは、同期さえ気にしなければ、他にもいくつかの方法があります。
ここでは、その種類を簡単に 2 つほど紹介します。


デリゲートを使った非同期処理

デリゲートには、BeginInvoke というメソッドがあり、このメソッドから実行することで、非同期に処理を開始することができます。


delegate void DThread();

static void Main(string[] args)
{
    DThread dthread = new DThread(SampleThread);

    dthread.BeginInvoke(null, null);
    Console.WriteLine("Thread is running.");
    System.Threading.Thread.Sleep(11000);
}

static void SampleThread()
{
    for (int i = 0; i < 20; i++)
    {
        Console.WriteLine(i);
        System.Threading.Thread.Sleep(500);
    }
}

スレッドプールを利用したスレッド

スレッドクラスインスタンスを利用するには意外とオーバーヘッドがあります。
すぐに処理が終了するようなメソッドは、スレッドプールを利用するのが効果的です。


static void Main(string[] args)
{
    System.Threading.ThreadPool.QueueUserWorkItem(
        new System.Threading.WaitCallback(SampleThread));

    Console.WriteLine("Thread is running.");
    System.Threading.Thread.Sleep(11000);
}

static void SampleThread(object dummy)
{
    for (int i = 0; i < 20; i++)
    {
        Console.WriteLine(i);
        System.Threading.Thread.Sleep(500);
    }
}

Queue の名前があるように、待ち行列でスレッド処理を行います。
スレッドプールには、同時実行数が限られており、同時実行できない分はキューに保持します。


尚、delegate を利用した処理は、内部的に ThreadPool を利用します。


次回は、ApplicationDomain 等の .NET のプロセス制御、実効制御について紹介いたします。


関連するエントリー

 
 
現在ページの上部へ戻る