C# Advanced Tutorial 1-11-Async


主要内容概要

1 进程-线程-多线程,同步和异步
2 委托启动异步调用
3 多线程特点:不卡主线程、速度快、无序性
4 异步的回调和状态参数
5 异步等待三种方式
6 异步返回值

一些概念

  • 进程:计算机概念,程序运行在服务器占据的全部计算机的资源。
  • 线程:计算机概念,是进程在相应操作时候的一个最小单元,也包括cpu/硬盘/内存 虚拟概念。
  • 进程和线程:包含关系,线程是属于某一个进程的,如果一个进程销毁,线程也就不会存在。
  • 句柄:描述程序中的某一个最小单元,是一个long数字,操作系统通过这个数字识别应用程序。
  • 多线程:计算概念,就是某一个进程中,多个线程同时运行。

什么是进程?
当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。
而一个进程又是由多个线程所组成的。

什么是线程?
线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。

什么是多线程?
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

多线程的好处:
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

多线程的不利方面:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要CPU时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
线程太多会导致控制太复杂,最终可能造成很多Bug;

何时使用多线程
多线程程序一般被用来在后台执行耗时的任务。主线程保持运行,并且工作线程做它的后台工作。对于Windows Forms程序来说,如果主线程试图执行冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应。由于这个原因,应该在工作线程中运行一个耗时任务时添加一个工作线程,即使在主线程上有一个有好的提示“处理中…”,以防止工作无法继续。这就避免了程序出现由操作系统提示的“没有相应”,来诱使用户强制结束程序的进程而导致错误。模式对话框还允许实现“取消”功能,允许继续接收事件,而实际的任务已被工作线程完成。_BackgroundWorker_恰好可以辅助完成这一功能。

在没有用户界面的程序里,比如说Windows Service, 多线程在当一个任务有潜在的耗时,因为它在等待另台电脑的响应(比如一个应用服务器,数据库服务器,或者一个客户端)的实现特别有意义。用工作线程完成任务意味着主线程可以立即做其它的事情。

另一个多线程的用途是在方法中完成一个复杂的计算工作。这个方法会在多核的电脑上运行的更快,如果工作量被多个线程分开的话(使用Environment.ProcessorCount属性来侦测处理芯片的数量)。

一个C#程序称为多线程的可以通过2种方式:明确地创建和运行多线程,或者使用.NET framework的暗中使用了多线程的特性——比如_BackgroundWorker_类, _线程池threading timer_,远程服务器,或Web Services或ASP.NET程序。在后面的情况,人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即使有这样的事情;幸运的是,应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。

何时不要使用多线程
多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程本身并不复杂,复杂是的线程的交互作用,这带来了无论是否交互是否是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的bugs。因此,要么多线程的交互设计简单一些,要么就根本不使用多线程。除非你有强烈的重写和调试欲望。

当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。在某些情况下,太多的I/O操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务块的多。稍后我们将实现生产者/耗费者 队列,它提供了上述功能。

并行和并发的区别?
并行:多核之间叫并行
并发:CPU分片的并发

Thread线程

Thread类是C#语言对线程对象一个封装。

硬件层面理解多线程

为什么可以多线程呢?
1、Cpu有多个核;可以并行计算;

  • 双核四线程:这里的线程是模拟核,把一个CPU虚拟成两个核;

2、cpu分片:某1s的处理能切分成1000份,操作系统调度去相应不同的任务;

  • 从宏观角度来说:感觉就有多个任务在并发执行;
  • 从微观角度来说:一个物理cpu不能在某一刻为某一个任务服务

