天天看點

[Abp 源碼分析]十六、背景作業與背景工作者

0. 簡介

在某些時候我們可能會需要執行背景任務,或者是執行一些周期性的任務。比如說可能每隔 1 個小時要清除某個臨時檔案夾内的資料,可能使用者會要針對某一個使用者群來群發一組短信。前面這些就是典型的應用場景,在 Abp 架構裡面為我們準備了背景作業和背景工作者來幫助我們解決這個問題。

背景作業與背景工作者的差別是,前者主要用于某些耗時較長的任務,而不想阻塞使用者的時候所使用。後者主要用于周期性的執行某些任務,從 “工作者” 的名字可以看出來,就是一個個勞工,而且他們每個勞工都擁有單獨的背景線程。

0.1 典型場景

背景作業

  • 某個使用者按下了報表按鈕來生成一個需要長時間等待的報表。你添加這個工作到隊列中,當報表生成完畢後,發送報表結果到該使用者的郵箱。
  • 在背景作業中發送一封郵件,有些問題可能會導緻發送失敗(網絡連接配接異常,或者主機當機);由于有背景作業以及持久化機制,在問題排除後,可以重試以保證任務的成功執行。

背景工作者

  • 背景工作者能夠周期性地執行舊日志的删除。
  • 背景工作者可以周期性地篩選出非活躍性使用者,并且發送回歸郵件給這些使用者。

1. 啟動流程

背景作業與背景工作者都是通過各自的 Manager(

IBackgroundJobManager

/

IBackgroundWorkerManager

) 來進行管理的。而這兩個 Manager 分别繼承了

ISingletonDependency

接口,是以在啟動的時候就會自動注入這兩個管理器以便開發人員管理操作。

這裡值得注意的一點是,

IBackgroundJobManager

接口是

IBackgroundWorker

的派生接口,而

IBackgroudWorker

是歸屬于

IBackgroundWorkerManager

進行管理的。

是以,你可以在

AbpKernelModule

裡面看到如下代碼:

public sealed class AbpKernelModule : AbpModule
{
	public override void PostInitialize()
	{
		// 注冊可能缺少的元件
		RegisterMissingComponents();
		
		// ... 忽略的代碼
		// 各種管理器的初始化操作

		// 從配置項中讀取,是否啟用了背景作業功能
		if (Configuration.BackgroundJobs.IsJobExecutionEnabled)
		{
			var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();
			// 開始啟動背景工作者
			workerManager.Start();
			// 增加背景作業管理器
			workerManager.Add(IocManager.Resolve<IBackgroundJobManager>());
		}
	}
}
           

可以看到,背景作業管理器是作為一個背景工作者被添加到了

IBackgroundWorkerManager

當中來執行的。

2. 代碼分析

2.1 背景工作者

2.1.1 背景工作者管理器

Abp 通過背景工作者管理器來管理背景作業隊列,是以我們首先來看一下背景工作者管理器接口的定義是什麼樣子的。

public interface IBackgroundWorkerManager : IRunnable
{
	void Add(IBackgroundWorker worker);
}
           

還是相當簡潔的,就一個

Add

方法用來添加一個新的背景工作者對象。隻是在這個地方,可以看到該接口又是內建自

IRunnable

接口,那麼該接口的作用又是什麼呢?

轉到其定義可以看到,

IRunable

接口定義了三個基本的方法:

Start()

Stop()

WaitStop()

,而且他擁有一個預設實作

RunableBase

,其實就是用來辨別一個任務的運作狀态。

public interface IRunnable
{
    // 開始執行任務
	void Start();

    // 停止執行任務
	void Stop();

    // 阻塞線程,等待任務執行完成後辨別為停止。
	void WaitToStop();
}

public abstract class RunnableBase : IRunnable
{
	// 用于辨別任務是否運作的布爾值變量
	public bool IsRunning { get { return _isRunning; } }

	private volatile bool _isRunning;

	// 啟動之後表示任務正在運作
	public virtual void Start()
	{
		_isRunning = true;
	}

	// 停止之後表示任務結束運作
	public virtual void Stop()
	{
		_isRunning = false;
	}

	public virtual void WaitToStop()
	{

	}
}
           

到目前為止整個代碼都還是比較簡單清晰的,我們接着看

IBackgroundWorkerManager

的預設實作

