<em id="3iliw"></em>
  • <progress id="3iliw"></progress>
  • <tbody id="3iliw"><pre id="3iliw"></pre></tbody><dd id="3iliw"><noscript id="3iliw"></noscript></dd>

    <progress id="3iliw"></progress>

    杭州.net培訓
    達內杭州.net培訓中心

    13732203138

    熱門課程

    .NETCore中的并發編程

    • 時間:2019-01-02 11:12
    • 發布:杭州.NET培訓
    • 來源:net知識

    一 并發編程-異步VS.多線程代碼

    并行編程是一個廣泛的術語,我們應該通過觀察異步方法和實際的多線程之間的差異展開探討。 盡管.NET Core使用了任務來表達同樣的概念,一個關鍵的差異是內部處理的不同。 

    調用線程在做其他事情時,異步方法在后臺運行。這意味著這些方法是I/O密集型的,即他們大部分時間用于輸入和輸出操作,例如文件或網絡訪問。 

    只要有可能,使用異步I/O方法代替同步操作很有意義。相同的時間,調用線程可以在處理桌面應用程序中的用戶交互或處理服務器應用程序中的同時處理其他請求,而不僅僅是等待操作完成。

    你可以在我的文章Asynchronous Programming in C# using Async Await–Best Practices中閱讀更多關于使用async和await調用異步方法。該文章來自DNC Magazine (9月刊)。

    計算密集型的方法要求CPU周期工作,并且只能運行在他們專用的后臺線程中。CPU的核心數限制了并行運行時的可用線程數量。操作系統負責在剩余的線程之間切換,使他們有機會執行代碼。 

    這些方法仍然被并發地執行,卻不必被并行地執行。盡管這意味著方法不是同時執行,卻可以在其他方法暫停的時候執行。

    SHAPE\* MERGEFORMAT

    并行vs并發

    本文將在最后一段中重點介紹 在.NET Core中多線程并發編程。

    二 任務并行庫

    .NET Framework 4引入了任務并行庫(TPL)作為編寫并發代碼的首選API。

    .NET Core采用相同的編程模式。 要在后臺運行一段代碼,需要將其包裝成一個 任務:

    var backgroundTask = Task.Run(() => DoComplexCalculation(42));

    // do other work

    var result = backgroundTask.Result;

    當需要返回結果時,Task.Run方法接收一個 函數(Func);當不需要返回結果時,方法Task.Run接收一個 動作(Action)。

    當然,所有的情況下都可以使用lambda表達式,就像我上面例子中調用帶一個參數的長時間方法。 線程池中的某個線程將會處理任務。

    .NET Core的運行時包含一個默認調度程序,使用線程池來處理隊列并執行任務。您可以通過派生TaskScheduler類實現自己的調度算法,代替默認的,但這超過本文的討論范圍。 

    正如我們之前所見,我使用Result屬性來合并被調用的后臺線程。對于不需要返回結果的線程,我可以調用Wait()來代替。這兩種方式都將被堵塞到后臺任務完成。 為了避免堵塞調用線程(如在ASP.NET Core應用程序中),可以使用await關鍵字:

    var backgroundTask = Task.Run(() => DoComplexCalculation(42));

    // do other work

    var result = await backgroundTask;

    這樣被調用的線程將被釋放以便處理其他傳入請求。一旦任務完成,一個可用的工作線程將會繼續處理請求。當然,控制器動作方法必須是異步的:

    public async Task<iactionresult> Index() {     // method body }

    三 處理異常

    將兩個線程合并在一起的時候,任務拋出的任何異常將被傳遞到調用線程中:

    ·        如果使用Result或Wait(),它們將被打包到AggregateException中。實際的異常將被拋出并存儲在其InnerException屬性中。

    ·        如果您使用await,原來的異常將不會被打包。

    在這兩種情況下,調用堆棧的信息將保持不變。

    四 取消任務

    由于任務是可以長時間運行的,所以你可能想要有一個可以提前取消任務的選項。實現這個選項,需要在任務創建的時候傳入取消的令牌(token),之后再使用令牌觸發取消任務:

    var tokenSource = new CancellationTokenSource();

    var cancellableTask = Task.Run(() =>

    {

    for (int i = 0; i < 100; i++)

    {

    if (tokenSource.Token.IsCancellationRequested)

    {

    // clean up before exiting

    tokenSource.Token.ThrowIfCancellationRequested();

    }

    // do long-running processing

    }

    return 42;

    }, tokenSource.Token);

    // cancel the task

    tokenSource.Cancel();

    try

    {

    await cancellableTask;

    }

    catch (OperationCanceledException e)

    {

    // handle the exception

    }

    實際上,為了提前取消任務,你需要檢查任務中的取消令牌,并在需要取消的時候作出反應:在執行必要的清理操作后,調用ThrowIfCancellationRequested()退出任務。

    這個方法將會拋出OperationCanceledException,以便在調用線程中執行相應的處理。

    五 協調多任務

    如果你需要運行多個后臺任務,這里有些方法可以幫助到你。 要同時運行多個任務,只需連續啟動它們并收集它們的引用,例如在數組中:

    var backgroundTasks = new []

    {

    Task.Run(() => DoComplexCalculation(1)),

    Task.Run(() => DoComplexCalculation(2)),

    Task.Run(() => DoComplexCalculation(3))

    };

    現在你可以使用Task類的靜態方法,等待他們被異步或者同步執行完畢。

    // wait synchronously

    Task.WaitAny(backgroundTasks);

    Task.WaitAll(backgroundTasks);

    // wait asynchronously

    await Task.WhenAny(backgroundTasks);

    await Task.WhenAll(backgroundTasks);

    實際上,這兩個方法最終都會返回所有任務的自身,可以像任何其他任務一樣再次操作。為了獲取對應任務的結果,你可以檢查該任務的Result屬性。 處理多任務的異常有點棘手。方法WaitAll和WhenAll不管哪個任務被收集到異常時都會拋出異常。

    不過,對于WaitAll,將會收集所有的異常到對應的InnerExceptions屬性;對于WhenAll,只會拋出第一個異常。為了確認哪個任務拋出了哪個異常,您需要單獨檢查每個任務的Status和Exception屬性。 在使用WaitAny和WhenAny時必須足夠小心。

    他們會等到第一個任務完成(成功或失敗),即使某個任務出現異常時也不會拋出任何異常。他們只會返回已完成任務的索引或者分別返回已完成的任務。

    你必須等到任務完成或訪問其result屬性時捕獲異常,例如:

    var completedTask = await Task.WhenAny(backgroundTasks);

    try

    {

    var result = await completedTask;

    }

    catch (Exception e)

    {

    // handle exception

    }

    如果你想連續運行多個任務,代替并發任務,可以使用延續(continuations)的方式:

    var compositeTask = Task.Run(() => DoComplexCalculation(42))

    .ContinueWith(previous => DoAnotherComplexCalculation(previous.Result),

    TaskContinuationOptions.OnlyOnRanToCompletion)

    ContinueWith()方法允許你把多個任務一個接著一個執行。這個延續的任務將獲取到前面任務的結果或狀態的引用。 你仍然可以增加條件判斷是否執行延續任務,例如只有在前面任務成功執行或者拋出異常時。

    對比連續等待多個任務,提高了靈活性。 當然,您可以將延續任務與之前討論的所有功能相結合:異常處理、取消和并行運行任務。這就有了很大的表演空間,以不同的方式進行組合:

    var multipleTasks = new[]

    {

    Task.Run(() => DoComplexCalculation(1)),

    Task.Run(() => DoComplexCalculation(2)),

    Task.Run(() => DoComplexCalculation(3))

    };

    var combinedTask = Task.WhenAll(multipleTasks);

    var successfulContinuation = combinedTask.ContinueWith(task =>

    CombineResults(task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);

    var failedContinuation = combinedTask.ContinueWith(task =>

    HandleError(task.Exception), TaskContinuationOptions.NotOnRanToCompletion);

    await Task.WhenAny(successfulContinuation, failedContinuation);

    六 任務同步

    如果任務是完全獨立的,那么我們剛才看到的協調方法就已足夠。然而,一旦需要同時共享數據,為了防止數據損壞,就必須要有額外的同步。 

    兩個以及更多的線程同時更新一個數據結構時,數據很快就會變得不一致。就好像下面這個示例代碼一樣:

    var counters = new Dictionary< int, int >();

    if (counters.ContainsKey(key))

    {

    counters[key] ++;

    }

    else

    {

    counters[key] = 1;

    }

    當多個線程同時執行上述代碼時,不同線程中的特定順序執行指令可能導致數據不正確,例如:

    ·        所有線程將會檢查集合中是否存在同一個key

    ·        結果,他們都會進入else分支,并將這個key的值設為1

    ·        最后結果將會是1,而不是2。如果是接連著執行代碼的話,將會是預期的結果。

    上述代碼中,臨界區(critical section)一次只允許一個線程可以進入。在C#中,可以使用lock語句來實現:

    var counters = new Dictionary< int, int >();

    lock (syncObject)

    {

    if (counters.ContainsKey(key))

    {

    counters[key]++;

    }

    else

    {

    counters[key] = 1;

    }

    }

    在這個方法中,所有線程都必須共享相同的的syncObject。

    作為最佳做法,syncObject應該是一個專用的Object實例,專門用于保護對一個獨立的臨界區的訪問,避免從外部訪問。 在lock語句中,只允許一個線程訪問里面的代碼塊。

    它將阻止下一個嘗試訪問它的線程,直到前一個線程退出。這將確保線程完整執行臨界區代碼,而不會被另一個線程中斷。

    當然,這將減少并行性并減慢代碼的整體執行速度,因此您最好最小化臨界區的數量并使其盡可能的短。

    使用Monitor類來簡化lock聲明:

    var lockWasTaken = false;

    var temp = syncObject;

    try

    {

    Monitor.Enter(temp, ref lockWasTaken);

    // lock statement body

    }

    finally

    {

    if (lockWasTaken)

    {

    Monitor.Exit(temp);

    }

    }

    盡管大部分時間您都希望使用lock語句,但Monitor類可以在需要時給予額外的控制。例如,您可以使用TryEnter()而不是Enter(),并指定一個限定時間,避免無止境地等待鎖釋放。

    七 其他同步基元

    Monitor只是.NET Core中眾多同步基元的一員。根據實際情況,其他基元可能更適合。

    Mutex是Monitor更重量級的版本,依賴于底層的操作系統,提供跨多個進程同步訪問資源[1], 是針對Mutex進行同步的推薦替代方案。

    SemaphoreSlim和Semaphore可以限制同時訪問資源的最大線程數量,而不是像Monitor一樣只能限制一個線程。 

    SemaphoreSlim比Semaphore更輕量,但僅限于單個進程。如果可能,您最好使用SemaphoreSlim而不是Semaphore。

    ReaderWriterLockSlim可以區分兩種對訪問資源的方式。它允許無限數量的讀取器(readers)同時訪問資源,并且限制同時只允許一個寫入器(writers)訪問鎖定資源。讀取時線程安全,但修改數據時需要獨占資源,很好地保護了資源。

    AutoResetEvent、ManualResetEvent和ManualResetEventSlim將堵塞傳入的線程,直到它們接收到一個信號(即調用Set() )。

    然后等待中的線程將繼續執行。AutoResetEvent在下一次調用Set()之前,將一直阻塞,并只允許一個線程繼續執行。

    ManualResetEvent和ManualResetEventSlim不會堵塞線程,除非Reset()被調用。ManualResetEventSlim比前兩者更輕量,更值得推薦。

    Interlocked提供一種選擇——原子操作,這是替代locking和其他同步基元更好的選擇(如果適用):

    // non-atomic operation with a lock

    lock (syncObject)

    {

    counter++;

    }

    // equivalent atomic operation that doesn't require a lock

    Interlocked.Increment(ref counter);

    八 并發集合

    當一個臨界區需要確保對數據結構的原子訪問時,用于并發訪問的專用數據結構可能是更好和更有效的替代方案。

    例如,使用ConcurrentDictionary而不是Dictionary,可以簡化lock語句示例:

    var counters = new ConcurrentDictionary< int, int >();

    counters.TryAdd(key, 0);

    lock (syncObject)

    {

    counters[key]++;

    }

    自然地,也有可能像下面一樣:

    counters.AddOrUpdate(key, 1, (oldKey, oldValue) => oldValue + 1);

    因為update的委托是臨界區外面的方法,因此,第二個線程可能在第一個線程更新值之前,讀取到同樣的舊值,使用自己的值有效地覆蓋了第一個線程的更新值,這就丟失了一個增量。

    錯誤使用并發集合也是無法避免多線程帶來的問題。 并發集合的另一個替代方案是 不變的集合(immutable collections)。 類似于并發集合,同樣是線程安全的,但是底層實現是不一樣的。

    任何關改變數據結構的操作將不會改變原來的實例。相反,它們返回一個更改后的副本,并保持原始實例不變:

    var original = new Dictionary< int, int >().ToImmutableDictionary();

    var modified = original.Add(key, value);

    因此在一個線程中對集合任何更改對于其他線程來說都是不可見的。因為它們仍然引用原來的未修改的集合,這就是不變的集合本質上是線程安全的原因。 當然,這使得它們對于解決不同集合的問題很有效。

    最好的情況是多個線程在同一個輸入集合的情況下,獨立地修改數據,在最后一步可能為所有線程合并變更。而使用常規集合,需要提前為每個線程創建集合的副本。

    九 并行LINQ (PLINQ)

    并行LINQ (PLINQ)是Task Parallel Library的替代方案。顧名思義,它很大程度上依賴于LINQ(語言集成查詢)功能。對于在大集合中執行相同的昂貴操作的場景是很有用的。

    與所有操作都是順序執行的普通LINQ to Objects不同的是,PLINQ可以在多個CPU上并行執行這些操作。 發揮優勢所需要的代碼改動也是極小的:

    // sequential execution

    var sequential = Enumerable.Range(0, 40)

    .Select(n => ExpensiveOperation(n))

    .ToArray();

    // parallel execution

    var parallel = Enumerable.Range(0, 40)

    .AsParallel()

    .Select(n => ExpensiveOperation(n))

    .ToArray();

    如你所見,這兩個代碼片段的不同僅僅是調用AsParallel()。這將IEnumerable轉換為ParallelQuery,導致查詢的部分并行運行。要切換為回順序執行,您可以調用AsSequential(),它將再次返回一個IEnumerable。

    默認情況下,PLINQ不保留集合中的順序,以便讓進程更有效率。但是當順序很重要時,可以調用AsOrdered():

    var parallel = Enumerable.Range(0, 40)

    .AsParallel()

    .AsOrdered()

    .Select(n => ExpensiveOperation(n))

    .ToArray();

    同理,你可以通過調用AsUnordered()切換回來。

    十 在完整的.NET Framework中并發編程

    由于.NET Core是完整的.NET Framework的簡化實現,所以.NET Framework中所有并行編程方法也可以在.NET Core中使用。

    唯一的例外是不變的集合,它們不是完整的.NET Framework的組成部分。

    它們作為單獨的NuGet軟件包(System.Collections.Immutable)分發,您需要在項目中安裝使用。

    預約申請免費試聽課

    怕錢不夠?就業掙錢后再付學費!    怕學不會?從入學起,達內定制課程!     擔心就業?達內多家實踐企業供你挑選!

    上一篇:沒有上一篇了
    下一篇:.NET Core 和 .NET Standard
    選擇城市和中心
    貴州省

    廣西省

    海南省

    国拍自产亚洲 2019国拍自产在线,国拍自产亚洲,国产a在线不卡 百度 好搜 搜狗
    <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>