.NET中的異步編程

開篇html

異步編程是程序設計的重點也是難點,還記得在剛開始接觸.net的時候,看的是一本c#的Winform實例教程,上面大部分都是教咱們如何使用Winform的控件以及操做數據庫的實例,那時候作的基本都是數據庫的demo,數據量也不大,程序在執行的時候基本上不會出現阻塞的狀況。隨着不斷的深刻.net,也開始進入的實戰,在實際的項目,數據量每每都是比較大,特別是在大量的數據入庫以及查詢數據並進行計算的時候,程序的UI界面每每卡死在那裏,發生了阻塞,這時候就須要對計算時間限制的過程進行異步處理,讓UI線程繼續相應用戶的操做,使得用戶體驗表現比較友好,同時正確的使用異步編程去處理計算限制的操做和耗時IO操做還能提高的應用程序的吞吐量及性能。因而可知,異步編程的重要性。數據庫

 
異步編程在程序設計中也是很是複雜的,稍有不慎,就會使得你的應用程序變得不穩定,出現異常,甚至會奔潰。可是,比較幸運的是,.net提供很是方便的框架來進行異步編程,在我看來.net中實現異步有兩種方式,第一種是多線程的方式,第二種是使用異步函數,其實在異步函數中使用的仍是多線程的技術。接下來就介紹在.net中如何使用多線程和異步函數來解決計算限制、耗時等這些不友好用戶體驗的問題。
 
異步編程中比較關心,也是比較重要的技術點在於,1)當異步線程在工做完成時如何通知調用線程,2)當異步線程出現異常的時候該如何處理,3)異步線程工做的進度如何實時的通知調用線程。4)如何在調用線程中取消正在工做的異步線程,並進行回滾操做。
 
1、異步函數模型
c#中提供異步函數編程模式,只要是使用委託對象封裝的函數均可以實現該函數的異步調用,這是由於委託類型有BeginInvokeEndInvoke這兩個方法來支持異步調用。
下面給出一個例子來說解如何使用委託的來實現異步調用函數。
class Program
    {
        public delegate void DoWork();
        static void Main(string[] args)
        {
            DoWork d = new DoWork(WorkPro);//no.1
 
            d.BeginInvoke(null, null);//no.2
            for (int i = 0; i < 100; i++)//no.3
            {
                Thread.Sleep(10);//主線程須要作的事
            }
            Console.WriteLine("主線程done");
            Console.ReadKey();
        }
        public static void WorkPro()
        {
            //作一些耗時的工做
            Thread.Sleep(2000);
            Console.WriteLine("異步調用結束");
        }
    }
程序定義了一個DoWork類型無參無返回值的的委託類型,no.1用WorkPro方法實例化一個DoWork類型的對象d ,no.2經過委託對象dBeginInvoke(null,null)(下面將會詳細介紹BeginInvoke函數中兩個參數如何使用)來實現WorkPro函數的異步調用,這樣就使得no.3主線程所作的for循環和WorkPro函數能夠同時執行,這樣使得程序的運行效率獲得了大幅度的提高。若是程序是同步執行的話,假設WorkPro函數執行須要2秒,for須要1秒,總共執行時間就須要3秒,若是WorkPro是異步執行的話,那麼整個程序執行完畢只須要2秒就夠了。
------
上面這個例子只是簡單演示瞭如何經過委託來實現函數的異步調用,而沒有傳遞給該異步函數任何的參數,也不須要獲取該異步函數的結果。若是主線須要傳遞給該異步函數一個參數,而且還要在該異步函數執行完畢以後獲取其執行結果,那應該如何實現呢?
 class Program
    {
        public delegate int DoWord(int count);
        static void Main(string[] args)
        {
            DoWord d = new DoWord(WorkPro);
            IAsyncResult r= d.BeginInvoke(1000,null,null);//no.1
            int result= d.EndInvoke(r);//no.2
            Console.WriteLine(result);
            for (int i = 0; i < 100; i++)//no.3
            {
                Thread.Sleep(10);//主線程須要作的事
            }
            Console.WriteLine("主線程done");
            Console.ReadKey();
        }
        public static int WorkPro(int count)
        {
            int sum = 0;
            //作一些耗時的工做
            for (int i = 0; i < count; i++)
            {
                sum += i;
            }
            return sum;        
        }
    } 