BackgroundWorkerManager

類,首先我們看一下該類擁有哪些屬性與字段。

public class BackgroundWorkerManager : RunnableBase, IBackgroundWorkerManager, ISingletonDependency, IDisposable
{
	private readonly IIocResolver _iocResolver;
	private readonly List<IBackgroundWorker> _backgroundJobs;
	
	public BackgroundWorkerManager(IIocResolver iocResolver)
	{
		_iocResolver = iocResolver;
		_backgroundJobs = new List<IBackgroundWorker>();
	}
}
           

在背景工作者管理器類的内部,預設有一個 List 集合,用于維護所有的背景工作者對象。那麼其他的

Start()

等方法肯定是基于這個集合進行操作的。

public override void Start()
{
    base.Start();

    _backgroundJobs.ForEach(job => job.Start());
}

public override void Stop()
{
    _backgroundJobs.ForEach(job => job.Stop());

    base.Stop();
}

public override void WaitToStop()
{
    _backgroundJobs.ForEach(job => job.WaitToStop());

    base.WaitToStop();
}
           

可以看到實作還是比較簡單的,接下來我們繼續看他的

Add()

方法是如何進行操作的?

public void Add(IBackgroundWorker worker)
{
    _backgroundJobs.Add(worker);

    if (IsRunning)
    {
        worker.Start();
    }
}
           

在這裡我們看到他會針對

IsRunning

進行判定是否立即啟動加入的背景工作者對象。而這個

IsRunning

屬性值唯一産生變化的情況就在于

Start()

方法與

Stop()

方法的調用。

最後肯定也有相關的銷毀方法,用于釋放所有注入的背景工作者對象,并将集合清除。

private bool _isDisposed;

public void Dispose()
{
    if (_isDisposed)
    {
        return;
    }

    _isDisposed = true;

    // 周遊集合,通過 Ioc 解析器的 Release 方法釋放對象
    _backgroundJobs.ForEach(_iocResolver.Release);
    // 清空集合
    _backgroundJobs.Clear();
}
           

是以,針對于所有背景工作者的管理,都是通過

IBackgroundWorkerManager

來進行操作的。

2.1.2 背景工作者

看完了管理器,我們來看一下

IBackgroundWorker

背景工作者對象是怎樣的構成。

public interface IBackgroundWorker : IRunnable
{

}
           

貌似隻是一個空的接口,其作用主要是辨別某個類型是否為背景工作者,轉到其抽象類實作

BackgroundWorkerBase

,裡面隻是注入了一些輔助對象與本地化的一些方法。

public abstract class BackgroundWorkerBase : RunnableBase, IBackgroundWorker
{
	// 配置管理器
	public ISettingManager SettingManager { protected get; set; }

	// 工作單元管理器
	public IUnitOfWorkManager UnitOfWorkManager
	{
		get
		{
			if (_unitOfWorkManager == null)
			{
				throw new AbpException("Must set UnitOfWorkManager before use it.");
			}

			return _unitOfWorkManager;
		}
		set { _unitOfWorkManager = value; }
	}
	private IUnitOfWorkManager _unitOfWorkManager;

	// 獲得目前的工作單元
	protected IActiveUnitOfWork CurrentUnitOfWork { get { return UnitOfWorkManager.Current; } }

	// 本地化資料總管
	public ILocalizationManager LocalizationManager { protected get; set; }

	// 預設的本地化資源的源名稱
	protected string LocalizationSourceName { get; set; }

	protected ILocalizationSource LocalizationSource
	{
		get
		{
			// 如果沒有配置源名稱,直接抛出異常
			if (LocalizationSourceName == null)
			{
				throw new AbpException("Must set LocalizationSourceName before, in order to get LocalizationSource");
			}

			if (_localizationSource == null || _localizationSource.Name != LocalizationSourceName)
			{
				_localizationSource = LocalizationManager.GetSource(LocalizationSourceName);
			}

			return _localizationSource;
		}
	}
	private ILocalizationSource _localizationSource;

	// 日志記錄器
	public ILogger Logger { protected get; set; }

	protected BackgroundWorkerBase()
	{
		Logger = NullLogger.Instance;
		LocalizationManager = NullLocalizationManager.Instance;
	}
	
	// ... 其他模闆代碼
}
           

我們接着看繼承并實作了

BackgroundWorkerBase

