今天遇到的小需求: 有個元件函數以非同步方式執行,透過旗標變數表示執行狀態,我想在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;
}

【補充參考】


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 的機制來跑...

Post a comment