本節說一下數據的預加載。這節的內容與SQL沒什麼關系。主要說的是在 GIX4項目 中,我們是如何設計符合需求的預加載類庫的。內 容如下:
1. 什麼是預加載,為什麼要用它?
2. 我們所需要的API
3. 一個簡單的例子
什麼是預加載?
預加載其實就是在真正開始使用數據之前,先異步把數據加載好,等到需要使用時,就可以直接使用之前加載好的數據。這時,由於數 據已經加載完成,而不用等待漫長的加載過程,所以程序的速度得到一個明顯的提升。
那麼,什麼時候需要使用它呢?我覺得,主要是這種情況:當我們可以預知程序接下來的步驟中,很可能會用到一些數據,而獲取這些 數據的操作比較耗時的時候,我們就可以使用預加載的方式,提前把數據准備好。
預加載需要使用異步方法,也就是使用後台線程來加載數據。這樣做的好處當然是不會阻塞當前的主線程。(不過如果當前線程本身就 是用於異步加載數據的話,那就沒必要再新開線程了。)
我們可以使用很多種方式來實現異步加載:在.NET Framework的類庫中,很多地方都提供了異步編程模式(Asynchronous Programming Design Patterns)的API,使用這個模式,可以方便地實現各種異步加載。當然,我們也可以使用2.0提供的 ThreadPool.QueueUserWorkItem來實現一些輕量級的異步操作。在.NET4.0最新的API中,提供了Task類來表示可執行任務。
但是,這些並不是我想要的API……
我們所需要的API
目前系統中預加載使用的場景需求是這樣的:
1. 預加載可以對指定的數據獲取操作(loading action)進行封裝,在需要時調用。
2. 使用數據的模塊(使用者),並不一定知道是誰、在何時給它提前加載的數據。它只會申請使用數據。
3. 發起異步加載的模塊(發起者),應該知道使用者是誰。
4. 多個發起者之間沒有關系,但是都可以為某一使用者發起預加載。但是保證真正的數據加載操作,只會發生一次。
5. 支持重新加載。
6. 一個類中,支持對它不同的數據進行不同加載方式,以方便按需加載。
7. 從使用者的角度來看,不管有沒有發起者為它進行預加載,它都可以申請並拿到想要的數據。也就是說:
當沒有發起者為它進行預加載,那麼它的數據申請會導致即時的數據加載;
如果已經發起了預加載,而且數據已經加載完成,則直接獲取到加載好的數據;
如果數據沒有完成,則數據使用者需要等待數據的加載完成後,才可以獲取到數據並繼續當前的操作。
其中,最重要的就是最後點。
可以看到,這裡需要用到異步操作、線程間同步。所以我們需要基於上面提到的多種API來實現,這裡我們使用的是簡單的線程池的方 式,比較簡單,不再贅述。
最後設計出的API大致是這樣的:
namespace OpenExpressApp
{
public enum LoaderStatus
{
NotStarted = 0,
Running = 1,
Completed = 2,
Failed = 3,
}
public class ForeAsyncLoader
{
public ForeAsyncLoader(Action loadAction);
public LoaderStatus Status { get; }
public event EventHandler ActionSucceeded;
/// <summary>
/// 申請啟用線程進行預加載。
/// 注意:
/// 本方法可以重入,多次調用也只會執行一次ladAction
/// </summary>
public void BeginLoading();
/// <summary>
/// 重設加載器。
/// 使用此方法後,再次申請預加載時,會再次執行loadAction。
/// </summary>
public void Reset();
/// <summary>
/// 等待數據加載完成。
/// </summary>
public void WaitForLoading();
}
}
例子
客戶程序使用時,需要為其定義一個屬性,舉例如下:
數據持有者:
public class DataHolder
{
private object _data;
public object Data
{
get
{
return this._data;
}
}
private object GetDataFromWeb()
{
//...
}
}
如果它的data1數據加載比較慢,我們可以為其定義一個預加載屬性:
private ForeAsyncLoader _dataLoader;
/// <summary>
/// 數據加載器
/// </summary>
public ForeAsyncLoader DataLoader
{
get
{
if (this._dataLoader == null)
{
this._dataLoader = new ForeAsyncLoader(() =>
{
//真正加載數據
this._data = this.GetDataFromWeb();
});
}
return this._dataLoader;
}
}
這樣,數據的“消費者”就可以使用這個數據:
public class DataConsumer
{
private void Process(DataHolder holder)
{
holder.DataLoader.WaitForLoading();
var data = holder.Data;
//consume data...
}
}
在這裡,雖然使用者並不知道有沒有其它代碼給holder執行了數據的預加載,但是當WaitForLoading方法執行完成後,數據是必然獲取 到本地了。所以就可以直接使用數據。我們甚至可以把這句代碼放在Data屬性的get代碼塊中,這樣,使用者甚至都不知道數據的獲取方案 !
然後,可以在運行於它之前的代碼中,為這個“DataHolder”申請預加載。例如,我們在應用程序啟動的時候,就開始預加載。下面的 方法調用了BeginLoading方法,此方法會使用後台線程加載數據,所以這裡會立即返回:
public class Invoker
{
private DataHolder _holder = new DataHolder();
private void App_Start()
{
this._holder.DataLoader.BeginLoading();
//do other things
}
}
至此,就完成了一個最簡單的預加載。
過程如下:
(圖畫得不熟,哪畫錯了,望大家指正,謝謝。)
小結 本篇主要說了一下在目前的系統中,如何設計出一個滿足場景應用需求的預加載API。
預加載是一個經常會被使用到的模式,希望對大家有用。
下一篇我會寫一下與目前系統關聯比較大的內容:與GIX4對象模型相關的“預加載、延遲加載、聚合SQL的組合應用”;另外可能會順 便說一下,如何讓CSLA的服務端框架支持多線程並發。