的類型

PeriodicBackgroundWorkerBase

,從字面意思上來看,該類型應該是一個定時背景工作者基類。

重點在于

Periodic

(定時),從其類型内部的定義可以看到,該類型使用了一個

AbpTimer

對象來進行周期計時與具體工作任務的觸發。我們暫時先不看這個

AbpTimer

,僅僅看

PeriodicBackgroundWorkerBase

的内部實作。

public abstract class PeriodicBackgroundWorkerBase : BackgroundWorkerBase
{
	protected readonly AbpTimer Timer;

	// 注入 AbpTimer
	protected PeriodicBackgroundWorkerBase(AbpTimer timer)
	{
		Timer = timer;
		// 綁定周期執行的任務,這裡是 DoWork()
		Timer.Elapsed += Timer_Elapsed;
	}

	public override void Start()
	{
		base.Start();
		Timer.Start();
	}

	public override void Stop()
	{
		Timer.Stop();
		base.Stop();
	}

	public override void WaitToStop()
	{
		Timer.WaitToStop();
		base.WaitToStop();
	}

	private void Timer_Elapsed(object sender, System.EventArgs e)
	{
		try
		{
			DoWork();
		}
		catch (Exception ex)
		{
			Logger.Warn(ex.ToString(), ex);
		}
	}

	protected abstract void DoWork();
}
           

可以看到,這裡基類綁定了

DoWork()

作為其定時執行的方法,那麼使用者在使用的時候直接繼承自該基類,然後重寫

DoWork()

方法即可綁定自己的背景工作者的任務。

2.1.3 AbpTimer 定時器

在上面的基類我們看到,基類的

Start()

Stop()

WaitTpStop()

方法都是調用的

AbpTimer

所提供的,是以說

AbpTimer

其實也繼承了

RunableBase

基類并實作其具體的啟動與停止操作。

其實

AbpTimer

的核心就是通過 CLR 的

Timer

來實作周期性任務執行的,不過預設的

Timer

類有兩個比較大的問題。

  1. CLR 的

    Timer

    并不會等待你的任務執行完再執行下一個周期的任務,如果你的某個任務耗時過長,超過了

    Timer

    定義的周期。那麼

    Timer

    會開啟一個新的線程執行,這樣的話最後我們系統的資源會因為線程大量重複建立而被拖垮。
  2. 如何知道一個

    Timer

    所執行的業務方法已經真正地被結束了。

是以 Abp 才會重新封裝一個

AbpTimer

作為一個基礎的計時器。第一個問題的解決方法很簡單,就是在執行具體綁定的業務方法之前,通過

Timer.Change()

方法來讓

Timer

臨時失效。等待業務方法執行完成之後,再将

Timer

的周期置為使用者設定的周期。

// CLR Timer 綁定的回調方法
private void TimerCallBack(object state)
{
	lock (_taskTimer)
	{
		if (!_running || _performingTasks)
		{
			return;
		}
		
        // 暫時讓 Timer 失效
		_taskTimer.Change(Timeout.Infinite, Timeout.Infinite);
        // 設定執行辨別為 TRUE,表示目前的 AbpTimer 正在執行
		_performingTasks = true;
	}

	try
	{
        // 如果綁定了相應的觸發事件
		if (Elapsed != null)
		{
            // 執行相應的業務方法,這裡就是最開始綁定的 DoWork() 方法
			Elapsed(this, new EventArgs());
		}
	}
	catch
	{

	}
	finally
	{
		lock (_taskTimer)
		{
            // 辨別業務方法執行完成
			_performingTasks = false;
			if (_running)
			{
                // 更改周期為使用者指定的執行周期,等待下一次觸發
				_taskTimer.Change(Period, Timeout.Infinite);
			}

			Monitor.Pulse(_taskTimer);
		}
	}
}
           

針對于第二個問題,Abp 通過

WaitToStop()

方法會阻塞調用這個

Timer

的線程,并且在

_performingTasks

辨別位是

false

的時候釋放。

public override void WaitToStop()
{
    // 鎖定 CLR 的 Timer 對象
    lock (_taskTimer)
    {
        // 循環檢測
        while (_performingTasks)
        {
            Monitor.Wait(_taskTimer);
        }
    }

    base.WaitToStop();
}
           

至于其他的

Start()

