2019年6月26日 星期三

To Debug String

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Utility
{
    public static class DebugString
    {
        public static string ToDebugString(this DataTable dataTable)
        {
            var output = new StringBuilder();

            var columnsWidths = new int[dataTable.Columns.Count];

            // Get column widths
            foreach (DataRow row in dataTable.Rows)
            {
                for (int i = 0; i < dataTable.Columns.Count; i++)
                {
                    var length = row[i].ToString().Length;
                    if (columnsWidths[i] < length)
                        columnsWidths[i] = length;
                }
            }

            // Get Column Titles
            for (int i = 0; i < dataTable.Columns.Count; i++)
            {
                var length = dataTable.Columns[i].ColumnName.Length;
                if (columnsWidths[i] < length)
                    columnsWidths[i] = length;
            }

            // Write Column titles
            for (int i = 0; i < dataTable.Columns.Count; i++)
            {
                var text = dataTable.Columns[i].ColumnName;
                output.Append("|" + PadCenter(text, columnsWidths[i] + 2));
            }
            output.Append("|\n" + new string('=', output.Length) + "\n");

            // Write Rows
            foreach (DataRow row in dataTable.Rows)
            {
                for (int i = 0; i < dataTable.Columns.Count; i++)
                {
                    var text = row[i].ToString();
                    output.Append("|" + PadCenter(text, columnsWidths[i] + 2));
                }
                output.Append("|\n");
            }
            return output.ToString();
        }
        private static string PadCenter(string text, int maxLength)
        {
            int diff = maxLength - text.Length;
            return new string(' ', diff / 2) + text + new string(' ', (int)(diff / 2.0 + 0.5));
        }
        public static string ToDebugString(this IDictionary dictionary)
        {
            return "{" + string.Join(",", dictionary.Select(kv => kv.Key.ToString() + "=" + kv.Value.ToString()).ToArray()) + "}";
        }
        public static string ToDebugString(this IList list)
        {
            return "{" + string.Join(",", list.ToArray()) + "}";
        }
        public static string ToDebugString(this HashSet list)
        {
            return "{" + string.Join(",", list.ToArray()) + "}";
        }
        public static string ToDebugString(this DateTime dateTime)
        {
            return dateTime.ToString("yyyy-MM-dd HH:mm:ss");
        }
        public static string ToDebugString(this byte[] bytes)
        {
            var sb = new StringBuilder("new byte[] { ");
            foreach (var b in bytes)
            {
                sb.Append(b + ", ");
            }
            sb.Append("}");
            return sb.ToString();
        }
        public static string ToDebugString(this IEnumerable list)
        {
            return "{" + string.Join(",", list.ToArray()) + "}";
        }
    }
}

2019年6月25日 星期二

《C#併發編程經典實例》學習筆記—2.5 等待任意一個任務完成 Task.WhenAny

問題

執行若干個任務,只需要對其中任意一個的完成進行響應。這主要用於:對一個操作進行多種獨立的嘗試,只要一個嘗試完成,任務就算完成。例如,同時向多個 Web 服務詢問股票價格,但是隻關心第一個響應的。
文中舉的是向多個Web服務詢問股票價格的例子。
我曾在過往的工作中遇到另一個不太相似的例子。一個問答項目,在問題詳情頁面,重要的是問題展示和回答展示。在該頁面有相關房型推薦和類似問題推薦等等多個模塊展示。也就是說在請求問題數據之外還需要請求多個接口,按理說這個時候最適合的是使用Task.WhenAll,但是當時情形下因爲服務器性能受限導致頁面加載過慢影響用戶訪問,所以其時最快需要解決的是頁面加載過慢的問題,所以這時使用Task.WhenAny或許也算得上是一個應急折中的方案,當然這裏不提緩存等其他優化方案。
Task.WhenAny與Task.WhenAll比較:
  • 相同點:參數都是一批任務
  • 不同點:Task.WhenAny返回的是完成的任務。
