C#中的进程 线程 同步,异步和多线程(一)--------------------- 进程线程,同步和异步

一.进程和线程

(一) 什么是进程?

计算机概念,程序在服务器运行时占据全部计算资源总和虚拟的,当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。


(二)什么是线程?

线程是程序中的一个执行流,他纯是一个计算机概念,进程在响应操作时最小单位,也包含CPU 内存  网络  硬盘IO 虚拟的概念,更加看不见摸不着,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。

(三)线程和进程的关系?

一个进程会包含多个线程:线程是隶属于某个进程,进程销毁,线程则消失

(四)句柄:其实是个long数字,是操作系统标识应用程序

(五)什么是多线程?

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

1 多个CPU的核可以并行工作,
    4核8线程,这里的线程指的是模拟核
 2 CPU分片,1s的处理能力分成1000份,操作系统调度着去响应不同的任务
    从宏观角度来说,感觉就是多个任务在并发执行
    从微观角度来说,一个物理cpu同一时刻只能为一个任务服务

(七) C#里面的多线程:

Thread类是C#语言对计算机的线程对象的一个封装,对Thread进行操作,并不是直接对计算机中的线程进行操作,用Thread开启线程,net framework需要去计算机申请线程,对Thread进行暂停或者停止操作,.net framework也只是给操作系统发一个信号,它并不能真正的控制操作系统的底层线程。ThreadPool又是对Thread的一个封装,它提供了一个线程池,对C#中的Thread进行一个管理。


(八)多线程的好处: 

可以提高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操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务快的多。

二. 同步和异步:

 

(一)同步方法:

 程序运行的时候,在调用其他方法的时候,会等待被调用的方法按顺序执行完,才会继续执行。非常符合开发思维,有序执行;

就像 诚心诚意的请人吃饭,邀请Nick,Nick要忙一会儿,等着Nick完成后,再一起去吃饭

(二)异步方法:

异步方法:在程序调用异步方法的时候,主程序不会等待方法执行完,而是主程序调用异步方法后直接继续运行,而异步方法会启动一个新线程来完成方法的计算。

就像 客气一下的请人吃饭,邀请王五,王五要忙一会儿,你忙着我去吃饭了,你忙完自己去吃饭吧             

 

下面举一个异步方法的例子:

/// 
/// 一个比较耗时耗资源的私有方法
/// 
/// 
private void DoSomethingLong(string name)
{
    Console.WriteLine($"*****DoSomethingLong开始;参数【{name}】;线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】;当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***");
    long result = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        result += i;
    }
    Console.WriteLine($"*****DoSomethingLong结束;参数【{name}】;线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】;当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")};result:{result}***");
}

///用委托的异步方式调用上面那个方法
private void AsyncWay()
{
    Console.WriteLine($"******异步方法开始执行,执行线程ID:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】,执行开始时间:【 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}】*****");
    Action action = this.DoSomethingLong;
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format($"async_{i}");                
        action.BeginInvoke(name, null, null);//委托自身需要的参数 + 2个异步参数(这两个参数我们在下面详细说明)
    }
    Console.WriteLine($"****异步方法执行结束,执行线程ID:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】,执行开始时间:【 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}】******");
}

C#中的进程 线程 同步,异步和多线程(一)--------------------- 进程线程,同步和异步_第1张图片

三.异步方法比同步方法各自的特点

通过上面 同步调用方法和异步调用方法的两段程序的比较,我们可以知道,异步方法比同步方法有如下几个方面的几个特点 :

(一).用户体验方面

同步进程会卡界面:在Winform、WPF或者Web应用程序中,同步方法会卡界面,因为主线程就是UI线程,它忙于计算被调用的方法中的程序,无法响应你对界面的一些操作。

异步线程用户体验比较好:UI主线程就是响应对主界面的操作,其他的计算都交给子线程去做,一般在web应用中异步方法会做发短信/记日志等功能。

(二) 耗时方面

同步线程耗时比较长:只有一个线程在工作,所有事情都要由这一个线程来完成,一个线程同一时间只能做一件事情,多个方法的时候,就要排队等待,所以耗时会比较长。

异步方法耗时比较短:多个线程同时工作,如果有多个事情需要做的时候,可以交给子线程来做,不需要排队等待,所以耗时比较短。

(三).执行顺序方面

同步方法的有序性:同步方法的执行是有序的,就是按照代码的顺序依次往下执行。非常符合开发思维,逻辑比较好理解。

异步方法是无序的:无序的原因有以下几点