方法就是使用 CLR 的

Timer

更改其執行周期,而

Stop()

就是直接将

Timer

的周期設定為無限大,使計時器失效。

2.1.4 總結

Abp 背景工作者的核心就是通過

AbpTimer

來實作周期性任務的執行,使用者隻需要繼承自

PeriodicBackgroundWorkerBase

,然後将其添加到

IBackgroundWorkerManager

的集合當中。這樣 Abp 在啟動之後就會周遊這個工作者集合,然後周期執行這些背景工作者綁定的方法。

當然如果你繼承了

PeriodicBackgroundWorkerBase

之後,可以通過設定構造函數的

AbpTimer

來指定自己的執行周期。

2.2 背景作業隊列

背景工作隊列的管理是通過

IBackgroundJobManager

來處理的,而該接口又繼承自

IBackgroundWorker

,是以一整個背景作業隊列就是一個背景工作者,隻不過這個工作者有點特殊。

2.2.1 背景作業管理器

IBackgroundJobManager

接口的定義其實就兩個方法,一個

EnqueueAsync<TJob, TArgs>()

用于将一個背景作業加入到執行隊列當中。而

DeleteAsync()

方法呢,顧名思義就是從隊列當中移除指定的背景作業。

首先看一下其預設實作

BackgroundJobManager

,該實作同樣是繼承自

PeriodicBackgroundWorkerBase

并且其預設周期為 5000 ms。

public class BackgroundJobManager : PeriodicBackgroundWorkerBase, IBackgroundJobManager, ISingletonDependency
{
		// 事件總線
		public IEventBus EventBus { get; set; }
        
		// 輪訓背景作業的間隔,預設值為 5000 毫秒.
        public static int JobPollPeriod { get; set; }

		// IOC 解析器
        private readonly IIocResolver _iocResolver;
		
		// 背景作業隊列存儲
        private readonly IBackgroundJobStore _store;

        static BackgroundJobManager()
        {
            JobPollPeriod = 5000;
        }

        public BackgroundJobManager(
            IIocResolver iocResolver,
            IBackgroundJobStore store,
            AbpTimer timer)
            : base(timer)
        {
            _store = store;
            _iocResolver = iocResolver;

            EventBus = NullEventBus.Instance;

            Timer.Period = JobPollPeriod;
        }
}
           

基礎結構基本上就這個樣子,接下來看一下他的兩個接口方法是如何實作的。

EnqueueAsync<TJob, TArgs>

方法通過傳入指定的背景作業對象和相應的參數,同時還有任務的優先級。将其通過

IBackgroundJobStore

進行持久化,并傳回一個任務的唯一 JobId 以便進行删除操作。

public async Task<string> EnqueueAsync<TJob, TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
	where TJob : IBackgroundJob<TArgs>
{
	// 通過 JobInfo 包裝任務的基本資訊
	var jobInfo = new BackgroundJobInfo
	{
		JobType = typeof(TJob).AssemblyQualifiedName,
		JobArgs = args.ToJsonString(),
		Priority = priority
	};

	// 如果需要延時執行的話,則用目前時間加上延時的時間作為任務下次運作的時間
	if (delay.HasValue)
	{
		jobInfo.NextTryTime = Clock.Now.Add(delay.Value);
	}

	// 通過 Store 進行持久話存儲
	await _store.InsertAsync(jobInfo);

	// 傳回背景任務的唯一辨別
	return jobInfo.Id.ToString();
}
           

至于删除操作,在 Manager 内部其實也是通過

IBackgroundJobStore

進行實際的删除操作的。

public async Task<bool> DeleteAsync(string jobId)
{
    // 判斷 jobId 的值是否有效
    if (long.TryParse(jobId, out long finalJobId) == false)
    {
        throw new ArgumentException($"The jobId '{jobId}' should be a number.", nameof(jobId));
    }

    // 使用 jobId 從 Store 處篩選到 JobInfo 對象的資訊
    BackgroundJobInfo jobInfo = await _store.GetAsync(finalJobId);
    if (jobInfo == null)
    {
        return false;
    }

    // 如果存在有 JobInfo 則使用 Store 進行删除操作
    await _store.DeleteAsync(jobInfo);
    return true;
}
           

背景作業管理器實質上是一個周期性執行的背景工作者,那麼我們的背景作業是每 5000 ms 執行一次,那麼他的

