主頁 > .NET開發 > 理解Task和和async await

理解Task和和async await

2020-11-09 14:21:24 .NET開發

本文將詳解C#類當中的Task,以及異步函式async await和Task的關系

一.Task的前世今生

1.Thread

一開始我們需要創建執行緒的時候一般是通過Thread創建執行緒,一般常用創建執行緒方式有以下幾種:

        static void Main(string[] args)
        {
            Console.WriteLine("begin");

            Thread thread = new Thread(() => TestMethod(2));
            thread.IsBackground = true;//設定為后臺執行緒,默認前臺執行緒
            thread.Start();

            Thread thread1 = new Thread(() => TestMethod1());
            //設定thread1優先級為最高,系統盡可能單位時間內調度該執行緒,默認為Normal
            thread1.Priority = ThreadPriority.Highest;
            thread1.Start();

            Thread thread2 = new Thread((state) => TestMethod2(state));
            thread2.Start("data");
            thread2.Join();//等待thread2執行完成
            Console.WriteLine("end");
        }

        static void TestMethod(int a)
        {
            Thread.Sleep(1000);
            Console.WriteLine($"TestMethod: run on Thread id :{Thread.CurrentThread.ManagedThreadId},is threadPool:{Thread.CurrentThread.IsThreadPoolThread}" +
                $",is Backgound:{Thread.CurrentThread.IsBackground}, result:{a}");
        }

        static void TestMethod1()
        {
            Thread.Sleep(1000);
            Console.WriteLine($"TestMethod1: run on Thread id :{Thread.CurrentThread.ManagedThreadId},is threadPool:{Thread.CurrentThread.IsThreadPoolThread}" +
                $",is Backgound:{Thread.CurrentThread.IsBackground},no result ");
        }

        static void TestMethod2(object state)
        {
            Thread.Sleep(2000);
            Console.WriteLine($"TestMethod2 :run on Thread id :{Thread.CurrentThread.ManagedThreadId},is threadPool:{Thread.CurrentThread.IsThreadPoolThread}" +
               $",is Backgound:{Thread.CurrentThread.IsBackground},result:{state}");
        }

輸出結果:

begin
TestMethod: run on Thread id :4,is threadPool:False,is Backgound:True, result:2
TestMethod1: run on Thread id :5,is threadPool:False,is Backgound:False,no result
TestMethod2 :run on Thread id :7,is threadPool:False,is Backgound:False,result:data
end

or

begin
TestMethod1: run on Thread id :5,is threadPool:False,is Backgound:False,no result
TestMethod: run on Thread id :4,is threadPool:False,is Backgound:True, result:2
TestMethod2 :run on Thread id :7,is threadPool:False,is Backgound:False,result:data
end

由于我的PC是多核CPU,那么TestMethod和TestMethod1所在兩個執行緒是真正并行的,所以有可能輸出結果先后不確定,雖然TestMethod1所在執行緒設定優先級為Highest最高,但可能系統不會優先調度,其實目前不怎么推薦用Thread.Start去創建執行緒,缺點大概如下:

  • 因為在大量需要創建執行緒情況下,用Thread.Start去創建執行緒是會浪費執行緒資源,因為執行緒用完就沒了,不具備重復利用能力
  • 現在一個行程中的CLR默認會創建執行緒池和一些作業執行緒(不要浪費),且執行緒池的作業執行緒用完會回到執行緒池,能夠重復利用,

除非是以下原因:

  • 真的需要操作執行緒優先級

  • 需要創建一個前臺執行緒,由于類似于控制臺程式當初始前臺執行緒執行完就會退出行程,那么創建前臺執行緒可以保證行程退出前該前臺執行緒正常執行成功

    例如在原來的例子注釋掉thread2.Join();,我們會發現輸出完控制臺初始的前臺執行緒輸出完end沒退出行程,只有在TestMethod2(該執行緒凍結2秒最久)執行完才退出

            static void Main(string[] args)
            {
                Console.WriteLine("begin");
    
                Thread thread = new Thread(() => TestMethod(2));
                thread.IsBackground = true;//設定為后臺執行緒,默認前臺執行緒
                thread.Start();
    
                Thread thread1 = new Thread(() => TestMethod1());
                //設定thread1優先級為最高,系統盡可能單位時間內調度該執行緒,默認為Normal
                thread1.Priority = ThreadPriority.Highest;
                thread1.Start();
    
                Thread thread2 = new Thread((state) => TestMethod2(state));
                thread2.Start("data");
                //thread2.Join();//等待thread2執行完成
                Console.WriteLine("end");
            }       
    

    輸出:

    begin
    end
    TestMethod1: run on Thread id :5,is threadPool:False,is Backgound:False,no result
    TestMethod: run on Thread id :4,is threadPool:False,is Backgound:True, result:2
    TestMethod2 :run on Thread id :7,is threadPool:False,is Backgound:False,result:data
    
  • 需要創建一個后臺執行緒,長時間執行的,其實一個Task的TaskScheduler在Default情況下,設定TaskCreationOptions.LongRunning內部也是創建了一個后臺執行緒Thread,而不是在ThreadPool執行,在不需要Task的一些其他功能情況下,Thread更輕量

      Thread longTask = new Thread(() => Console.WriteLine("doing long Task..."));
      longTask.IsBackground = true;
      longTask.Start();
    
    //等價于
    
       new Task(() => Console.WriteLine("doing long Task..."), TaskCreationOptions.LongRunning).Start();
       //OR
       Task.Factory.StartNew(() => Console.WriteLine("doing long Task..."), TaskCreationOptions.LongRunning);
    

2.ThreadPool