1.启动的顺序无序:并不是哪个线程先被调用就会先执行,因为线程资源是向系统申请的,由操作系统的调度策略来决定,也许第一个被调用的线程去申请线程的时候,恰好没有,它要等待,而第二个被调用的线程去申请的时候,恰好有空闲的线程,然后操作系统还有其他的一些调度策略,导致线程的启动顺序是无序的。

2.同一个任务,同一个线程,执行的速度也是不同的,这跟CPU分片有关(关于CPU的分片,将在另外文章中介绍 )

3.因为以上两点,导致,线程的结束也是无序的。

四.实现异步方法的实现途径之---- 委托的异步调用

            利用委托的 BeginInvoke 和 EndInvoke方法,可以实现委托的异步调用

(一)委托的异步调用的使用方法:

1.定义与你需要异步调用的方法具有相同签名(返回值,参数列表相同,就是相同签名)的委托,公共语言运行库将自动为该委托定义具有适当签名的 BeginInvoke 和 EndInvoke 方法。

//Action是系统声明的一个委托,实例化它给他一个方法。
Action action = this.DoSomethingLong;
//这个委托调用BeginInvoke,就是异步调用this.DoSomethingLong这个方法。
asyncResult = action.BeginInvoke("异步控制顺序", callback, "ar.AsyncState参数");

 

(二)BeginInvoke 方法详解

BeginInvoke 方法用于启动异步调用。如下图所示,BeginInvoke需要以下几个参数:

 

前面的参数是跟委托所需要的参数相同的,只是额外增加了两个参数,AsyncCallback类型的参数,和一个object类型的参数。

AsyncCallback类型的参数,它是一个委托,实际上就是定义了回调函数,注意这个 回调函数 的参数跟 BeginInvoke的返回值的类型是一样的,都是IAsynResult,实际上这两个变量是同一个,这个参数记录了异步调用方法的状态。异步调用方法执行完之后,会把这个返回参数传递给回调函数,

最后一个object的参数 ,前面一个参数是 AsyncCallback的委托,这个委托是封装 传入参数为IAsyncResult 这类方法的委托。

在使用BeginInvoke的时候,最后一个参数是 IAsyncResult .AsyncState的属性,可以作为参数传给前面的回调函数。具体例子如下:


private void FirstWay()
{
    Console.WriteLine($"**开始线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");

    //实例化一个 只传入一个string类型的参数的委托实例
    Action action = this.DoSomethingLong;
    IAsyncResult asyncResult = null;//是对异步调用操作的描述
  
    //回调委托方法:将后续动作通过回调参数传递进去,子线程完成计算后,去调用这个回调委托
    AsyncCallback callback = ar =>
    {
        Console.WriteLine($"asyncResult和ar是否是同一个对象:{object.ReferenceEquals(ar, asyncResult)}"); //可以说明ar就是asyncResult
        Console.WriteLine($"dosomething计算成功了。ar.AsyncState=【{ar.AsyncState}】。线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】");
    };
    //第1个参数:是action需要的参数;
    //第2个参数:AsyncCallback,就是执行完action方法后要执行的内容;
    //第3个参数:是一个object的对象,指的是IAsyncResult函数中的AsyncState,可以作为参数等传进去
    asyncResult = action.BeginInvoke("异步控制顺序", callback, "ar.AsyncState参数");
    Console.WriteLine($"**结束:线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
}

//需要异步调用的私有方法
private void DoSomethingLong(string name)
{
    Console.WriteLine($"*****DoSomethingLong开始;参数【{name}】;线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】;当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***");
    long result = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        result += i;
    }
    Console.WriteLine($"*****DoSomethingLong结束;参数【{name}】;线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】;当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")};result:{result}***");
}

运行结果如下:注意传入的AsyncState参数

我们通过上图可以分析出:

  • A:BeginInvoke是一个异步方法,因为主线程【01】只负责打印,而DoSomethingLong这个方法是开启一个新的线程【03】执行
  • B:AsyncCallback这个委托的参数其实是 action.BeginInvoke返回的结果类型为IAsyncResult,就是例子中的asyncResult
  • C:action.BeginInvoke第三个参数就是IAsyncResult中的AsyncState,是一个object类型,可以作为参数传入到AsyncCallback函数中
  • D:action.BeginInvoke这个函数的执行顺序是:先执行Action函数,然后执行完返回一个IAsyncResult结果asyncResult,然后把asyncResult作为参数传入到AsyncCallback这个委托中,最后再执行AsyncCallback委托

1:BeginInvoke 立即返回,不等待异步调用完成。

2:BeginInvoke 返回 IasyncResult,属性IAsyncResult.IsCompleted可用于监视调用进度。

如下例,完全可以修改出一个显示进度条的程序,用IAsyncResult.IsCompleted 来监控异步方法是否做完。