同步和异步

  • 同步方法:发起调用,只有在调用的方法完成以后,才能继续执行一下一行代码,按照顺序执行;

  • 异步方法:发起调用,不等待完成,直接进入下一行代码的执行,启动一个新的线程来完成计算。
    对于同样一个委托方法,是一个比较耗时耗资源的私有方法,分别用同步和异步的方法去实现:

    private void DoSomethingLong(string name)
    {
    Console.WriteLine($"****************DoSomethingLong Start  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    long lResult = 0;
    Thread.Sleep(2000);//线程等待
    
    Console.WriteLine($"****************DoSomethingLong   End  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
    }

    同步方法代码:

    Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    int l = 3;
    int m = 4;
    int n = l + m;
    for (int i = 0; i < 5; i++)
    {
    string name = string.Format($"btnSync_Click_{i}");
    this.DoSomethingLong(name);
    }
    Console.WriteLine($"****************btnSync_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    

异步方法代码:
```csharp
Console.WriteLine($"****************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

//Action<string> action = new Action<string>(this.DoSomethingLong);
Action<string> action = this.DoSomethingLong;
//action.Invoke("大白");
//action("蓝冰");//这个是同步方法

for (int i = 0; i < 5; i++)
{
  action.BeginInvoke("btnAsync_Click", null, null);//第一个参数是委托自身需要的参数 异步方法
}

Console.WriteLine($"****************btnAsync_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");     

小结一下:
异步方法:
1、同步方法卡界面:主线线程(UI线程)忙于计算,无暇他顾
异步方法不卡界面:因为异步方法是新启动一个线程去完后计算,主线程闲置
改善用户体验,winform程序点击某一个按钮,不会卡死界面;
web开发 例如 发短信,发邮件可以交给一个子线程去完成

2、同步方法执行慢:只有一个线程完成计算
异步方法执行快:多个线程去完成计算
10000ms 3000ms 快了三倍多
20000ms 15000ms cpu密集型计算
多线程是资源换性能 CPU占用的更多,资源不是无限的,资源调度损耗

3、同步方法有序执行,异步多线程无顺序
启动无序,线程资源是向操作系统申请的,操作系统有自己的调度策略,所以启动是随机的;
同一个任务同一个线程,执行时间也不确定,CPU分片;
以上两点得出: 启动无序,结束也是没有顺序。

控制异步方法的顺序

思考一下:多线程在使用的时候,如果需要控制顺序呢?怎么实现? 以上面的异步方法为例,希望在执行完异步方法后输出点内容,如何实现?
方式一:回调 线程结束之后回调某个方法,做一些操作。
回调是把后续的动作通过回调参数传递进去,子线程完成计算以后,去调用这个回调委托。
BeginInvoke的第二个参数就是callback,第三个参数是异步结果。

Action<string> action = this.DoSomethingLong;
AsyncCallback callback = asyncResult => Console.WriteLine("计算结束");
asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);

我们看一下这个过程:callback 是一个委托,要求输入一个参数类型为IAsyncResult的参数,这里自定义委托action的异步调用方法在调用时,可以输入三个参数:一个是委托本身的输入参数string,第二个是callback委托,最后一个是object类型的回调参数。
不经会问:callback的输入参数在这个过程中没有看到啊,就是这个asyncResult ?这是如何理解呢?
这是因为BeginInvoke这个方法本身就会返回IAsyncResult这个类型的返回结果,所以这个callback中要求的传入参数asyncResult 就是BeginInvoke方法返回的IAsyncResult。
此外,还可以给回调方法callback传参,也就是这里action.BeginInvoke方法的第三个参数object。
下面可以进行验证。

AsyncCallback callback = ar =>
{
  Thread.Sleep(5000);
  Console.WriteLine($"这里是beginInvoke的第三个参数{ar.AsyncState}");
  Console.WriteLine(object.ReferenceEquals(ar, asyncResult));//true
  Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
  Console.WriteLine("计算结束");
};

调用的时候就可以这样来用,同时发现asyncResult 是BeginInvoke的返回值,它和ar实际上是同一个:

Action<string> action = this.DoSomethingLong;
//如果把自定义的参数传入到回调函数中去?
asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", callback, "八万公里");

方式二:IsCompleted 完成等待

int i = 0;
while (!asyncResult.IsCompleted)
{
  if (i < 9)
  {
    Console.WriteLine($"正在玩命为你加载中。。。已经完成{++i * 10}%");
  }
  else
  {
    Console.WriteLine($"正在玩命为你加载中。。。已经完成99.9999%");
  }
  Thread.Sleep(200);
}
Console.WriteLine("加载完成。。。");

方式三:WaitOne等待

asyncResult.AsyncWaitHandle.WaitOne();//一直等待任务完成
asyncResult.AsyncWaitHandle.WaitOne(-1);//一直等待任务完成
asyncResult.AsyncWaitHandle.WaitOne(3000);//最多等待3000ms,如果超时了,就不等待了 

方式四:EndInvoke
EndInvoke不仅可以等待某次异步调用结束(即时等待),还可以获取委托返回值,但是一个异步操作只能End一次。

Func<int> func = () =>
{
  //Thread.Sleep(5000);
  return DateTime.Now.Year;
};
func.Invoke();
IAsyncResult asyncResult1 = func.BeginInvoke(ar =>
  {
    //func.EndInvoke(ar);
  }, null);

int iResult = func.EndInvoke(asyncResult1);//可以获取委托的真实返回值

上述过程中定义了一个没有输入参数的委托func,然后定义了一个空的AsyncCallBack委托ar,执行BeginInvoke方法得到返回结果asyncResult1 ,这样就可以使用func.EndInvoke(asyncResult1)来等待异步方法执行结束,甚至得到泛型委托func的int类型返回结果。
值得注意的是:可以写在回调里面func.EndInvoke(ar);,但是一个异步操作只能End一次。后面func.EndInvoke(asyncResult1)就不能写了。


文章作者: Chaoqiang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chaoqiang !
评论
 上一篇
C# Advanced Tutorial 1-12-Threads01 C# Advanced Tutorial 1-12-Threads01
主要内容概要线程池ThreadPool ThreadC#中的多线程 1.0已经存在Thread类: C#对线程对象的一个封装ThreadStart是一个没有参数没有返回值的委托:public delegate void ThreadStar
下一篇 
C# Advanced Tutorial 1-10-IOSerialize C# Advanced Tutorial 1-10-IOSerialize
主要内容概要1 文件夹/文件 检查、新增、复制、移动、删除,递归编程技巧2 文件读写,记录文本日志,读取配置文件3 三种序列化器,xml和json4 验证码、图片缩放 IO文件夹检测和管理配置文件AppSettings:会有一些在开发环境
  目录