一個.NET行程中的CLR在行程初始化時,CLR會開辟一塊記憶體空間給ThreadPool,默認ThreadPool默認沒有執行緒,在內部會維護一個任務請求佇列,當這個佇列存在任務時,執行緒池則會通過開辟作業執行緒(都是后臺執行緒)去請求該佇列執行任務,任務執行完畢則回回傳執行緒池,執行緒池盡可能會用回傳的作業執行緒去執行(減少開辟),如果沒回傳執行緒池,則會開辟新的執行緒去執行,而后執行完畢又回傳執行緒池,大概執行緒池模型如下:

我們通過代碼來看:

        static void Main(string[] args)
        {
            //獲取默認執行緒池允許開辟的最大作業執行緒樹和最大I/O異步執行緒數
            ThreadPool.GetMaxThreads(out int maxWorkThreadCount, 
                                     out int maxIOThreadCount);
            Console.WriteLine($"maxWorkThreadCount:{maxWorkThreadCount},
                              maxIOThreadCount:{maxIOThreadCount}");
            //獲取默認執行緒池并發作業執行緒和I/O異步執行緒數
            ThreadPool.GetMinThreads(out int minWorkThreadCount, 
                                     out int minIOThreadCount);
            Console.WriteLine($"minWorkThreadCount:{minWorkThreadCount},
                              minIOThreadCount:{minIOThreadCount}");
            for (int i = 0; i < 20; i++)
            {
                ThreadPool.QueueUserWorkItem(s =>
                {
                    var workThreadId = Thread.CurrentThread.ManagedThreadId;
                    var isBackground = Thread.CurrentThread.IsBackground;
                    var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
                    Console.WriteLine($"work is on thread {workThreadId}, 
                                      Now time:{DateTime.Now.ToString("ss.ff")}," +
                        $" isBackground:{isBackground}, isThreadPool:{isThreadPool}");
                    Thread.Sleep(5000);//模擬作業執行緒運行
                });
            }
            Console.ReadLine();
        }

輸出如下:

maxWorkThreadCount:32767,maxIOThreadCount:1000
minWorkThreadCount:16,minIOThreadCount:16
work is on thread 18, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 14, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 16, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 5, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 13, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 12, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 10, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 4, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 15, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 7, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 19, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 17, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 8, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 11, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 9, Now time:06.50, isBackground:True, isThreadPool:True
work is on thread 6, Now time:06.50, isBackground:True, isThreadPool:True

work is on thread 20, Now time:07.42, isBackground:True, isThreadPool:True
work is on thread 21, Now time:08.42, isBackground:True, isThreadPool:True
work is on thread 22, Now time:09.42, isBackground:True, isThreadPool:True
work is on thread 23, Now time:10.42, isBackground:True, isThreadPool:True

? 由于我CPU為8核16執行緒,默認執行緒池給我分配了16條作業執行緒和I/O執行緒,保證在該行程下實作真正的并行,可以看到前16條作業執行緒的啟動時間是一致的,到最后四條,執行緒池嘗試去用之前的作業執行緒去請求那個任務佇列執行任務,由于前16潭訓在運行沒回傳到執行緒池,則每相隔一秒,創建新的作業執行緒去請求執行,而且該開辟的最多執行緒數是和執行緒池允許開辟的最大作業執行緒樹和最大I/O異步執行緒數有關的

我們可以通過ThreadPool.SetMaxThreads 將作業執行緒數設定最多只有16,在執行任務前新增幾行代碼:

var success = ThreadPool.SetMaxThreads(16, 16);//只能設定>=最小并發作業執行緒數和I/O執行緒數
Console.WriteLine($"SetMaxThreads success:{success}");
ThreadPool.GetMaxThreads(out int maxWorkThreadCountNew, out int maxIOThreadCountNew);
Console.WriteLine($"maxWorkThreadCountNew:{maxWorkThreadCountNew},
                  maxIOThreadCountNew:{maxIOThreadCountNew}");

輸出如下:

maxWorkThreadCount:32767,maxIOThreadCount:1000
minWorkThreadCount:16,minIOThreadCount:16
SetMaxThreads success:True
maxWorkThreadCountNew:16,maxIOThreadCountNew:16
work is on thread 6, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 12, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 7, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 8, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 16, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 10, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 15, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 13, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 11, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 4, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 9, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 19, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 17, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 5, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 14, Now time:01.71, isBackground:True, isThreadPool:True
work is on thread 18, Now time:01.71, isBackground:True, isThreadPool:True

work is on thread 8, Now time:06.72, isBackground:True, isThreadPool:True
work is on thread 5, Now time:06.72, isBackground:True, isThreadPool:True
work is on thread 19, Now time:06.72, isBackground:True, isThreadPool:True
work is on thread 10, Now time:06.72, isBackground:True, isThreadPool:True
 

可以很清楚知道,由于執行緒池最多只允許開辟16條作業執行緒和I/O執行緒,那么在執行緒池再開辟了16條執行緒之后,將不會再開辟新執行緒,新的任務也只能等前面的作業執行緒執行完回執行緒池后,再用回傳的執行緒去執行新任務,導致新任務的開始執行時間會在5秒后

ThreadPool的優點如下:

  • 默認執行緒池已經根據自身CPU情況做了配置,在需要復雜多任務并行時,智能在時間和空間上做到均衡,在CPU密集型操作有一定優勢,而不是像Thread.Start那樣,需要自己去判斷和考慮
  • 同樣可以通過執行緒池一些方法,例如ThreadPool.SetMaxThreads手動配置執行緒池情況,很方便去模擬不同電腦硬體的執行情況
  • 有專門的I/O執行緒,能夠實作非阻塞的I/O,I/O密集型操作有優勢(后續Task會提到)

但同樣,缺點也很明顯:

  • ThreadPool原生不支持對作業執行緒取消、完成、失敗通知等互動性操作,同樣不支持獲取函式回傳值,靈活度不夠,Thread原生有Abort (同樣不推薦)、Join等可選擇
  • 不適合LongTask,因為這類會造成執行緒池多創建執行緒(上述代碼可知道),這時候可以單獨去用Thread去執行LongTask

3.Task

在.NET 4.0時候,引入了任務并行庫,也就是所謂的TPL(Task Parallel Library),帶來了Task類和支持回傳值的Task<TResult> ,同時在4.5完善優化了使用,Task解決了上述Thread和ThreadPool的一些問題,Task究竟是個啥,我們來看下代碼:

以下是一個WPF的應用程式,在Button的Click事件:

 private void Button_Click(object sender, RoutedEventArgs e)
 {
     Task.Run(() =>
     {
         var threadId = Thread.CurrentThread.ManagedThreadId;
         var isBackgound = Thread.CurrentThread.IsBackground;
         var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
         Thread.Sleep(3000);//模擬耗時操作
         Debug.WriteLine($"task1 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
            });
         new Task(() =>
         {
             var threadId = Thread.CurrentThread.ManagedThreadId;
             var isBackgound = Thread.CurrentThread.IsBackground;
             var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
             Thread.Sleep(3000);//模擬耗時操作
             Debug.WriteLine($"task2 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
         }).Start(TaskScheduler.FromCurrentSynchronizationContext());

         Task.Factory.StartNew(() =>
         {
            var threadId = Thread.CurrentThread.ManagedThreadId;
            var isBackgound = Thread.CurrentThread.IsBackground;
            var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;
            Thread.Sleep(3000);//模擬耗時操作
            Debug.WriteLine($"task3 work on thread:{threadId},isBackgound:{isBackgound},isThreadPool:{isThreadPool}");
          }, TaskCreationOptions.LongRunning);
    }

輸出:

main thread id :1
//由于是并行,輸出結果的前后順序可能每次都不一樣
task1 work on thread:4,isBackgound:True,isThreadPool:True
task3 work on thread:10,isBackgound:True,isThreadPool:False
task2 work on thread:1,isBackgound:False,isThreadPool:False

我用三種不同的Task開辟運行任務的方式,可以看到,Task運行在三種不同的執行緒:

  • task1是運行在執行緒池上,是沒進行任何對Task的設定
  • task2通過設定TaskSchedulerTaskScheduler.FromCurrentSynchronizationContext()是沒有開辟執行緒,利用主執行緒運行
  • task3通過設定TaskCreationOptionsLongRunning和默認TaskScheduler情況下,實際是開辟了一個后臺Thread去運行

因此,其實Task不一定代表開辟了新執行緒,可為在執行緒池上運行,又或是開辟一個后臺Thread,又或者沒有開辟執行緒,通過主執行緒運行任務,這里提一句TaskScheduler.FromCurrentSynchronizationContext(),假設在控制臺或者ASP.NET Core程式運行,會發生報錯,原因是主執行緒的SynchronizationContext為空,可通過TaskScheduler原始碼得知:

public static TaskScheduler FromCurrentSynchronizationContext()
{
     return new SynchronizationContextTaskScheduler();
}
        
internal SynchronizationContextTaskScheduler()
{
     m_synchronizationContext = SynchronizationContext.Current ??
     throw new InvalidOperationException
     (SR.TaskScheduler_FromCurrentSynchronizationContext_NoCurrent);
}

大致對于Task在通過TaskScheduler和TaskCreationOptions設定后對于將任務分配在不同的執行緒情況,如下圖:

原生支持延續、取消、例外(失敗通知)

1.延續

Task其實有兩種延續任務的方式,一種通過ContinueWith方法,這是Task在.NET Framework4.0就支持的,一種則是通過GetAwaiter方法,則是在.NET Framework4.5開始支持,而且該方法也是async await異步函式所用到

控制臺代碼:

 static void Main(string[] args)
 {
      Task.Run(() =>
      {
          Console.WriteLine($"ContinueWith:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
                return 25;
      }).ContinueWith(t =>
      {
          Console.WriteLine($"ContinueWith Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
          Console.WriteLine($"ContinueWith Completed:{t.Result}");
      });

//等價于
     
     var awaiter = Task.Run(() =>
     {
          Console.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
          return 25;
     }).GetAwaiter();
     awaiter.OnCompleted(() =>
     {
          Console.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
          Console.WriteLine($"GetAwaiter Completed:{awaiter.GetResult()}");
     });

     Console.ReadLine();
}

輸出結果:

ContinueWith:threadId:4,isThreadPool:True
GetAwaiter:threadId:5,isThreadPool:True
GetAwaiter Completed:threadId:5,isThreadPool:True
GetAwaiter Completed:25
ContinueWith Completed:threadId:4,isThreadPool:True
ContinueWith Completed:25

//事實上,運行的代碼執行緒,可能和延續的執行緒有可能不是同一執行緒,取決于執行緒池本身的調度
可以手動設定TaskContinuationOptions.ExecuteSynchronously(同一執行緒)
或者 TaskContinuationOptions.RunContinuationsAsynchronously(不同執行緒)
默認RunContinuationsAsynchronously優先級大于ExecuteSynchronously

但有意思的是,同樣的代碼,在WPF/WinForm等程式,運行的輸出是不一樣的:

WPF程式代碼:

      private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task.Run(() =>
            {
                Debug.WriteLine($"ContinueWith:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            }).ContinueWith(t =>
            {
                Debug.WriteLine($"ContinueWith Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            }, TaskContinuationOptions.ExecuteSynchronously);


            Task.Run(() =>
            {
                Debug.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            }).GetAwaiter().OnCompleted(() =>
            {
                Debug.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
            });
        }

輸出:

ContinueWith:threadId:7,isThreadPool:True
GetAwaiter:threadId:9,isThreadPool:True
ContinueWith Completed:threadId:7,isThreadPool:True
GetAwaiter Completed:threadId:1,isThreadPool:False

原因就是GetAwaiter().OnCompleted()會去檢測有沒有SynchronizationContext,因此其實就是相當于以下代碼:

 Task.Run(() =>
  {
       Debug.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
  }).ContinueWith(t =>
  {
       Debug.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
  },TaskScheduler.FromCurrentSynchronizationContext());

如果在WPF程式中要獲得控制臺那樣效果,只需要修改為ConfigureAwait(false),延續任務不在SynchronizationContext即可,如下:

 Task.Run(() =>
 {
      Debug.WriteLine($"GetAwaiter:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
 }).ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
 {
     Debug.WriteLine($"GetAwaiter Completed:threadId:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
 });

2.取消

在.NET Framework4.0帶來Task的同時,同樣帶來了與取消任務有關的類CancellationTokenSourceCancellationToken,下面我們將大致演示下其用法

WPF程式代碼如下:

CancellationTokenSource tokenSource;


private void BeginButton_Click(object sender, RoutedEventArgs e)
{

      tokenSource = new CancellationTokenSource();
      LongTask(tokenSource.Token);
}
        
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
      tokenSource?.Cancel();
}

private void LongTask(CancellationToken cancellationToken)
{
      Task.Run(() =>
      {
          for (int i = 0; i < 10; i++)
          {
               Dispatcher.Invoke(() =>
               {
                  this.tbox.Text += $"now is {i} \n";
               });
               Thread.Sleep(1000);
               if (cancellationToken.IsCancellationRequested)
               {
                   MessageBox.Show("取消了該操作");
                   return;
               }
           }
        }, cancellationToken);
}


效果如下:

其實上述代碼,也可以適用于Thread和ThreadPool,等價于如下代碼:

//當TaskCreationOptions為LongRunning和默認TaskScheduler情況下
new Thread(() =>
{
    for (int i = 0; i < 10; i++)
    {
         Dispatcher.Invoke(() =>
         {
            this.tbox.Text += $"now is {i} \n";
         });
         Thread.Sleep(1000);
         if (cancellationToken.IsCancellationRequested)
         {
             MessageBox.Show("取消了該操作");
             return;
         }
   }
}).Start();

//默認TaskScheduler情況下
ThreadPool.QueueUserWorkItem(t =>
{
      for (int i = 0; i < 10; i++)
      {
           Dispatcher.Invoke(() =>
           {
                this.tbox.Text += $"now is {i} \n";
           });
           Thread.Sleep(1000);
           if (cancellationToken.IsCancellationRequested)
           {
               MessageBox.Show("取消了該操作");
               return;
           }
      }
});

因此,.NET Framework4.0后ThreadThreadPool也同樣能夠通過CancellationTokenSourceCancellationToken類支持取消功能,只是一般這兩者都可以用Task通過設定,底層同樣呼叫的ThreadThreadPool,所以一般沒怎么這么使用,而且關于Task的基本很多方法都默認支持了,例如,Task.Wait、Task.WaitAll、Task.WaitAny、Task.WhenAll、Task.WhenAny、Task.Delay等等

3.例外(失敗通知)

下面控制臺代碼:

 static void Main(string[] args)
 {
      var parent = Task.Factory.StartNew(() =>
      {
            int[] numbers = { 0 };
            var childFactory = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None);
            childFactory.StartNew(() => 5 / numbers[0]); // Division by zero 
            childFactory.StartNew(() => numbers[1]); // Index out of range 
            childFactory.StartNew(() => { throw null; }); // Null reference 
       });
       try
       {
            parent.Wait();
       }
       catch (AggregateException aex)
       {
            foreach (var item in aex.InnerExceptions)
            {
                Console.WriteLine(item.InnerException.Message.ToString());
            }
        }
        Console.ReadLine();
   }

輸出如下:

嘗試除以零,
索引超出了陣列界限,
未將物件參考設定到物件的實體,

這里面parent任務有三個子任務,三個并行子任務分別都拋出不同例外,回傳到parent任務中,而當你對parent任務Wait或者獲取其Result屬性時,那么將會拋出例外,而使用AggregateException則能將全部例外放在其InnerExceptions例外串列中,我們則可以分別對不同例外進行處理,這在多任務并行時候是非常好用的,而且AggregateException的功能例外強大,遠遠不止上面的功能,但是如果你只是單任務,使用AggregateException比普通則其實會有浪費性能,也可以這樣做;

try
{
     var task = Task.Run(() =>
     {
         string str = null;
         str.ToLower();
         return str;
     });
     var result = task.Result;
}
catch (Exception ex)
{

     Console.WriteLine(ex.Message.ToString());
}

//或者通過async await
try
{
      var result = await Task.Run(() =>
      {
          string str = null;
          str.ToLower();
          return str;
      });
      
catch (Exception ex)
{

      Console.WriteLine(ex.Message.ToString());
}

輸出:

未將物件參考設定到物件的實體,

二.異步函式async await

async await是C#5.0,也就是.NET Framework 4.5時期推出的C#語法,通過與.NET Framework 4.0時引入的任務并行庫,也就是所謂的TPL(Task Parallel Library)構成了新的異步編程模型,也就是TAP(Task-based asynchronous pattern),基于任務的異步模式

語法糖async await

我們先來寫下代碼,看看async await的用法:

下面是個控制臺的代碼:

 static async Task Main(string[] args)
 {
     var result = await Task.Run(() =>
     {
         Console.WriteLine($"current thread:{Thread.CurrentThread.ManagedThreadId}," +
                    $"isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
         Thread.Sleep(1000);
         return 25;
     });
    Console.WriteLine($"current thread:{Thread.CurrentThread.ManagedThreadId}," +
    $"isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
    Console.WriteLine(result);
    Console.ReadLine();
 }

輸出結果:

current thread:4,isThreadPool:True
current thread:4,isThreadPool:True
25

換成在WPF/WinForm程式執行,結果如下:

current thread:4,isThreadPool:True
current thread:1,isThreadPool:false
25

是不是感覺似曾相識?上面埋下的彩蛋在這里揭曉了,在講Task的延續的時候我們講到.NET Framework4.5的一種通過GetAwaiter延續方法,事實上,async await就是上面的一種語法糖,編譯的時候大致會編譯成那樣,所以我們一般不手動寫GetAwaiter的延續方法,而是通過async await,大大簡化了編程方式,說它是語法糖,那么有啥證據呢?

我們再寫一些代碼來驗證:

class Program
{
    static void Main(string[] args)
    {
       ShowResult(classType: typeof(Program), methodName: nameof(AsyncTaskResultMethod));
       ShowResult(classType: typeof(Program), methodName: nameof(AsyncTaskMethod));
       ShowResult(classType: typeof(Program), methodName: nameof(AsyncVoidMethod));
       ShowResult(classType: typeof(Program), methodName: nameof(RegularMethod));
       Console.ReadKey();
    }

    public static async Task<int> AsyncTaskResultMethod()
    {
       return await Task.FromResult(5);
    }

    public static async Task AsyncTaskMethod()
    {
       await new TaskCompletionSource<int>().Task;
    }

    public static async void AsyncVoidMethod()
    {

    }

    public static int RegularMethod()
    {
        return 5;
    }

    private static bool IsAsyncMethod(Type classType, string methodName)
    {
       MethodInfo method = classType.GetMethod(methodName);

       Type attType = typeof(AsyncStateMachineAttribute);

       var attrib = (AsyncStateMachineAttribute)method.GetCustomAttribute(attType);

       return (attrib != null);
    }

    private static void ShowResult(Type classType, string methodName)
    {
       Console.Write((methodName + ": ").PadRight(16));

       if (IsAsyncMethod(classType, methodName))
           Console.WriteLine("Async method");
       else
           Console.WriteLine("Regular method");
    }
}

輸出:

AsyncTaskResultMethod: Async method
AsyncTaskMethod: Async method
AsyncVoidMethod: Async method
RegularMethod:  Regular method

在這其中,其實async在方法名的時候,只允許,回傳值為void,Task,Task,否則會發生編譯報錯,事實上,這和其編譯后的結果有關,我們通過ILSpy反編譯這段代碼,截圖關鍵代碼:

internal class Program
{
  [CompilerGenerated]
  private sealed class <AsyncTaskResultMethod>d__1 : IAsyncStateMachine
  {
	  public int <>1__state;
	  public AsyncTaskMethodBuilder<int> <>t__builder;
	  private int <>s__1;
	  private TaskAwaiter<int> <>u__1;
	  void IAsyncStateMachine.MoveNext()
	  {
		  int num = this.<>1__state;
		  int result;
		  try
		  {
			 TaskAwaiter<int> awaiter;
			 if (num != 0)
			 {
				awaiter = Task.FromResult<int>(5).GetAwaiter();
				if (!awaiter.IsCompleted)
				{
					this.<>1__state = 0; 
					this.<>u__1 = awaiter;
				    Program.<AsyncTaskResultMethod>d__1 <AsyncTaskResultMethod>d__ = this;
					this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<AsyncTaskResultMethod>d__1>(ref awaiter, ref <AsyncTaskResultMethod>d__);
					return;
				}
		         }
		         else
		         {
		                awaiter = this.<>u__1;
				this.<>u__1 = default(TaskAwaiter<int>);
				this.<>1__state = -1;
		         }
			 this.<>s__1 = awaiter.GetResult();
			 result = this.<>s__1;
		  }
		  catch (Exception exception)
		  {
			this.<>1__state = -2;
			this.<>t__builder.SetException(exception);
			return;
		  }
		  this.<>1__state = -2;
		  this.<>t__builder.SetResult(result);
	}
	[DebuggerHidden]
	void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
	{
	}
  }
    
  [CompilerGenerated]
  private sealed class <AsyncTaskMethod>d__2 : IAsyncStateMachine
  {
	  public int <>1__state;
	  public AsyncTaskMethodBuilder <>t__builder;
	  private TaskAwaiter<int> <>u__1;
	  void IAsyncStateMachine.MoveNext()
	  {
		   int num = this.<>1__state;
		   try
		   {
				TaskAwaiter<int> awaiter;
				if (num != 0)
				{
					awaiter = new TaskCompletionSource<int>().Task.GetAwaiter();
					if (!awaiter.IsCompleted)
					{
						this.<>1__state = 0;
						this.<>u__1 = awaiter;
						Program.<AsyncTaskMethod>d__2 <AsyncTaskMethod>d__ = this;
						this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<AsyncTaskMethod>d__2>(ref awaiter, ref <AsyncTaskMethod>d__);
						return;
					}
				}
				else
				{
					awaiter = this.<>u__1;
					this.<>u__1 = default(TaskAwaiter<int>);
					this.<>1__state = -1;
				}
				awaiter.GetResult();
			}
			catch (Exception exception)
			{
				this.<>1__state = -2;
				this.<>t__builder.SetException(exception);
				return;
			}
			this.<>1__state = -2;
			this.<>t__builder.SetResult();
		}
      
		[DebuggerHidden]
		void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
		{
		}
	}
    
    private sealed class <AsyncVoidMethod>d__3 : IAsyncStateMachine
	{
		public int <>1__state;
		public AsyncVoidMethodBuilder <>t__builder;
		void IAsyncStateMachine.MoveNext()
		{
			int num = this.<>1__state;
			try
			{
			}
			catch (Exception exception)
			{
				this.<>1__state = -2;
				this.<>t__builder.SetException(exception);
				return;
			}
			this.<>1__state = -2;
			this.<>t__builder.SetResult();
		}
		[DebuggerHidden]
		void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
		{
		}
	}
    
   [DebuggerStepThrough, AsyncStateMachine(typeof(Program.<AsyncTaskResultMethod>d__1))]
   public static Task<int> AsyncTaskResultMethod()
   {
	   Program.<AsyncTaskResultMethod>d__1 <AsyncTaskResultMethod>d__ = new Program.<AsyncTaskResultMethod>d__1();
	  <AsyncTaskResultMethod>d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
	  <AsyncTaskResultMethod>d__.<>1__state = -1;
	  <AsyncTaskResultMethod>d__.<>t__builder.Start<Program.<AsyncTaskResultMethod>d__1>(ref <AsyncTaskResultMethod>d__);
	  return <AsyncTaskResultMethod>d__.<>t__builder.Task;
	}
    
  [DebuggerStepThrough, AsyncStateMachine(typeof(Program.<AsyncTaskMethod>d__2))]
   public static Task AsyncTaskMethod()
   {
		Program.<AsyncTaskMethod>d__2 <AsyncTaskMethod>d__ = new Program.<AsyncTaskMethod>d__2();
		<AsyncTaskMethod>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
		<AsyncTaskMethod>d__.<>1__state = -1;
		<AsyncTaskMethod>d__.<>t__builder.Start<Program.<AsyncTaskMethod>d__2>(ref <AsyncTaskMethod>d__);
		return <AsyncTaskMethod>d__.<>t__builder.Task;
   }

   [DebuggerStepThrough, AsyncStateMachine(typeof(Program.<AsyncVoidMethod>d__3))]
   public static void AsyncVoidMethod()
   {
	Program.<AsyncVoidMethod>d__3 <AsyncVoidMethod>d__ = new Program.<AsyncVoidMethod>d__3();
	<AsyncVoidMethod>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
	<AsyncVoidMethod>d__.<>1__state = -1;
	<AsyncVoidMethod>d__.<>t__builder.Start<Program.<AsyncVoidMethod>d__3>(ref <AsyncVoidMethod>d__);
   }
    
   public static int RegularMethod()
   {
	return 5;
   }
    
}

我們大致來捋一捋,事實上,從反編譯后的代碼可以看出來一些東西了,編譯器大致是這樣的,以AsyncTaskResultMethod方法為例子:

  1. 將標識async的方法,打上AsyncStateMachine 特性
  2. 根據AsyncStateMachine 該特性,編譯器為該方法新增一個以該方法名為名的類AsyncTaskMethodClass,并且實作介面IAsyncStateMachine,其中最主要的就是其MoveNext方法
  3. 該方法去除標識async,在內部實體化新增的類AsyncTaskMethodClass,用AsyncTaskMethodBuilder的Create方法創建一個狀態機物件賦值給物件的該型別的build欄位,并且將狀態state設定為-1.即初始狀態,然后通過build欄位啟動狀態機

實際上,上述只是編譯器為async做的事情,我們可以看到通過AsyncVoidMethod方法編譯器生成的東西和其他方法大致一樣,那么await為編譯器做的就是MoveNext方法里面try那段,這也是AsyncVoidMethod方法和其他方法不一致的地方:

private TaskAwaiter<int> <>u__1;

try
{
	  TaskAwaiter<int> awaiter;
	  if (num != 0)
	  {
		  awaiter = new TaskCompletionSource<int>().Task.GetAwaiter();
		  if (!awaiter.IsCompleted)
		  {
			  this.<>1__state = 0;
			  this.<>u__1 = awaiter;
			  Program.<AsyncTaskMethod>d__2 <AsyncTaskMethod>d__ = this;
			  this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<AsyncTaskMethod>d__2>(ref awaiter, ref <AsyncTaskMethod>d__);
			  return;
		  }
	  }
	  else
	  {
		awaiter = this.<>u__1;
	        this.<>u__1 = default(TaskAwaiter<int>);
		this.<>1__state = -1;
	  }
	  awaiter.GetResult();
}

我們再看看this.<>t__builder.AwaitUnsafeOnCompleted內部:

public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine
{
	try
	{
		AsyncMethodBuilderCore.MoveNextRunner runner = null;
		Action completionAction = this.m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runner);
		if (this.m_coreState.m_stateMachine == null)
		{
			Task<TResult> task = this.Task;
			this.m_coreState.PostBoxInitialization(stateMachine, runner, task);
		}
		awaiter.UnsafeOnCompleted(completionAction);
	}
	catch (Exception exception)
	{
		AsyncMethodBuilderCore.ThrowAsync(exception, null);
	}
}

GetCompletionAction方法內部:

[SecuritySafeCritical]
internal Action GetCompletionAction(Task taskForTracing, ref AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize)
{
	Debugger.NotifyOfCrossThreadDependency();
	ExecutionContext executionContext = ExecutionContext.FastCapture();
	Action action;
	AsyncMethodBuilderCore.MoveNextRunner moveNextRunner;
	if (executionContext != null && executionContext.IsPreAllocatedDefault)
	{
		action = this.m_defaultContextAction;
		if (action != null)
		{
			return action;
		}
		moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext, this.m_stateMachine);
		action = new Action(moveNextRunner.Run);
		if (taskForTracing != null)
		{
			action = (this.m_defaultContextAction = this.OutputAsyncCausalityEvents(taskForTracing, action));
		}
		else
		{
			this.m_defaultContextAction = action;
		}
	}
	else
	{
		moveNextRunner = new AsyncMethodBuilderCore.MoveNextRunner(executionContext, this.m_stateMachine);
		action = new Action(moveNextRunner.Run);
		if (taskForTracing != null)
		{
		    action = this.OutputAsyncCausalityEvents(taskForTracing, action);
		}
	}
	if (this.m_stateMachine == null)
	{
	    runnerToInitialize = moveNextRunner;
	}
	return action;
}

void moveNextRunner.Run()
{
  if (this.m_context != null)
  {
	 try
	 {
		ContextCallback contextCallback = AsyncMethodBuilderCore.MoveNextRunner.s_invokeMoveNext;
		if (contextCallback == null)
		{
		    contextCallback = (AsyncMethodBuilderCore.MoveNextRunner.s_invokeMoveNext = new ContextCallback(AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext));
		}
		ExecutionContext.Run(this.m_context, contextCallback, this.m_stateMachine, true);
		return;
	}
	finally
	{
	     this.m_context.Dispose();
	}
  }
	this.m_stateMachine.MoveNext();
}

從上面的代碼可以看出,其實this.<>t__builder.AwaitUnsafeOnCompleted內部就做了以下:

  1. 從GetCompletionAction方法獲取要給awaiter.UnsafeOnCompleted的action
  2. GetCompletionAction內部先用ExecutionContext.FastCapture()捕獲了當前執行緒的執行背景關系,在用執行背景關系執行了那個回呼方法MoveNext,也就是又一次回到那個一開始那個MoveNext方法

大致執行流程圖如下:

因此,我們驗證了async await確實是語法糖,編譯器為其在背后做了太多的事情,簡化了我們撰寫異步代碼的方式,我們也注意到了其中一些問題:

  • 方法標識async,方法內部沒使用await實際就是同步方法,但是會編譯出async有關的東西,會浪費一些性能
  • 能await Task,事實上能await Task是因為后面編譯器有用到了awaiter的一些東西,例如:
    • !awaiter.IsCompleted
    • awaiter.GetResult()
    • awaiter.UnsafeOnCompleted

確實如猜想的,像await Task.Yield()等等,被await的物件,它必須包含以下條件:

  • 有一個GetAwaiter方法,為實體方法或者擴展方法

  • GetAwaiter方法的回傳值類,必須包含以下條件

    • 直接或者間接實作INotifyCompletion介面,ICriticalNotifyCompletion也繼承自ICriticalNotifyCompletion介面,也就是實作了其UnsafeOnCompleted或者OnCompleted方法

    • 有個布爾屬性IsCompleted,且get開放

    • 有個GetResult方法,回傳值為void或者TResult

    因此可以自定義一些能被await的類,關于如何自定義的細節,可以參考林德熙大佬的這篇文章:C# await 高級用法

async await的正確用途

事實上,我們在執行緒池上還埋下一個彩蛋,執行緒池上有作業執行緒適合CPU密集型操作,還有I/O完成埠執行緒適合I/O密集型操作,而async await異步函式實際上的主場是在I/O密集型這里,我們先通過一段代碼

static void Main(string[] args)
{
     ThreadPool.SetMaxThreads(8, 8);//設定執行緒池最大作業執行緒和I/O完成埠執行緒數量
     Read();
     Console.ReadLine();
}

static void Read()
{
      byte[] buffer;
      byte[] buffer1;

       FileStream fileStream = new FileStream("E:/test1.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 10000, useAsync: true);
       buffer = new byte[fileStream.Length];
       var state = Tuple.Create(buffer, fileStream);

       FileStream fileStream1 = new FileStream("E:/test2.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 10000, useAsync: true);
       buffer1 = new byte[fileStream1.Length];
       var state1 = Tuple.Create(buffer1, fileStream1);

       fileStream.BeginRead(buffer, 0, (int)fileStream.Length, EndReadCallback, state);
       fileStream1.BeginRead(buffer, 0, (int)fileStream1.Length, EndReadCallback, state1);

}

 static void EndReadCallback(IAsyncResult asyncResult)
 {
       Console.WriteLine("Starting EndWriteCallback.");
       Console.WriteLine($"current thread:{Thread.CurrentThread.ManagedThreadId},isThreadPool:{Thread.CurrentThread.IsThreadPoolThread}");
       try
       {
          var state = (Tuple<byte[], FileStream>)asyncResult.AsyncState;
          ThreadPool.GetAvailableThreads(out int workerThreads, out int portThreads);
          Console.WriteLine($"AvailableworkerThreads:{workerThreads},AvailableIOThreads:{portThreads}");
          state.Item2.EndRead(asyncResult);
        }
        finally
        {
           Console.WriteLine("Ending EndWriteCallback.");
        }
}

輸出結果:

Starting EndWriteCallback.
current thread:3,isThreadPool:True
AvailableworkerThreads:8,AvailableIOThreads:7
Ending EndWriteCallback.
Starting EndWriteCallback.
current thread:3,isThreadPool:True
AvailableworkerThreads:8,AvailableIOThreads:7
Ending EndWriteCallback.

我們看到,事實上,兩個回呼方法都呼叫了相同的執行緒,且是執行緒池的I/O完成埠執行緒,假如將兩個實體化FileStream時的引數改下,改為useAsync: false,輸出結果如下:

Starting EndWriteCallback.
current thread:4,isThreadPool:True
AvailableworkerThreads:6,AvailableIOThreads:8
Ending EndWriteCallback.
Starting EndWriteCallback.
current thread:5,isThreadPool:True
AvailableworkerThreads:7,AvailableIOThreads:8
Ending EndWriteCallback.

我們會發現這次用到的是執行緒池的兩條作業執行緒了,其實這就是同步I/O和異步I/O的區別,我們可以大概看下最底層BeginRead代碼:

private unsafe int ReadFileNative(SafeFileHandle handle, byte[] bytes, int offset, int count, NativeOverlapped* overlapped, out int hr)
 {
       if (bytes.Length - offset < count)
       {
            throw new IndexOutOfRangeException(Environment.GetResourceString("IndexOutOfRange_IORaceCondition"));
       }

       if (bytes.Length == 0)
       {
           hr = 0;
           return 0;
       }

       int num = 0;
       int numBytesRead = 0;
       fixed (byte* ptr = bytes)
       {
           num = ((!_isAsync) ? Win32Native.ReadFile(handle, ptr + offset, count, out numBytesRead, IntPtr.Zero) : Win32Native.ReadFile(handle, ptr + offset, count, IntPtr.Zero, overlapped));
       }

       if (num == 0)
       {
           hr = Marshal.GetLastWin32Error();
           if (hr == 109 || hr == 233)
           {
               return -1;
           }

           if (hr == 6)
           {
               _handle.Dispose();
           }

           return -1;
       }
        hr = 0;
        return numBytesRead;
 }

實際上底層是Pinvoke去呼叫win32api ,Win32Native.ReadFile,關于該win32函式細節可參考MSDN:ReadFile,是否異步的關鍵就是判斷是否傳入overlapped物件,而該物件會關聯到一個window內核物件,IOCP(I/O Completion Port),也就是I/O完成埠,事實上行程創建的時候,創建執行緒池的同時就會創建這么一個I/O完成埠內核物件,大致流程如下:

  • 我們兩個I/O請求,事實上對應著我們傳入的兩個IRP(I/O request packet)資料結構,其中包括檔案句柄和檔案中偏移量,會在Pinvoke去呼叫win32api進入win32用戶模式
  • 然后通過win32api函式進入window內核模式,我們兩個請求之后會放在一個IRP佇列
  • 之后系統就會從該IRP佇列,根據檔案句柄和偏移量等資訊去對應請求處理不同的I/O設備,完成后會放入到一個完成IRP佇列中
  • 然后執行緒池的I/O完成埠執行緒通過執行緒池的I/O完成埠物件去拿取那些已經完成IRP佇列

那么在多請求的時候,IOCP模型異步的這種情況,少量的I/O完成埠執行緒就能做到這一切,而同步則要因為一條執行緒要等待該請求處理的完成,那么會大大浪費執行緒,正如上面一樣,兩個請求卻要兩個作業執行緒完成通知,而在async await時期,上面的一些方法已經被封裝以TaskTask<TResult> 物件來代表完成讀取了,那么上面可以簡化為:

 static async Task Main(string[] args)
{
      ThreadPool.SetMaxThreads(8, 8);//設定執行緒池最大作業執行緒和I/O完成埠執行緒數量
      await ReadAsync();
      Console.ReadLine();
}

static async Task<int> ReadAsync()
{
      FileStream fileStream = new FileStream("E:/test1.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 10000, useAsync: true);
      var buffer = new byte[fileStream.Length];
      var result = await fileStream.ReadAsync(buffer, 0, (int)fileStream.Length);
      return result;
 }

底層沒變,只是回呼的時候I/O完成埠執行緒再通過作業執行緒進行回呼(這能避免之前回呼的時候阻塞I/O完成埠執行緒的操作),但是大大的簡化了異步I/O編程,而async await并非不適合CPU密集型,只是I/O操作一般比較耗時,如果用執行緒池的作業執行緒,就會有可能創建更多執行緒來應付更多的請求,CPU密集型的任務并行庫 (TPL)有很多合適的api

總結

我們了解了Task是.NET 撰寫多執行緒的一個非常方便的高層抽象類,你可以不用擔心底層執行緒處理,通過對Task不同的配置,能寫出較高性能的多執行緒并發程式,然后探尋了.NET 4.5引入了的async await異步函式內部做了些啥,知道async await通過和TPL的配合,簡化了撰寫異步編程的方式,特別適合I/O密集型的異步操作,本文只是起到對于Task和async await有個快速的理解作用,而關于微軟圍繞Task做的事情遠遠不止如此,例如通過ValueTask優化Task,還有更利于CPU密集型操作的TPL中的Parallel和PLINQ api等等,可以參考其他書籍或者msdn更深入了解

參考

?Asynchronous programming patterns
?Async in depth
?ThreadPool 類
?Understanding C# async / await
《CLR Via C# 第四版》
《Window核心編程第五版》

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/207928.html

標籤:.NET技术

上一篇:C#.Net與SQLServer時間范圍的最小值最大值

下一篇:使用TreeView樹型選單欄(遞回呼叫資料庫自動創建選單)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more