天天看點

C#多線程程式設計執行個體介紹

問題的提出

所謂單個寫入程式/多個閱讀程式的線程同步問題,是指任意數量的線程通路共享資源時,寫入程式(線程)需要修改共享資源,而閱讀程式(線程)需要讀取資料。在這個同步問題中,很容易得到下面二個要求:

1)當一個線程正在寫入資料時,其他線程不能寫,也不能讀;  

2)當一個線程正在讀入資料時,其他線程不能寫,但能夠讀。

在資料庫應用程式環境中經常遇到這樣的問題。比如說,有n個最終使用者,他們都要同時通路同一個資料庫。其中有m個使用者要将資料存入資料庫,n-m個使用者要讀取資料庫中的記錄。

很顯然,在這個環境中,我們不能讓兩個或兩個以上的使用者同時更新同一條記錄,如果兩個或兩個以上的使用者都試圖同時修改同一記錄,那麼該記錄中的資訊就會被破壞。

我們也不讓一個使用者更新資料庫記錄的同時,讓另一使用者讀取記錄的内容。因為讀取的記錄很有可能同時包含了更新和沒有更新的資訊,也就是說這條記錄是無效的記錄。

實作分析

規定任一線程要對資源進行寫或讀操作前必須申請鎖。根據操作的不同,分為閱讀鎖和寫入鎖,操作完成之後應釋放相應的鎖。将單個寫入程式/多個閱讀程式的要求改變一下,可以得到如下的形式:

一個線程申請閱讀鎖的成功條件是:目前沒有活動的寫入線程。

一個線程申請寫入鎖的成功條件是:目前沒有任何活動(對鎖而言)的線程。

是以,為了标志是否有活動的線程,以及是寫入還是閱讀線程,引入一個變量m_nActive,如果m_nActive > 0,則表示目前活動閱讀線程的數目,如果m_nActive=0,則表示沒有任何活動線程,m_nActive <0,表示目前有寫入線程在活動,注意m_nActive<0,時隻能取-1的值,因為隻允許有一個寫入線程活動。

為了判斷目前活動線程擁有的鎖的類型,我們采用了線程局部存儲技術(請參閱其它參考書籍),将線程與特殊标志位關聯起來。

申請閱讀鎖的函數原型為:public void AcquireReaderLock( int millisecondsTimeout ),其中的參數為線程等待排程的時間。函數定義如下:

public void AcquireReaderLock(int millisecondsTimeout)
        {

            // m_mutext很快可以得到,以便進入臨界區 

            m_mutex.WaitOne();
            // 是否有寫入線程存在    
            bool bExistingWriter = (m_nActive < 0);

            if (bExistingWriter)
            { //等待閱讀線程數目加1,當有鎖釋放時,根據此數目來排程線程 

                m_nWaitingReaders++;
            }
            else
            { //目前活動線程加1 
                m_nActive++;
            }

            m_mutex.ReleaseMutex();

            //存儲鎖标志為Reader    
            System.LocalDataStoreSlot slot = Thread.GetNamedDataSlot(m_strThreadSlotName);
            object obj = Thread.GetData(slot);
            LockFlags flag = LockFlags.None;

            if (obj != null)
                flag = (LockFlags)obj;
            if (flag == LockFlags.None)
            {
                Thread.SetData(slot, LockFlags.Reader);
            }
            else
            {
                Thread.SetData(slot, (LockFlags)((int)flag | (int)LockFlags.Reader));
            }
            if (bExistingWriter)
            { //等待指定的時間    
                this.m_aeReaders.WaitOne(millisecondsTimeout, true);

            }
        }       

它首先進入臨界區(用以在多線程環境下保證活動線程數目的操作的正确性)判斷目前活動線程的數目,如果有寫線程(m_nActive<0)存在,則等待指定的時間并且等待的閱讀線程數目加1。如果目前活動線程是讀線程(m_nActive>=0),則可以讓讀線程繼續運作。

申請寫入鎖的函數原型為:public void AcquireWriterLock( int millisecondsTimeout ),其中的參數為等待排程的時間。函數定義如下:

public void AcquireWriterLock(int millisecondsTimeout)
        {
            // m_mutext很快可以得到,以便進入臨界區    
            m_mutex.WaitOne();
            // 是否有活動線程存在 

            bool bNoActive = m_nActive == 0;

            if (!bNoActive)
            {
                m_nWaitingWriters++;

            }
            else
            {
                m_nActive--;
            }
            m_mutex.ReleaseMutex();
            //存儲線程鎖标志 

            System.LocalDataStoreSlot slot = Thread.GetNamedDataSlot("myReaderWriterLockDataSlot");

            object obj = Thread.GetData(slot);
            LockFlags flag = LockFlags.None;
            if (obj != null)
                flag = (LockFlags)Thread.GetData(slot);
            if (flag == LockFlags.None)
            {
                Thread.SetData(slot, LockFlags.Writer);
            }
            else
            {
                Thread.SetData(slot, (LockFlags)((int)flag | (int)LockFlags.Writer));
            }

            //如果有活動線程,等待指定的時間 

            if (!bNoActive)
                this.m_aeWriters.WaitOne(millisecondsTimeout, true);
        }       