DoWork()

方法又在執行什麼操作呢?

protected override void DoWork()
{
    // 從 Store 當中獲得等待執行的背景作業集合
    var waitingJobs = AsyncHelper.RunSync(() => _store.GetWaitingJobsAsync(1000));

    // 周遊這些等待執行的背景任務,然後通過 TryProcessJob 進行執行
    foreach (var job in waitingJobs)
    {
        TryProcessJob(job);
    }
}
           

可以看到每 5 秒鐘我們的背景作業管理器就會從

IBackgroundJobStore

當中拿到最大 1000 條的背景作業資訊,然後周遊這些資訊。通過

TryProcessJob(job)

方法來執行背景作業。

TryProcessJob()

方法,本質上就是通過反射建構出一個

IBackgroundJob

對象,然後取得序列化的參數值,通過反射得到的

MethodInfo

對象來執行我們的背景任務。執行完成之後,就會從 Store 當中移除掉執行完成的任務。

針對于在執行過程當中所出現的異常,會通過

IEventBus

觸發一個

AbpHandledExceptionData

事件記錄背景作業執行失敗時的異常資訊。并且一旦在執行過程當中出現了任何異常的情況,都會将該任務的

IsAbandoned

字段置為

true

,當該字段為

true

時,該任務将不再回被執行。

PS:就是在

GetWaitingJobsAsync()

方法時,會過濾掉 IsAbandoned 值為

true

的任務。
private void TryProcessJob(BackgroundJobInfo jobInfo)
{
    try
    {
        // 任務執行次數自增 1
        jobInfo.TryCount++;
        // 最後一次執行時間設定為目前時間
        jobInfo.LastTryTime = Clock.Now;

        // 通過反射取得背景作業的類型
        var jobType = Type.GetType(jobInfo.JobType);
        // 通過 Ioc 解析器得到一個臨時的背景作業對象,執行完之後既被釋放
        using (var job = _iocResolver.ResolveAsDisposable(jobType))
        {
            try
            {
                // 通過反射得到背景作業的 Execute 方法
                var jobExecuteMethod = job.Object.GetType().GetTypeInfo().GetMethod("Execute");
                var argsType = jobExecuteMethod.GetParameters()[0].ParameterType;
                var argsObj = JsonConvert.DeserializeObject(jobInfo.JobArgs, argsType);

                // 結合持久話存儲的參數資訊,調用 Execute 方法進行背景作業
                jobExecuteMethod.Invoke(job.Object, new[] { argsObj });

                // 執行完成之後從 Store 删除該任務的資訊
                AsyncHelper.RunSync(() => _store.DeleteAsync(jobInfo));
            }
            catch (Exception ex)
            {
                Logger.Warn(ex.Message, ex);

                // 計算下一次執行的時間,一旦超過 2 天該任務都執行失敗,則傳回 null
                var nextTryTime = jobInfo.CalculateNextTryTime();
                if (nextTryTime.HasValue)
                {
                    jobInfo.NextTryTime = nextTryTime.Value;
                }
                else
                {
                    // 如果為 null 則說明該任務在 2 天的時間内都沒有執行成功,則放棄繼續執行
                    jobInfo.IsAbandoned = true;
                }

                // 更新 Store 存儲的任務資訊
                TryUpdate(jobInfo);

                // 觸發異常事件
                EventBus.Trigger(
                    this,
                    new AbpHandledExceptionData(
                        new BackgroundJobException(
                            "A background job execution is failed. See inner exception for details. See BackgroundJob property to get information on the background job.", 
                            ex
                        )
                        {
                            BackgroundJob = jobInfo,
                            JobObject = job.Object
                        }
                    )
                );
            }
        }
    }
    catch (Exception ex)
    {
        Logger.Warn(ex.ToString(), ex);
		// 表示任務不再執行
        jobInfo.IsAbandoned = true;
		// 更新 Store
        TryUpdate(jobInfo);
    }
}
           

2.2.2 背景作業

背景作業的預設接口定義為

IBackgroundJob<in TArgs>

,他隻有一個

Execute(TArgs args)

方法,用于接收指定類型的作業參數,并執行。

一般來說我們不建議直接通過繼承

IBackgroundJob<in TArgs>

來實作背景作業,而是繼承自

BackgroundJob<TArgs>

