CODE-非同步方法之同步化及逾時機制
4 |
今天遇到的小需求: 有個元件函數以非同步方式執行,透過旗標變數表示執行狀態,我想在Console Application中以同步方式呼叫該函數,等待其執行完畢程式就結束,但要有逾時中止的功能。
非同步函數的例子如以下範例中的AsyncJobClass.DoAsyncWork(),它會以另開執行緒方式執行,在5秒後將Ready旗標設為true。透用這種方式,我模擬了一個非同步執行的作業。若非同步過程發生錯誤時,AsyncJobClass會呼叫自訂OnError事件執行特定邏輯。
我寫了一個AsyncToSyncHelper實現非同步作業的同步化,主要透過方法CheckStatusWithTimeout(Func<bool>, interval, period, jobDesc),可以在逾時期間(period)內每隔一段時間(interval)檢查Func<bool>一次,直到其傳回true為止。但若等待傳回true的過程超出逾時上限,或非同步作業發生錯誤時,則會抛出例外。跨越不同執行緒的例外處理要額外考量,因此這裡的做法是由非同步作業提供出錯時的自訂事件,在錯誤事件中便可呼叫RaiseCheckStatusException通知CheckStatusWithTimeout停止檢查。
程式碼如下,歡迎參考指教:
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
AsyncJobClass.Ready = false;
AsyncJobClass.DoAsyncJob(false);
//Test 1, Timoeout = 8s
AsyncToSyncHelper.CheckStatusWithTimeout(
() => { return AsyncJobClass.Ready; },
100, 8000, "Test 1");
Console.WriteLine("Test 1, shorter than timeout, OK!");
try
{
AsyncJobClass.Ready = false;
AsyncJobClass.DoAsyncJob(false);
//Test 2, Timeout = 3s
AsyncToSyncHelper.CheckStatusWithTimeout(
() => { return AsyncJobClass.Ready; },
100, 3000, "Test 2");
}
catch (Exception e)
{
Console.WriteLine("Error=" + e.Message);
Console.WriteLine("Test 2, longer than timeout, OK!");
}
try
{
AsyncJobClass.Ready = false;
AsyncJobClass.DoAsyncJob(true);
//when error, use RaiseCheckStatusException() to stop
AsyncJobClass.OnError = (e) =>
{
AsyncToSyncHelper.RaiseCheckStatusException(e);
};
//Test 3, throw the exception in purpose
AsyncToSyncHelper.CheckStatusWithTimeout(
() => { return AsyncJobClass.Ready; },
100, 8000, "Test 3");
}
catch (Exception e)
{
Console.WriteLine("Error=" + e.Message);
Console.WriteLine("Test 3, stopped when exception, OK!");
}
Console.Read();
}
}
/// <summary>
/// An helper class to sync with async job
/// </summary>
public class AsyncToSyncHelper
{
public static Exception CheckStatusException = null;
/// <summary>
/// Check the status function until it return true or timeout
/// </summary>
/// <param name="check">return true when ready</param>
/// <param name="interval">interval of checking in milliseconds</param>
/// <param name="period">timeout period in milliseconds</param>
/// <param name="jobDesc">job description for timeout exception message</param>
public static void CheckStatusWithTimeout(Func<bool> check, int interval,
long period, string jobDesc)
{
DateTime dtTimeout = DateTime.Now.AddMilliseconds(period);
CheckStatusException = null;
while (DateTime.Now.CompareTo(dtTimeout) < 0 && !check()
&& CheckStatusException == null)
Thread.Sleep(interval);
if (CheckStatusException != null)
throw new ApplicationException("CheckStatusWithTimeout Exception: " +
CheckStatusException.Message);
if (DateTime.Now.CompareTo(dtTimeout) >= 0)
throw new ApplicationException(
string.Format(
"CheckStatusWithTimeout Exception: Timeout after {0:N0}ms @ {1}",
period, jobDesc));
}
private static object _lock = new object();
/// <summary>
/// Raise an exception to stop CheckStatusWithTimeout
/// </summary>
/// <param name="e"></param>
public static void RaiseCheckStatusException(Exception e)
{
lock (_lock)
{
CheckStatusException = e;
}
}
}
class AsyncJobClass
{
public static bool Ready = false;
//Simulating async work
//delay 5 sec, than set Ready = true, or throw an exception in purpose
public static void DoAsyncJob(bool raiseException)
{
ThreadPool.QueueUserWorkItem((o) => {
if (raiseException)
OnError(new ApplicationException("Exception in Purpose!"));
else
{
Thread.Sleep(5000);
Ready = true;
}
});
}
//The event triggered when exception raised
public static Action<Exception> OnError;
}
【補充參考】
- () => { ... }寫法可參考C# 3.0 極簡風- Lambda Expression
- ThreadPool.QueueUserWorkItem((o) => { ... });寫法可參考匿名方法 vs 具名方法 的小小效能實驗
- Func<T>與Action<T>
Comments
# by chicken
提供個好物... .NET FX 裡就有替所有的 delegate 定義非同步呼叫的方法,會用 IAsyncResult 的介面封裝呼叫的結果,其中就提供了 bool IsCompleted {get;} 讓你檢查跑完了沒,也提供 WaitHandle 物件讓你等待它執行完畢 (可以設 timeout, 跟 thread sync 用的是一樣的機制)。 這樣至少是 wait / notify 的模式,不是 pooling 的模式,CPU 會閒很多,同時偵測的精確度也高的多。
# by chicken
順手補段 sample code, 時間故意都用非整數,主要是看這作法精確度: class Program { private delegate void AsyncMethod(); static void Main(string[] args) { AsyncMethod x = DoAsyncJob; Stopwatch timer = new Stopwatch(); timer.Start(); IAsyncResult result = x.BeginInvoke(null, null); while (result.AsyncWaitHandle.WaitOne(543) == false) { Console.Write('#'); //Console.WriteLine(DateTime.Now); } Console.WriteLine("\n{0}", timer.ElapsedMilliseconds); } private static void DoAsyncJob() { Thread.Sleep(TimeSpan.FromSeconds(12.345)); } }
# by Jeffrey
to chicken, 有個小問題,元件是別人提供的,固定只能透過屬性取得處理狀態,在不修改元件的前題下,是否就無法套用IAsyncResult機制,只能走Polling這條路?
# by chicken
要避開 pooling , 只有兩種方法... 一個是找的到同步呼叫的版本 (就是呼叫後會等到它跑完才RETURN),另一個是要有辦法實作 CALLBACK 的機制 (變數有辦法設 trigger, 或是他們自己提供你指定 callback 等等..) 才有辦法... 否則,就只好自己 pooling 吧 @@ 不過自己實作 Async 真的是件苦差事,要是我的話,我大概會做個折衷的版本,就是先用 pooling 重新包裝個很笨的 sync 版本 method, 再用 IAsyncResult 的機制來跑...