它首先進入臨界區判斷目前活動線程的數目,如果目前有活動線程存在,不管是寫線程還是讀線程(m_nActive),線程将等待指定的時間并且等待的寫入線程數目加1,否則線程擁有寫的權限。

釋放閱讀鎖的函數原型為:public void ReleaseReaderLock()。函數定義如下:

public void ReleaseReaderLock()
        {
            System.LocalDataStoreSlot slot = Thread.GetNamedDataSlot(m_strThreadSlotName);
            LockFlags flag = (LockFlags)Thread.GetData(slot);
            if (flag == LockFlags.None)
            {
                return;
            }

            bool bReader = true; switch (flag)
            {
                case LockFlags.None:
                    break;
                case LockFlags.Writer:
                    bReader = false;
                    break;
            }

            if (!bReader)
                return;
            Thread.SetData(slot, LockFlags.None);
            m_mutex.WaitOne();
            AutoResetEvent autoresetevent = null;
            this.m_nActive--;
            if (this.m_nActive == 0)
            {
                if (this.m_nWaitingReaders > 0)
                {
                    m_nActive++;
                    m_nWaitingReaders--;
                    autoresetevent = this.m_aeReaders;
                }
                else if (this.m_nWaitingWriters > 0)
                {
                    m_nWaitingWriters--;
                    m_nActive--;
                    autoresetevent = this.m_aeWriters;
                }
            }

            m_mutex.ReleaseMutex();
            if (autoresetevent != null)
                autoresetevent.Set();
        }       

釋放閱讀鎖時,首先判斷目前線程是否擁有閱讀鎖(通過線程局部存儲的标志),然後判斷是否有等待的閱讀線程,如果有,先将目前活動線程加1,等待閱讀線程數目減1,然後置事件為有信号。如果沒有等待的閱讀線程,判斷是否有等待的寫入線程,如果有則活動線程數目減1,等待的寫入線程數目減1。釋放寫入鎖與釋放閱讀鎖的過程基本一緻,可以參看源代碼。

注意在程式中,釋放鎖時,隻會喚醒一個閱讀程式,這是因為使用AutoResetEvent的原曆,讀者可自行将其改成ManualResetEvent,同時喚醒多個閱讀程式,此時應令m_nActive等于整個等待的閱讀線程數目。

測試

測試程式取自.Net FrameSDK中的一個例子,隻是稍做修改。測試程式如下:

using System;
using System.Threading;
using MyThreading;
class Resource
{
    myReaderWriterLock rwl = new myReaderWriterLock();
    public void Read(Int32 threadNum)
    {
        rwl.AcquireReaderLock(Timeout.Infinite);

        try
        {
            Console.WriteLine("Start Resource reading (Thread={0})", threadNum);

            Thread.Sleep(250);

            Console.WriteLine("Stop Resource reading (Thread={0})", threadNum);
        }

        finally
        {
            rwl.ReleaseReaderLock();

        }
    }

    public void Write(Int32 threadNum)
    {
        rwl.AcquireWriterLock(Timeout.Infinite);
        try
        {
            Console.WriteLine("Start Resource writing (Thread={0})", threadNum);
            Thread.Sleep(750);
            Console.WriteLine("Stop Resource writing (Thread={0})", threadNum);
        }

        finally
        {
            rwl.ReleaseWriterLock();
        }
    }
}
class App
{

    static Int32 numAsyncOps = 20;
    static AutoResetEvent asyncOpsAreDone = new AutoResetEvent(false);
    static Resource res = new Resource();
    public static void Main()
    {
        for (Int32 threadNum = 0; threadNum < 20; threadNum++)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(UpdateResource), threadNum);
        }

        asyncOpsAreDone.WaitOne();
        Console.WriteLine("All operations have completed.");
        Console.ReadLine();
    }

    // The callback method's signature MUST match that of a System.Threading.TimerCallback    
    // delegate (it takes an Object parameter and returns void)   
    static void UpdateResource(Object state)
    {
        Int32 threadNum = (Int32)state;
        if ((threadNum % 2) != 0) res.Read(threadNum);
        else res.Write(threadNum);
        if (Interlocked.Decrement(ref numAsyncOps) == 0)
            asyncOpsAreDone.Set();
    }
}