咱們已經把委託類型改成具備一個int類型的參數和int類型返回值。在這裏解釋一下,每當你的編譯器發現定義了一個委託類型,就會對應的生成一個類型,而且該類型BeginInvoke方法的參數個數也是不一樣的,本例聲明的委託類型爲:
public delegate int DoWord(int count);
實際生成的BeginInvoke原型爲:IAsyncResult   BeginInvoke(int count, AsyncCallBack callback, object @object)
在no.1處仍是和第一個例子同樣調用委託,不一樣的是用IAsyncResult接口的變量接收了異步調用(並非異步函數)的返回狀態,這是方便後面調用EndInvoke方法接受這個異步函數調用結果而使用的,也能夠經過該參數查看異步函數執行的狀態,該接口有一個 IsCompleted的屬性。在no.2處使用 d.EndInvoke(r)來接受異步函數返回值的。必須指出的是,主線程在調用委託的 EndInvoke(r)方法時,當異步函數沒有執行完畢的話,主線程會一直處於阻塞,等待異步函數執行完畢,獲取返回值以後才執行no.3的 for循環。這樣就還會致使主線程處於阻塞狀態。
理想的狀態的是,當異步函數調用完成以後,自動通知任務執行完成。固然委託也可以作到,這就要使用BeginInvoke方法的後兩個參數啦。看下面這個例子。
class Program
    {
        public delegate int DoWord(int count);
        static void Main(string[] args)
        {
            DoWord d = new DoWord(WorkPro);
            IAsyncResult r= d.BeginInvoke(100,CallBack ,d);//no.1
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);//主線程須要作的事
            }
            Console.WriteLine("主線程done");
            Console.ReadKey();
        }
        public static int WorkPro(int count)
        {
            int sum = 0;
            //作一些耗時的工做
            for (int i = 0; i < count; i++)
            {
                sum += i;
                Thread.Sleep(10);
            }
            return sum;        
        }
 
        public static void CallBack(IAsyncResult r)
        {
            DoWord d = (DoWord)r.AsyncState;
            Console.WriteLine("異步調用完成,返回結果爲{0}", d.EndInvoke(r));
        }
    }
首先來解釋一下BeginInvoke方法的第二個參數是AsyncCallBack 類型的委託(回調函數),當該參數不爲空,那麼在異步函數執行完畢以後,會調用該委託;第三個參數Object 類型的,表明傳遞給回調函數的異步調用狀態。CallBack回調函數必須帶有一個IAsyncResult 類型的參數,經過這個參數能夠在回調方法內部獲取異步調用的結果。在no.1出就給BeginInvoke函數傳遞了回調函數CallBack,和委託d,當異步數WorkPro執行完畢以後,就當即通知CallBack回調函數來顯示執行結果。這下主線程就不須要阻塞一直的等待異步函數的結果,大大的提高了程序的運行效率。在.net還提供許多類的 BeinXXX()EndXXX()的異步版本,好比文件的讀寫等,具體能夠查閱相關的資料。
其中異步函數內部所使用的線程均是線程池中的工做線程,由線程池去分配管理的。
 
2、多線程模型

.net在System.ThreadingSystem.Threading.Tasks這兩個命名空間中提供了ThreadThreadPool,和Task三個類來處理多線程的問題,其中Thread是創建一個專用線程,ThreadPool是使用線程池中工做線程,而Task類是採用任務的方式,其內部也是使用線程池中的工做線程。本節只講Tread類和Tasks類的使用以及其優劣。編程


一、Thread類
Thread類的使用方法很簡單,它開闢的是一個專用線程,不是線程池中的工做線程,不禁線程池去管理。該類提供4個重載版本,常見的使用前面兩個就行了。
1) public  Thread( ThreadStart start ):其中 ThreadStart是一個無參無返回值的委託類型。
2) public  Thread( ParameterizedThreadStart start ):其中 ParameterizedThreadStart 是一個帶有一個 Object類型的參數,無返回值的委託類型。
Thread類提供了兩個構造函數能夠看出,Thread類可以異步調用無參無返回值的函數,也可以異步調用帶一個Object類型的無返回值的函數。下面就給出一個例子簡單的演示一下如何使用Thread異步執行一個帶參數的函數。
 