關於返回值的描述有點不太好理解。結合代碼很容易就能明白。
// 返回第一個響應的 URL 的數據長度。
private static async Task<int> FirstRespondingUrlAsync(string urlA, string urlB)
{
         var httpClient = new HttpClient();
         // 併發地開始兩個下載任務。
         Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
         Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
         // 等待任意一個任務完成。
         Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB);
         // 返回從 URL 得到的數據的長度。
         byte[] data = await completedTask;
         return data.Length;
}
注意Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB);,使用await獲取返回結果仍然是一個Task任務,如果Task.WhenAny返回的Task有異常,這行代碼並不會拋出異常,而是在byte[] data = await completedTask; 這裏拋出異常。
在第一個任務完成之後,如果其他任務沒有被取消,也不曾await,那麼這些任務將被遺棄,被遺棄的任務並不是代表任務停止,而是任務繼續執行直到完成,當然這些被遺棄的任務的結果或異常都會被忽略。
文中提到了另外兩個對WhenAny的使用方法。我試着寫了demo。

使用Task.WhenAny實現超時功能

書中給出的思路是其中一個任務是Delay的,這樣返回的第一個任務如果是該Delay任務。文中不推薦該方式,因爲沒有取消功能,即其他超時的任務不能被取消,無疑對計算機資源是一種浪費,對性能也會造成影響。
        // 返回第一個響應的 URL 的數據長度。
        private static async Task<int> FirstRespondingUrlAsync(string urlA, string urlB)
        {
            var httpClient = new HttpClient();
            // 併發地開始兩個下載任務。
            Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
            Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
            Task<byte[]> delayTask = GetDelayTask();
            // 等待任意一個任務完成。
            Task<byte[]> completedTask = await Task.WhenAny(downloadTaskA, downloadTaskB, delayTask);
            // 返回從 URL 得到的數據的長度。
            byte[] data = await completedTask;
            if (data.Length == 1 && data[0] == byte.MaxValue)
            {
                Console.WriteLine("超時提醒");
            }
            return data.Length;
        }

       
        // 獲取超時任務
        private static Task<byte[]> GetDelayTask()
        {
            return new Task<byte[]>(() =>
            {
                Task.Delay(1000);
                return new[] { byte.MaxValue };
            });
        }

使用Task.WhenAny處理已完成的任務

書中給出的思路是,列表存放Task,完成一個任務就移除一個已完成的Task。文中不推薦此方法,因爲執行時間是 O(N^2),2.6小節有 O(N) 的算法。
        // 處理已完成的任務
        private static async Task ProcessTasksAsync(string urlA, string urlB)
        {
            var httpClient = new HttpClient();
            // 併發地開始兩個下載任務。
            Task<byte[]> downloadTaskA = httpClient.GetByteArrayAsync(urlA);
            Task<byte[]> downloadTaskB = httpClient.GetByteArrayAsync(urlB);
            var tasks = new List<Task<byte[]>> { downloadTaskA, downloadTaskB };
            while (true)
            {
                // 等待任意一個任務完成。
                Task<byte[]> completedTask = await Task.WhenAny(tasks);
                //移除已完成的任務
                tasks.Remove(completedTask);
                if (!tasks.Any())
                {
                    break;
                }
            }
        }

from : https://www.twblogs.net/a/5c9bdd7cbd9eee7523882884

《C#併發編程經典實例》學習筆記—2.4 等待一組任務完成

解決方案

Task.WhenAll 傳入若干任務,當所有任務完成時,返回一個完成的任務。
重載方法
  • Task WhenAll(IEnumerable<Task>)
  • Task WhenAll(params Task[])
  • Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>>)
  • Task<TResult[]> WhenAll<TResult>(params Task<TResult>[])
    舉例:
            var task1 = Task.Delay(TimeSpan.FromSeconds(1));
            var task2 = Task.Delay(TimeSpan.FromSeconds(2));
            var task3 = Task.Delay(TimeSpan.FromSeconds(3));

            await Task.WhenAll(task1, task2, task3);
當任務返回結果類型相同,所有任務完成返回的是,存着每個任務執行結果的數組。
            var task1 = Task.FromResult(1);
            var task2 = Task.FromResult(2);
            var task3 = Task.FromResult(3);

            int[] array = await Task.WhenAll(task1, task2, task3); //{1,2,3}