抽象類。該抽象類内部也沒有什麼特别的實作,主要是注入了一些基礎設施,比如說 UOW 與 本地化資料總管,友善我們開發使用。

背景作業本身是具體執行的對象,而

BackgroundJobInfo

則是存儲了背景作業的 Type 類型和參數,友善在需要執行的時候通過反射的方式執行背景作業。

2.2.2 背景作業隊列存儲

IBackgroundJobStore

我們就可以猜到以 Abp 架構的套路,他肯定會有兩種實作,第一種就是基于記憶體的

InMemoryBackgroundJobStore

。而第二種呢,就是由 Abp.Zero 子產品所提供的基于資料庫的

BackgroundJobStore

IBackgroundJobStore

接口所定義的方法基本上就是增删改查,沒有什麼複雜的。

public interface IBackgroundJobStore
{
    // 通過 JobId 擷取背景任務資訊
    Task<BackgroundJobInfo> GetAsync(long jobId);

    // 插入一個新的背景任務資訊
    Task InsertAsync(BackgroundJobInfo jobInfo);

    /// <summary>
    /// Gets waiting jobs. It should get jobs based on these:
    /// Conditions: !IsAbandoned And NextTryTime &lt;= Clock.Now.
    /// Order by: Priority DESC, TryCount ASC, NextTryTime ASC.
    /// Maximum result: <paramref name="maxResultCount"/>.
    /// </summary>
    /// <param name="maxResultCount">Maximum result count.</param>
    Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(int maxResultCount);

    /// <summary>
    /// Deletes a job.
    /// </summary>
    /// <param name="jobInfo">Job information.</param>
    Task DeleteAsync(BackgroundJobInfo jobInfo);

    /// <summary>
    /// Updates a job.
    /// </summary>
    /// <param name="jobInfo">Job information.</param>
    Task UpdateAsync(BackgroundJobInfo jobInfo);
}
           

這裡先從簡單的記憶體 Store 說起,這個

InMemoryBackgroundJobStore

内部使用了一個并行字典來存儲這些任務資訊。

public class InMemoryBackgroundJobStore : IBackgroundJobStore
{
	private readonly ConcurrentDictionary<long, BackgroundJobInfo> _jobs;
	private long _lastId;
	
	public InMemoryBackgroundJobStore()
	{
		_jobs = new ConcurrentDictionary<long, BackgroundJobInfo>();
	}
}
           

相當簡單,這幾個接口方法基本上就是針對與這個并行字典操作的一層封裝。

public Task<BackgroundJobInfo> GetAsync(long jobId)
{
	return Task.FromResult(_jobs[jobId]);
}

public Task InsertAsync(BackgroundJobInfo jobInfo)
{
	jobInfo.Id = Interlocked.Increment(ref _lastId);
	_jobs[jobInfo.Id] = jobInfo;

	return Task.FromResult(0);
}

public Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(int maxResultCount)
{
	var waitingJobs = _jobs.Values
        // 首先篩選出不再執行的背景任務
		.Where(t => !t.IsAbandoned && t.NextTryTime <= Clock.Now)
        // 第一次根據背景作業的優先級進行排序,高優先級優先執行
		.OrderByDescending(t => t.Priority)
        // 再根據執行次數排序,執行次數越少的,越靠前
		.ThenBy(t => t.TryCount)
		.ThenBy(t => t.NextTryTime)
		.Take(maxResultCount)
		.ToList();

	return Task.FromResult(waitingJobs);
}

public Task DeleteAsync(BackgroundJobInfo jobInfo)
{
	_jobs.TryRemove(jobInfo.Id, out _);

	return Task.FromResult(0);
}

public Task UpdateAsync(BackgroundJobInfo jobInfo)
{
    // 如果是不再執行的任務,删除
	if (jobInfo.IsAbandoned)
	{
		return DeleteAsync(jobInfo);
	}

	return Task.FromResult(0);
}
           

至于持久化到資料庫,無非是注入一個倉儲,然後針對這個倉儲進行增删查改的操作罷了,這裡就不在贅述。

2.2.3 背景作業優先級

背景作業的優先級定義在

BackgroundJobPriority

枚舉當中,一共有 5 個等級,分别是

Low

BelowNormal

Normal

AboveNormal

High

,他們從最低到最高排列。

3.點此跳轉到總目錄