class Program
    {
        static void Main(string[] args)
        {
            Thread t = new Thread(WorkPro);//no.1
            t.IsBackground = true;//no.2
            t.Start(1000);//no.3
        }
        public static void WorkPro(object  t)
        {
            //作一些耗時的工做   
            int count=(int)t;
            for (int i = 0; i < count; i++)
            {
                Thread.Sleep(2000);
            }
 
            Console.WriteLine("任務處理完成");
        }
    }

 

no.1實例化一個 Thread對象,給傳入一個 ParameterizedThreadStart 類型的委託;no.2將創建的專用線程設置爲後臺的任務線程(後臺線程會隨着調用線程(即便任務沒完成)的終止而強制終止,而前臺線程若是任務沒有處理完,是不會隨着調用線程的終止而終止的);no.3調用 Start(1000)方法,其中 1000是傳遞給異步執行函數的參數。記住,若是構造 Thread對象是 ThreadStart委託,那麼 Start()就直接調用,不然會出現異常。只須要簡單的幾行代碼就能實現函數的異步調用。
其中,當異步函數中處理須要多個參數時,那麼只須要創建一個參數類,參數類中包括你函數須要的參數個數,而後將這個參數類傳遞給異步函數便可。
 
Thread類的使用雖然簡單,可是它仍是有必定的劣勢的,通常不推薦使用。
1)Thread類建立的是一個專用線程,創建一個專用線程是很是耗用系統的資源,建議是使用線程池中的線程。
2)Thread類不能很好的和調用線程進行交互,當任務完成時不能及時的通知,在調用線程也不能隨時的取消正在進行的任務。
另外在如下狀況下,就只能選擇使用Thread類了。
1)執行任務的線程要以非普通的優先級去執行,由於線程池的線程都是以普通優先級運行的。
2)執行任務的線程要表現爲一個前臺線程,由於線程池的線程始終都是一個後臺線程。
3)異步執行的任務須要長時間的,那麼就可使用Thread類爲該任務創建一個專用線程。
 

二、Task類c#

Task類是封裝的一個任務類,內部使用的是ThreadPool類,提供了內建機制,讓你知道何時異步完成以及如何獲取異步執行的結果,而且還能取消異步執行的任務。下面看一個例子是如何使用Task類來執行異步操做的。
 class Program
    {
        static void Main(string[] args)
        {
            Task t = new Task((c) =>
                {
                    int count = (int)c;
                    for (int i = 0; i < count; i++)
                    {
                        Thread.Sleep(10);
                    }
                    Console.WriteLine("任務處理完成");
                }, 100);//no.1
            t.Start();
 
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
    }
no.1處使用Task的構造函數爲:
public Task( Action<Object> action, Object state )一個 Action<Object>類型的委託(即異步調用函數具備一個 Object類型的參數),和一個 Object類型的參數,也就是傳遞給異步函數的參數,Task類還有幾種方式的重載,咱們還能夠傳遞一些 TaskCreationOptions標誌來控制 Task的執行方式。在這裏我使用的是 lambda表達去寫委託的,這樣使得程序的結構更加的清晰,使用 Start()來啓動異步函數的調用。
--------
若是須要異步函數有返回值,那麼此時就須要使用Task<TResult>泛型類(派生自Task)來實現,其中TResult表明返回的類型。由於異步函數具備返回值,因此Task<TResult>的各類重載版本的構造函數第一個委託類型的參數都是Fun<TResult>或者Fun<Object,TResult>。下面演示等待任務完成並獲取其結果。
 class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = new Task<int>((c) =>
                {
                    int count = (int)c;
                    int sum=0;
                    for (int i = 0; i < count; i++)
                    {
                        Thread.Sleep(10);
                        sum+=i;
                    }
                    Console.WriteLine("任務處理完成");
                    return sum;
                }, 100);
            t.Start();
            t.Wait();//no.1
            Console.WriteLine("任務執行的結果{0}", t.Result);//no.2
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
}
若是任務中出現了異常,那麼異常會被吞噬掉,並存儲到一個集合中去,而線程能夠返回到線程池中去。可是若是在代碼中調用了Wait方法或者是Result屬性,任務有異常發生就會被引起,不會被吞噬掉。其中Result屬性內部自己也調用了Wati方法。Wait方法和上一節中的委託的EndInvoke方法相似,會使得調用線程阻塞直到異步任務完成。下面咱們會介紹如何避免獲取異步結果的阻塞狀況,在講解以前,先說一下,如何取消正在運行的任務。
------
看下面一段代碼如何演示取消正在運行的任務。
class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();//no.1
            Task<int> t = new Task<int>((c) =>Sum(cts.Token ,(int)c), 100);//no.2
            t.Start();
            cts.Cancel();//no.3若是任務還沒完成,可是Task有可能完成啦
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
        static int Sum(CancellationToken ct, int count)
        {
            int sum = 0;
            for (int i = 0; i < count; i++)
            {
                if (!ct.CanBeCanceled)
                {
                    Thread.Sleep(10);
                    sum += i;
                }
                else
                {
                    Console.WriteLine("任務取消");
                    //進行回滾操做
                    return -1;//退出任務
                }
            }
            Console.WriteLine("任務處理完成");
            return sum;
        }
    }
