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