private void SecondWay()
 {
     Console.WriteLine($"**开始线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
     Action action = this.DoSomethingLong;
     IAsyncResult asyncResult = action.BeginInvoke("异步控制顺序", null, null);

     //通过IsComplate等待,卡界面--主线程在等待,边等待边提示
     int i = 0;
     while (!asyncResult.IsCompleted)
     {
         if (i < 9)
         {
             Console.WriteLine($"DoSomethingLong完成{++i * 10}%....");
         }
         else
         {
             Console.WriteLine($"DoSomethingLong完成99.999999%....");
         }
         Thread.Sleep(300);
     }
     Console.WriteLine("DoSomethingLong完全执行结束!");
     Console.WriteLine($"**结束:线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
 }

3:使用 WaitOne等待,即时等待  限时等待,示例代码如下:

private void ThirdWay()
 {
     Console.WriteLine($"**开始线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
     Action action = this.DoSomethingLong;
     IAsyncResult asyncResult = action.BeginInvoke("第三种异步控制顺序", null, null);
     asyncResult.AsyncWaitHandle.WaitOne();//直接等待任务完成
     //asyncResult.AsyncWaitHandle.WaitOne(-1);//一直等待任务完成
     //asyncResult.AsyncWaitHandle.WaitOne(1000);//最多等待1000ms,超时就不等了

     Console.WriteLine("DoSomethingLong完全执行结束!");
     Console.WriteLine($"**结束:线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
 }

(三) EndInvoke方法详解:

EndInvoke方法是即时等待,获取方法的返回值,一个异步操作只能EndInvoke一次。BeginInvoke方法只是实现方法的异步调用,而调用过之后,程序就会往下执行,如果被调用的异步方法是有返回值的时候,它是无法获取这个返回值的,BeginInvoke的返回值只是记录了异步调用的状态。

如果想获取被调用的异步方法的返回值,就需要用到EndInvoke(IAsyncResult aResult)方法,这个方法接受的传入参数是BeginInvoke的返回值。

EndInvoke 方法用于检索异步调用结果。调用 BeginInvoke 后可随时调用 EndInvoke 方法;如果异步调用未完成,EndInvoke 将一直阻塞到异步调用完成。
            ////action.EndInvoke(asyncResult);//等待某次异步调用操作结束
EndInvoke 的参数包括由BeginInvoke 返回的 IAsyncResult。

private void ForthWay()
{
    Console.WriteLine($"**开始线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");

    Action action = this.DoSomethingLong;
    IAsyncResult asyncResult4Action = action.BeginInvoke("第四种EndInvoke异步控制顺序", null, null);
    action.EndInvoke(asyncResult4Action);


    Func func = () => { return 10; };
    IAsyncResult asyncResult4Fun = func.BeginInvoke(ar => 
    {
        Console.WriteLine($"IAsyncResult中的线程Id:【{ Thread.CurrentThread.ManagedThreadId.ToString("00")}】");
    }, null);
    int result=func.EndInvoke(asyncResult4Fun);//func 是一个有返回值的委托。EndInvoke里面的参数不能为空

    Console.WriteLine($"endInvoke返回的func委托的值:{result}");
    Console.WriteLine("DoSomethingLong完全执行结束!");
    Console.WriteLine($"**结束:线程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
}

IAsyncResult最后一个输出来,通过这个我们可以得知: EndInvoke是让主线程的动作,等着子线程完成,但是这个等待并不会等待回调函数。所以会出现上面的结果

说明:以上四种方法中DoSomethingLong这个函数我们都使用上面异步或者同步方法的示例

 通过以上4种方法总结得出,使用 BeginInvoke 和 EndInvoke 进行异步调用的常用方法。调用了 BeginInvoke 后,可以:

  1. 进行某些操作,然后调用 EndInvoke 一直阻塞到调用完成。
  2. 使用 IAsyncResult.AsyncWaitHandle 获取 WaitHandle,使用它的 WaitOne 方法将执行一直阻塞到发出 WaitHandle 信号,然后调用EndInvoke。这里主要是主程序等待异步方法,等待异步方法的结果。
  3. 轮询由 BeginInvoke 返回的 IAsyncResult,IAsyncResult.IsCompeted确定异步调用何时完成,然后调用 EndInvoke。此处理个人认为与相同。
  4. 将用于回调方法的委托传递给 BeginInvoke。该方法在异步调用完成后在 ThreadPool 线程上执行,它可以调用 EndInvoke。这是在强制装换回调函数里面IAsyncResult.AsyncState(BeginInvoke方法的最后一个参数)成委托,然后用委托执行EndInvoke。
  5. 警告 始终在异步调用完成后调用 EndInvoke。

你可能感兴趣的:(.Net)