返回的數組中結果的順序,並非可控,如上述例子中,只是結果爲包含了1,2,3的數組,順序是不定的。
書中不建議使用以 IEnumerable 類型作爲參數的重載。文中沒有介紹作者不建議的原因。我找到作者個人博客的一篇文中中提到如下文字(文章地址:https://blog.stephencleary.com/2015/01/a-tour-of-task-part-7-continuations.html
The IEnumerable<> overloads allow you to pass in a sequence of tasks, such as a LINQ expression. The sequence is immediately reified (i.e., copied to an array). For example, this allows you to pass the results of a Select expression directly to WhenAll. Personally, I usually prefer to explicitly reify the sequence by calling ToArray() so that it’s obvious that’s what’s happening, but some folks like the ability to pass the sequence directly in.
該段文字解釋了作者更喜歡使用LINQ結合ToArray的方式使用異步,因爲作者認爲代碼會更清晰。書中有例子,如下所示:
        static async Task<string> DownloadAllAsync(IEnumerable<string> urls)
        {
            var httpClient = new HttpClient();
            // 定義每一個 url 的使用方法。
            var downloads = urls.Select(url => httpClient.GetStringAsync(url));
            // 注意,到這裏,序列還沒有求值,所以所有任務都還沒真正啓動。
            // 下面,所有的 URL 下載同步開始。
            Task<string>[] downloadTasks = downloads.ToArray();
            // 到這裏,所有的任務已經開始執行了。
            // 用異步方式等待所有下載完成。
            string[] htmlPages = await Task.WhenAll(downloadTasks);
            return string.Concat(htmlPages);
        }
如果報錯記得添加如下引用
using System.Linq;
using System.Net.Http;

返回的Task的狀態

            var task1 = ......;
            var task2 = ......;
            var task3 = ......;

            Task allTasks = Task.WhenAll(task1, task2, task3);
以上述僞代碼爲例說明allTasks的狀態
  • 當task1出現異常,異常會拋給allTasks,allTasks的狀態同task1狀態,也是Faulted。
  • 當task1被取消,allTasks的狀態是Canceled
  • 當task1, task2, task3,不出現異常,也未被取消,allTasks的狀態是RanToCompletion

Task.WhenAll的異常處理

上面提到了異常處理,當一個task異常,該異常會被allTasks接收,當多個task異常,這些異常也都會被allTasks接收。但是task1拋異常,task2也出異常,但是try catch 處理await Task.WhenAll(task1, task2);只能抓取其中某一個異常。如何獲取所有異常呢?書中列舉了兩種處理方法,代碼如下
拋出異常的方法
        static async Task ThrowNotImplementedExceptionAsync()
        {
            throw new NotImplementedException();
        }

        static async Task ThrowInvalidOperationExceptionAsync()
        {
            throw new InvalidOperationException();
        }
第一種處理方式,只能獲取其中一個異常
        static async Task ObserveOneExceptionAsync()
        {
            var task1 = ThrowNotImplementedExceptionAsync();
            var task2 = ThrowInvalidOperationExceptionAsync();
            try
            {
                await Task.WhenAll(task1, task2);
            }
            catch (Exception ex)
            {
                // ex 要麼是 NotImplementedException,要麼是 InvalidOperationException
                //...
            }
        }
第二種處理方式,可以獲取所有異常
        static async Task ObserveAllExceptionsAsync()
        {
            var task1 = ThrowNotImplementedExceptionAsync();
            var task2 = ThrowInvalidOperationExceptionAsync();
            Task allTasks = Task.WhenAll(task1, task2);
            try
            {
                await allTasks;
            }
            catch
            {
                AggregateException allExceptions = allTasks.Exception;
                //...
            }
        }
兩種方式的區別是,await調用Task.WhenAll返回的Task對象,即例子中的allTasks,代碼await allTasks;
作者在書中將對Task.WhenAll的異常處理放在了討論中,並說明了自己的處理方式
使用 Task.WhenAll 時,我一般不會檢查所有的異常。通常情況下,只處理第一個錯誤就足夠了,沒必要處理全部錯誤。
顯然作者更中意第一種方式。那麼你呢?

from : https://www.twblogs.net/a/5c2610d7bd9eee16b3db8997