取消任務要引用一個CancellationTokenSource 對象。在須要異步執行的方法中增長一個CancellationToken類型的形參。而後在異步函數的for循環代碼中用一個if語句判斷CancellationTokenCanBeCanceled屬性,這個屬性能夠用來判斷在調用線程是否取消任務的執行,除CanBeCanceled屬性以外,還可使用ThrowIfCancellationRequested方法,該方法的做用是若是在調用線程調用CancellationTokenSource對象的Cancel方法,那麼就會引起一個異常,而後在調用線程進行捕捉就行了,這是在異步函數中的處理方式。no.1在構建任務以前須要創建一個CancellationTokenSource ,no2.而且把CancellationTokenSource傳遞給異步調用函數,傳遞的是CancellationTokenSource對象的Toke屬性,該屬性是一個CancellationToken類型的對象。這樣就完成任務的取消模式,若是想在調用線程中取消任務的執行, 只須要調用 CancellationTokenSource Cancel方法就行啦。
 
------
前面就說過了,獲取任務結果調用Wait方法和Result屬性致使調用線程阻塞,那麼如何處理這種狀況呢,這就使用了Task<TResult>類提供的ContinueWith方法。該方法的做用是當任務完成時,啓動一個新的任務,不只僅是如此,該方法還有能夠在任務只出現異常或者取消等狀況的時候才執行,只須要給該方法傳遞TaskContinuationOptions枚舉類型就能夠了。下面就演示一下如何使用 ContinueWith方法。
首先看下ContinueWith方法的原型。
public  Task ContinueWith( Action<Task> continuationAction )採用一個Action<Task>類型的委託。該方法提供了多種重載的版本,這只是最簡單的一種。
 

public Task ContinueWith( Action<Task> continuationAction, TaskContinuationOptions continuationOptions )第二個參數表明新任務的執行條件,當任務知足這個枚舉條件才執行 Action<Task>類型的回調函數。安全

代碼以下:
class Program
    {
        static void Main(string[] args)
        {           
            Task<int> t = new Task<int>((c) =>Sum((int)c), 100);
            t.Start();
            t.ContinueWith(task => Console.WriteLine("任務完成的結果{0}", task.Result));//當任務執行完以後執行
            t.ContinueWith(task => Console.WriteLine(""), TaskContinuationOptions.OnlyOnFaulted);//當任務出現異常時才執行
            for (int i = 0; i < 200; i++)
            {
                Thread.Sleep(10);
            }
            Console.WriteLine("done");
        }
        static int Sum( int count)
        {
            int sum = 0;
            for (int i = 0; i < count; i++)
            {       
                    Thread.Sleep(10);
                    sum += i;          
            }
            Console.WriteLine("任務處理完成");
            return sum;
        }
    }
t.Start()以後調用第一個ContinueWith方法,該方法第一參數就是一個Action<Task>的委託類型,至關因而一個回調函數,在這裏我也用lambda表達式,當任務完成就會啓用一個新任務去執行這個回調函數。而第二個ContinueWith裏面的回調方法卻不會執行,由於咱們的任務也就是Sum方法不會發生異常,不能知足TaskContinuationOptions.OnlyOnFaulted這個枚舉條件這種用法比委託的異步函數編程看起來要簡單些。最關鍵的是ContinueWith的還有一個重載版本能夠帶一個TaskScheduler對象參數,該對象負責執行被調度的任務。FCL中提供兩種任務調度器,均派生自TaskScheduler類型:線程池調度器,和同步上下文任務調用器。而在Winform窗體程序設計中TaskScheduler尤其有用,爲何這麼說呢?由於在窗體程序中的控件都是有ui線程去建立,而咱們所執行的後臺任務使用線程都是線程池中的工做線程,因此當咱們的任務完成以後須要反饋到Winform控件上,可是控件建立的線程和任務執行的線程不是同一個線程,若是在任務線程中去更新控件就會致使控件對象安全問題會出現異常。因此操做控件,就必需要使用ui線程去操做。所以在ContinueWith獲取任務執行的結果的並反饋到控件的任務調度上不能使用線程池任務調用器,而要使用同步上下文任務調度器去調度,即採用ui這個線程去調用ContinueWith方法所綁定的回調用函數即Action<Task>類型的委託。下面將使用任務調度器來把異步執行的Sum計算結果反饋到Winform界面的TextBox控件中。
界面以下。

代碼以下。
 public partial class Form1 : Form
    {
        private readonly TaskScheduler contextTaskScheduler;//聲明一個任務調度器
        public Form1()
        {
            InitializeComponent();
            contextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();//no.1得到一個上下文任務調度器
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            Task<int> t = new Task<int>((n) => Sum((int)n),100);
            t.Start();
            t.ContinueWith(task =>this.textBox1 .Text =task.Result.ToString(),contextTaskScheduler);//當任務執行完以後執行
            t.ContinueWith(task=>MessageBox .Show ("任務出現異常"),CancellationToken.None ,TaskContinuationOptions.OnlyOnFaulted,contextTaskScheduler );//當任務出現異常時才執行
        }
        int Sum(int count)
        {
            int sum = 0;
            for (int i = 0; i < count; i++)
            {
                Thread.Sleep(10);
                sum += i;
            }
            Console.WriteLine("任務處理完成");
            return sum;
        }
    }
在no.1窗體的構造函數獲取該UI線程的同步上下文調度器。在按鈕的事件接受異步執行的結果時候,都傳遞了contextTaskScheduler同步上下文的調度器,目的是,當異步任務完成以後,調度UI線程去執行任務完成以後的回調函數。
------
到目前爲止,我日常用到的異步編程模式也就這麼多了,固然Task類的ContinueWith還有不少重載的版本,會提供不同效果。在開篇的時候就說,如何在調用線程中實時獲取異步任務的執行狀況,好比個人任務是插入100w條數據到數據庫,我在界面中須要實時的刷新數據導入的進度條,這種狀況使用上述所講的是作不到的。具體如何作到,我在另一篇文章已經詳細的講過啦,採用回調函數的方法(委託)來實現,連接: http://www.cnblogs.com/mingjiatang/p/5079632.html
 
3、小結
雖然在.net中提供了衆多的異步編程模式,可是推薦最好使用Task類,由於Task類使用線程池中的任務線程,又由線程池管理,效率相對來講較高,並且Task類內部有比較好的機制,能讓調用線程與任務進行交互。反正無論用哪一種模式,總之儘可能不要出現阻塞的狀況,只要程序中出現線程阻塞,線程池就會建立新的活動線程,由於線程池老是要保證活動的任務線程數量與CPU的核數一致,它以爲這樣性能最佳,當阻塞的線程恢復正常以後,線程池又會將多餘的線程銷燬,避免系統調度線程時頻繁的進行上下文切換。這樣的建立、銷燬線程是很是的浪費系統資源影響性能的。而在線程同步的時候經常會出現阻塞的狀況,因此能設計不用線程同步去解決問題,儘可能不用線程同步。最後要是有寫的不對的地方,請各位指正,謝謝!
相關文章
相關標籤/搜索