• .NET 线程 Thread 进程 Process、线程池 pool、Invoke、begininvoke、异步回调


    windows系统是一个多线程的操作系统。一个程序至少有一个进程,一个进程至少有一个线程。进程是线程的容器,一个C#客户端程序开始于一个单独的线程,CLR(公共语言运行库)为该进程创建了一个线程,该线程称为主线程。例如当我们创建一个C#控制台程序,程序的入口是Main()函数,Main()函数是始于一个主线程的。它的功能主要 是产生新的线程
    和执行程序。C#是一门支持多线程的编程语言,通过Thread类创建子线程,
    引入using System.Threading命名空间。
    多线程优点:
    1、 多线程可以提高CPU的利用率,因为当一个线程处于等待状态的时候,CPU会去执行另外的线程
    2、 提高了CPU的利用率,就可以直接提高程序的整体执行速度
    多线程缺点:
    1、线程开的越多,内存占用越大
    2、协调和管理代码的难度加大,需要CPU时间跟踪线程
    3、线程之间对资源的共享可能会产生可不遇知的问题
    C#中的线程分为前台线程和后台线程,线程创建时不做设置默认是前台线程。即线程属性IsBackground=false。
    Thread.IsBackground = false;//false:设置为前台线程,系统默认为前台线程。
    进程》》线程,进程是线程的容器。
    线程有时候也被称为轻量级进程,它的概念和进程十分相似,是一个可以被调度的单元,并且维护自己的堆栈和上下文环境。线程是附属于进程的,一个进程可以包含1个或多个线程,并且同一进程内的多个线程共享一块内存块和资源。
    进程和线程的区别
      最大的区别在于隔离性,每个进程都会被单独隔离(进程拥有自己的内存、资源和运行数据,一个进程的崩溃不会影响到其他进程,因此进程间的交互也相对困难),而同一进程内的所有线程则共享内存和资源,并且一个线程可以访问和结束同一进程内的其他线程
    多个线程由操作系统进行调度控制,决定何时运行哪个线程。所谓线程调度,是指操作系统决定{非开发者控制}如何安排线程执行顺序的算法。按常规分类,线程调度可以分为以下两种:

    ①抢占式调度

    抢占式调度是指每个线程都只有极少的运行时间(在Windows NT内核模式下这个时间不会超过20ms),而当时间片用完时该线程就会被强制暂停,保存上下文并把运行权利交给下一个线程。这样调度的结果就是:所有的线程都在被不停地快速切换运行,使得用户感觉所有的线程都在并行运行。

    ②非抢占式调度

    非抢占式调度是指某个线程在运行时不会被操作系统强制暂停,它可以持续地运行直到运行告一段落并主动交出运行权。在这样的调度方式之下,线程的运行就是单队列的,并且可能产生恶意程序长期霸占运行权的情况。

    在这里插入图片描述

    Join方法主要是用来阻塞调用线程,直到某个线程终止或经过了指定时间为止。官方的解释比较乏味,通俗的说就是创建
    一个子线程,给它加了这个方法,其它线程就会暂停执行,直到这个线程执行完为止才去执行(包括主线程)。
    public void Join();
    public bool Join(int millisecondsTimeout); //毫秒数
    public bool Join(TimeSpan timeout); //时间段
    线程的Priority

    优先级越高表示CPU分配给该线程的时间片越多,执行时间就多
    优先级越低表示CPU分配给该线程的时间片越少,执行时间就少
    尝试对尚未启动的线程调用Abort
    如果对一个尚未启动的线程调用Abort的话,一旦该线程启动就被停止了

    如果在已挂起的线程上调用 Abort,则将在调用 Abort 的线程中引发 ThreadStateException,并将 AbortRequested 添加到被中止的线程的ThreadState 属性中。直到调用 Resume 后,才在挂起的线程中引发 ThreadAbortException。如果在正在执行非托管代码的托管线程上调用
    Abort,则直到线程返回到托管代码才引发 ThreadAbortException。
    当主线程被终止时{About()}其实报出了一个ThreadAbortException, 从中我们可以进行捕获,但是注意的是,主线程直
    到finally语句块执行完毕之后才真正结束(可以仔细看下主线程的状态一直处于AbortRequest),如果你在finally语句块中执行很复杂的逻辑或者计算的话,那么只有等待直到运行完毕才真正销毁主线程(也就是说主线程的状态会变成Aborted,但是由于是主线程所以无法看出).
    Interrupt 方法将当前的调用该方法的线程处于挂起状态,同样在调用此方法的线程上引发一个异常:ThreadInterruptedException,和Abort方法不同的是,被挂起的线程可以唤醒

    static void Main(string[] args)
    {
    Thread thread1 = new Thread(TestInterrupt);
    thread1.Name = “Thread1”;
    thread1.Start();
    Thread.Sleep(1000);
    thread1.Interrupt();
    thread1.Join();
    Console.WriteLine(“finally语句块后,线程{0}主线程的状态:{1}”, thread1.Name, thread1.ThreadState);
    Console.ReadKey();
    }
    static void TestInterrupt()
    {
    try
    {
    Thread.Sleep(3000);
    }
    catch (ThreadInterruptedException e)
    {
    Console.WriteLine(“线程{0}接受到被Interrupt的信号”, Thread.CurrentThread.Name);
    Console.WriteLine(“捕获到Interrupt异常时线程{0}的状态:{1}”, Thread.CurrentThread.Name,
    Thread.CurrentThread.ThreadState);
    }
    finally
    {
    Console.WriteLine(“进入finally语句块后线程{0}的状态:{1}”, Thread.CurrentThread.Name,
    Thread.CurrentThread.ThreadState);
    }
    }
    在这里插入图片描述

    线程安全:
    线程安全是指在当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取
    完,其他线程才可使用。不会出现数据不一致或者数据污染。
    线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

    private Object thisLock = new Object();//创建对象锁
    lock (thisLock)
    {

         // Critical code section
    
    }
    
    • 1
    • 2
    • 3

    线程池
    上面介绍了介绍了平时用到的大多数的多线程的例子,但在实际开发中使用的线程往往是大量的和更为复杂的,这时,
    每次都创建线程、启动线程。从性能上来讲,这样做并不理想(因为每使用一个线程就要创建一个,需要占用系统开销);
    从操作上来讲,每次都要启动,比较麻烦。为此引入的线程池的概念
    系统在创建线程是很复杂的过程,而且很浪费资源的。
    初始化的线程池中是没有线程的,当应用程序区请求线程池时,线程池会制造一个初始线程,一般情况下,线程池会重复使用这个线程来尽量少的创建线程,这样线程池就能尽量避免去创建新的线程而减少的创建线程的开销

    如果某一线程长时间挂起而不工作的话,需要彻底销毁并且释放资源有可能在多个程序请求线程池执行后,线程池中产生了许多挂起的线程,并且这些线程池中的线程会一直处于空闲状态间接导致的内存的浪费,所以微软为线程池设定了一个超时时间,当挂起的线程超时之后会自动销毁这些线程
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    1.减少在创建和销毁线程上所花的时间以及系统资源的开销
    2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”

    在这里插入图片描述

    在什么情况下使用线程池?
    1.单个任务处理的时间比较短
    2.需要处理的任务的数量大
    线程池最多管理线程数量=“处理器数 * 250”。也就是说,如果您的机器为2个2核CPU,那么CLR线程池的容量默认上限便是1000
    通过线程池创建的线程默认为后台线程,优先级默认为Normal。
    工作者线程与I/O线程
    CLR线程池分为工作者线程(workerThreads)与I/O线程 (completionPortThreads) 两种,工作者线程是主要用作管理CLR内部对
    象的运作,I/O(Input/Output) 线程顾名思义是用于与外部系统交换信息,IO线程的细节将在下一节详细说明。
    通过ThreadPool.GetMax(out int workerThreads,out int completionPortThreads )和 ThreadPool.SetMax( int workerThreads, int
    completionPortThreads)两个方法可以分别读取和设置CLR线程池中工作者线程与I/O线程的最大线程数。
    若想测试线程池中有多少的线程正在投入使用,可以通过ThreadPool.GetAvailableThreads( out int workerThreads,out int
    completionPortThreads ) 方法。
    使用CLR线程池的工作者线程一般有两种方式,
    一是直接通过 ThreadPool.QueueUserWorkItem() 方法,
    二是通过委托,

    ThreadPool线程池中包含有两个静态方法可以直接启动工作者线程:
    一为 ThreadPool.QueueUserWorkItem(WaitCallback)
    二为 ThreadPool.QueueUserWorkItem(WaitCallback,Object)
    三为 ThreadPool.UnsafeQueueUserWorkItem(WaitCallback callBack, object state)
    在这里插入图片描述

    class Program
        {
            static void Main(string[] args)
            {
                string taskInfo = "运行10秒";
                // 插入一个新的请求到线程池
                bool result = ThreadPool.QueueUserWorkItem(DoWork, taskInfo);
                // 分配线程有可能会失败
                if (!result)
                {
                    Console.WriteLine("分配线程失败");
                }
                else
                {
                    Console.WriteLine("按回车键结束程序");
                }
    
                Console.ReadKey();
            }
    
            private static void DoWork(object state)
            {
                // 模拟做了一些操作,耗时10s
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("工作者线程的任务是:{0}", state);
                    Thread.Sleep(1000);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    {
        static void Main(string[] args)
        {
            //把CLR线程池的最大值设置为1000
            ThreadPool.SetMaxThreads(1000, 1000);
            //显示主线程启动时线程池信息
            ThreadMessage("Start");
            //启动工作者线程
            ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback));
            Console.ReadKey();
        }
        
        static void AsyncCallback(object state)
        {
            Thread.Sleep(200);
            ThreadMessage("AsyncCallback");
            Console.WriteLine("Async thread do work!");
        }
    
        //显示线程现状
        static void ThreadMessage(string data)
        {
            string message = string.Format("{0}\n  CurrentThreadId is {1}",
                 data, Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine(message);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    首先说下,invoke和begininvoke的使用有两种情况:

    1. control中的invoke、begininvoke。 都运行UI线程,并未开辟新的线程。都会造成阻塞
    2. delegrate中的invoke、begininvoke 、endinvoke。 从ThreadPool中获取线程,如果获取不到,会创建一个线程,后台线程,不阻塞。
      这两种情况是不同的,我们这里要讲的是第1种。下面我们在来说下.NET中对invoke和begininvoke的官方定义。
      control.invoke(参数delegate)方法:在拥有此控件的基础窗口句柄的线程上执行指定的委托。
      control.begininvoke(参数delegate)方法:在创建控件的基础句柄所在线程上异步执行指定委托。

      根据这两个概念我们大致理解invoke表是同步、begininvoke表示异步。同步异步是相对概念,相对当前活跃的线程而言
      delegrate的BeginInvoke方法可以使用线程异步地执行委托所指向的方法。 然后通过delegrate.EndInvoke方法获得方法的返回值(EndInvoke方法的返回值就 是被调用方法的返回值)
      公共语言运行时会自动使用适当的签名为该委托定义 BeginInvoke 和 EndInvoke 方法,也就是说委托的 BeginInvoke 和 EndInvoke 方法是自动生成的,无需定义。
      那么,这个异步到底是什么意思呢?
      异步是指相对于调用BeginInvoke的线程异步(一般是支线程调用),而不是相对于UI线程异步,你在UI线程上调用BeginInvoke ,当然不行了。

      BeginInvoke :执行BeginInvoke方法时,会线程池中获取一个独立线程来执行引用方法,并立即返回一个实现IAsyncResult接口的对象的(该对象包含了线程池中线程运行异步方法的状态),调用线程不阻塞,而引用方法在线程池的线程中并行执行。EndInvoke : 获取异步方法调用返回的值,并释放资源,该方法把异步方法的返回值作为自己的返回值。

    在支线程中调用Invoke和BeginInvoke,都会把代理函数封装成一个消息,放到消息队列,等待OS分发,然后响应
    在这里插入图片描述
    一个队列完成,才能继续下一个队列, 还有宏观队列,微观队列之说
    在这里插入图片描述
    总结:

    1. 当在UI线程中执行Invoke时,其实就是函数的直接调用,没什么区别。

    2. 当在UI线程中执行BeginInvoke时,其实是往消息队列发送了一个消息,这个消息告诉UI线程要执行哪个函数。当butBeginInvoke_Click消息处理完成后,OS分配了这个BeginInvoke消息,才去执行相应的函数。

    Control的Invoke和BeginInvoke的委托方法是在主线程,即UI线程上执行的。也就是说如果你的委托方法用来取花费时间长的数据,然后更新界面什么的,千万别在主线程或者支线程上调用Control.Invoke和Control.BeginInvoke,因为这些是依然阻塞UI线程的,造成界面的假死。

    我们用Thread来调用BeginInvoke和Invoke
    我们开一个线程,让线程执行一些耗费时间的操作,然后再用Control.Invoke和Control.BeginInvoke回到用户UI线程,执行界面更新。
    代码(三) Thread调用Control的Invoke:

    private Thread invokeThread;
    private delegate void invokeDelegate();
    private void StartMethod(){
       //C代码段......
       Control.Invoke(new invokeDelegate(invokeMethod));
      //D代码段......
    }
    private void invokeMethod(){
      //E代码段
    }
    private void butInvoke_Click(object sender, EventArgs e) {
       //A代码段.......
       invokeThread = new Thread(new ThreadStart(StartMethod));   //默认是后台线程
       invokeThread.Start();
       //B代码段......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    解释:

    1. UI执行A
    2. UI开线程InvokeThread,B和C同时执行,B执行在线程UI上,C执行在线程invokeThread上。
    3. invokeThread封送消息给UI,然后自己等待,UI处理完消息后,处理invokeThread封送的消息,即代码段E
    4. UI执行完E后,转到线程invokeThread上,invokeThread线程执行代码段D
      Thread调用Control的BeginInvoke
    private Thread beginInvokeThread;
    private delegate void beginInvokeDelegate();
    private void StartMethod(){
       //C代码段......
       Control.BeginInvoke(new beginInvokeDelegate(beginInvokeMethod));
      //D代码段......
    }
    private void beginInvokeMethod(){
      //E代码段
    }
    private void butBeginInvoke_Click(object sender, EventArgs e) {
       //A代码段.......
       beginInvokeThread = new Thread(new ThreadStart(StartMethod));
       beginInvokeThread .Start();
       //B代码段......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述C-
    C-D,不需要等待E执行完在执行,而invoke,要等到E执行完,才能执行D。
    在这里插入图片描述
    在这里插入图片描述

    小结:在支线程中调用Invoke和BeginInvoke,都会把代理函数封装成一个消息,放到消息队列,等待OS分发,然后响应。
    注意:在子线程中不管调用Invoke或者BeginInvoke,都是封装消息,发送到UI线程的消息队列中去。那么他们的区别是什么?
    Invoke会阻塞子线程。
    BeginInvoke不会阻塞子线程,它和子线程是异步执行的。

    1. 在UI线程中调用Invoke,跟普通的函数调用一样一样的;在UI线程中调用BeginInvoke就是封装消息,挂到UI线程的消息队列中去。
    2. 在子线程中control调用Invoke和BeginInvoke都是封装消息,挂到主线程的消息队列中去
    3. 在子线程中control调用Invoke和BeginInvoke的区别是,子线程会等待Invoke返回再往下继续执行,但是BeginInvoke是同子线程异步执行的,子线程不会等BeginInvoke返回。

    在 WinForm开发过程中经常会用到线程,有时候还往往需要在线程中访问线程外的控件,比如:设置textbox的Text属性等
    等。如果直接设置程序必 定会报出:从不是创建控件的线程访问它,这个异常。通常我们可以采用两种方法来解决。一是通
    过设置control的属性。二是通过delegate,而通 过delegate也有两种方式,一种是常用的方式,另一种就是匿名方式。下面
    分别加以说明.
    首先,通过设置control的一个属性值为false.我们可以在Form_Load方法中添加:
    **Control.CheckForIllegalCrossThreadCalls=false;**来解决。设置为false表示不对错误线程的调用进行捕获。
    这样在线程中对textbox的Text属性进行设置时就不会再报错了。
    其次,通过delegate的方法来解决。
    delegate void SafeSetText(string strMsg);
    private void SetText(string strMsg)
    {
    if(textbox1.InvokeRequired)
    {
    SafeSetText objSet=new SafeSetText(SetText);
    textbox1.Invoke(objSet,new object[]{strMsg});
    }
    else
    {
    textbox1.Text=strMsg;
    }
    }
    在线程内需要设置textbox的值时调用SetText方法既可。我们还可以采用另一种委托的方式来实现,那就是匿名代理,例
    如:
    delegate void SafeSetText(string strMsg);
    private void SetText2(string strMsg)
    {
    SafeSetText objSet = delegate(string str)
    {
    textBox1.Text = str;
    }
    textBox1.Invoke(objSet,new object[]{strMsg});
    }
    以上可以用lambda => 语法 更加简洁 【=> 读goes】 跟JavaScript中箭头函数类似。

    invoke方法的参数很简单,一个委托,一个参数表(可选),而Invoke方法的主要功能就是帮助你在UI线程上调用委托所指定的方法。Invoke方法首先检查发出调用的线程(即当前线程)是不是UI线程,如果是,直接执行委托指向的方法,
    如果不是,它将切换到UI线程,然后执行委托指向的方法。不管当前线程是不是UI线程,Invoke都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。
    所以Invoke方法的参数和返回值和调用他的委托应该是一致的。

    IAsyncResult result = handler.BeginInvoke(1,2,null,null);
    BeginInvoke : 开始一个异步的请求,调用线程池中一个线程来执行,
    返回IAsyncResult 对象(异步的核心). IAsyncResult 简单的说,他存储异步操作的状态信息的一个接口,也可以用他来结束当前异步。
    注意: BeginInvoke和EndInvoke必须成对调用.即使不需要返回值,但EndInvoke还是必须调用,否则可能会造成内存泄漏
    IAsyncResult.AsyncState 属性:

    BeginInvoke方法触发你的异步方法,它和你想要执行的异步方法有相同的参数。另外还有两个可选参数,第一个是AsyncCallback委托是异步完成的回调方法。第二个是用户自定义对象,该对象将传递到回调方法中。BeginInvoke立即返回并且不等待完成异步的调用(继续执行该下面的代码,不需要等待)。BeginInvoke返回IAsyncResult接口,可用于检测异步调用的过程。
    通过EndInvoke方法检测异步调用的结果。如果异步调用尚未完成,EndInvoke将阻塞调用线程,直到它完成。EndInvoke参数包括out和ref参数。
    1、做一些其他操作,然后调用EndInvoke方法阻塞线程直到该方法完成。
    在这里插入图片描述

    2、使用IAsyncResult.AsyncWaitHandle属性,使用它的WaitOne方法阻塞线程直到收到WaitHandle信号,然后调用EndInvoke。使用IAsyncResult.AsyncWaitHandle属性,使用它的WaitOne方法阻塞线程直到收到WaitHandle信号,然后调用EndInvoke。
    在这里插入图片描述

    3、检查BeginInvoke返回值IAsyncResult的状态来决定方法是否完成,然后调用EndInvoke方法。检查BeginInvoke返回值IAsyncResult的状态来决定方法是否完成,然后调用EndInvoke方法。
    在这里插入图片描述

    4、通过在BeginInvoke方法中传递该委托,在回调方法中调用该委托的EenInvoke方法。通过在BeginInvoke方法中传递该委托,在回调方法中调用该委托的EenInvoke方法。如果需要在异步完成后需要做一些其他的操作,你可以在异步完成时执行一个回调方法。在该回调方法中做处理。
    在这里插入图片描述

    注意 无论你怎么使用,都必须调用EndInvoke方法结束你的异步调用。
    获取用户定义的对象,它限定或包含关于异步操作的信息。 例如:
    static void AddComplete(IAsyncResult result)
    {
    AddHandler handler = (AddHandler)result.AsyncState;
    Console.WriteLine(handler.EndInvoke(result));
    。。。。。
    }

    在这里插入图片描述

    异步回调当主线程在执行一段代码的时候,我们用委托执行了一个线程,这个线程要返回一个结果,关键是什么时候返回这个结果,异步回调就是在这个线程执行完成后立即返回这个线程的结果,然后继续执行主线程的方法。异步回调在JavaScript中应用特别常见,而且特别灵活,特别方便,比.NET 方便多了。
    **异步执行:**在主线程在执行的时候,打开一个子线程,主线程继续执行,当主线程需要子线程运行的结果的时候,主线程直接调用子线程运行结果,如果在调用的时候子线程还没有执行完成,主线程等待,直到子线程执行完成,主线程再继续执行。
    这边面就涉及到异步回调了,在使用异步结果的地方,必须在IAsyncResult 对应的处理。
    使用异步结果必须在异步回调中写。

    static void Main(string[] args)
    {
    #region 异步执行 Func 泛型最后一个参数类型就是返回的类型。
    Func d2 = TakeAWhile;
    // 1,3000是委托对应方法的参数,后面参数AsyncCallBack委托,第二个参数Object asyncState,同时都可以为null的。
    IAsyncResult ar = d2.BeginInvoke(1, 3000, null, null);
    while (!ar.IsCompleted)
    {
    Console.Write(“.”);
    Thread.Sleep(50);
    }
    //EndInvoke:一个参数列表、另外一个参数IAsyncResult:表示异步操作的状态。
    int result = d2.EndInvoke(ar);
    Console.Write(“result:{0}”,result);
    #endregion
    }
    static int TakeAWhile(int data, int ms)
    {
    Console.Write(“TakesAWhile started”);
    Thread.Sleep(ms);
    Console.WriteLine(“TakeAWhile Completed”);
    return ++data;
    }

    异步回调

    static void Main(string[] args)
    {
    #region 异步回调
    Func dl = TakeAWhile;
    dl.BeginInvoke(1, 3000, TakesAWhileCompleted, dl);
    for (int i = 0; i < 100; i++)
    {
    Console.Write(“.”);
    Thread.Sleep(50);
    }
    #endregion
    }
    static int TakeAWhile(int data, int ms)
    {
    Console.Write(“TakesAWhile started”);
    Thread.Sleep(ms);
    Console.WriteLine(“TakeAWhile Completed”);
    return ++data;
    }
    // ar 是dl委托异步操作的结果。
    static void TakesAWhileCompleted(IAsyncResult ar)
    {
    if (ar == null)
    throw new ArgumentNullException(“ar”);
    Func dl = (Func)ar.AsyncState;
    int result = dl.EndInvoke(ar);
    Console.WriteLine(“Result:{0}”,result);
    }

    
            static void Main(string[] args)
    
            {
    
                Console.WriteLine("Main ThreadId = " + Thread.CurrentThread.ManagedThreadId);
                //给委托赋值
                Func<long, long> delegateMethod = new Func<long, long>(CalcSum);
                //异步执行委托,这里把委托本身作为asyncState对象传进去,在回调函数中需要使用委托的EndInvoke来获得结果
    
                delegateMethod.BeginInvoke(200, DoneCallback, delegateMethod);
                //Console.WriteLine("444"+delegateMethod.EndInvoke(delegateMethod.BeginInvoke(200, DoneCallback, "5fr")));
                异步执行委托,抛出异常
                //delegateMethod.BeginInvoke(10000000000, DoneCallback, delegateMethod);
                Console.ReadLine();
            }
    
            //委托回调函数
            static void DoneCallback(IAsyncResult asyncResult)
            {
    
                //到这儿委托已经在异步线程中执行完毕
    
                Console.WriteLine("DoneCallback ThreadId = " + Thread.CurrentThread.ManagedThreadId);
                Func<long, long> method = (Func<long, long>)asyncResult.AsyncState;
                //委托执行的异常会在EndInvoke时抛出来
                try
                {
                    //使用BeginInvoke时传入委托的EndInvoke获得计算结果,这时候计算结果已经出来了,有异常的话也在这儿抛出来
                    long sum = method.EndInvoke(asyncResult);
                    Console.WriteLine("sum = {0}", sum);
                }
                catch (OverflowException)
                {
                    Console.WriteLine("运算溢出了");
                }
            }
            //委托方法
            static long CalcSum(long topLimit)
            {
                //委托在另一个线程中开始执行
                Console.WriteLine("Calc ThreadId = " + Thread.CurrentThread.ManagedThreadId);
                checked
                {
                    long result = 0;
                    for (long i = 0; i < topLimit; i++)
                    {
                        result += i;
                    }
                    return result;
                }
            }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    在这里插入图片描述

    APM .Net 1.0
    APM:Asynchronous Programing Model,顾名思义,它即为异步编程模型,最早出现于.Net Framework 1.x中
    APM模型的引入,使得编写异步程序变的如此简单,只需定义委托,将要执行的方法包含其中,并调用Begin/End方法对,即可实现异步编程。在一些基础类库中,也已经提供了异步操作的方法,直接调用即可。同时我们可以看到,BeginInvoke方法,实际上是调用了线程池中的线程进行操作,因此APM模型也应属于多线程程序,同时包含主线程与线程池线程。
    但是APM模型也存在一些缺点:
    1、若不使用回调机制,则需等待异步操作完成后才能继续执行,此时未达到异步操作的效果。
    2、在异步操作的过程中,无法取消,也无法得知操作进度。
    3、若编写GUI程序,异步操作内容与主线程未在同一线程,操作控件时会引起线程安全问题。

    在这里插入图片描述

    APM:就是用委托.beginInvoke、委托.EndInvke
    EAP .Net 2.0
    EAP:全称Event-based Asynchronous Pattern,BackgroundWorker
    EAP显得更加简洁明了,只需更少的代码即可实现更多的功能。
    尤其是BackgroundWorker组件,定义相应的事件后,在不同阶段根据需求编写方法即可实现异步操作、报告
    进度及取消等。
    但是EAP模型的使用,局限性会更强,主要包括以下几点:
    1、可用组件少,除了BackgroundWorker之外,仅有WebClient类支持此模型,在B/S程序中难以使用
    2、只能使用预定义事件,无法手动定义回调函数,且依赖事件的执行顺序
    3、内部封装较多,占用资源比APM方式多
    TPL:TPL .NET 4.0
    .Net在Framework4.0中增加了任务并行库,对开发人员来说利用多核多线程CPU环境变得更加简单,TPL正符合我们本系列的技术需求。因TPL涉及内容较多,且本系列文章为异步程序开发,所以本文并未涉及TPL全部内容。
    1. 当执行返回参数为Task或者Task<>类型的函数时,假如该函数没有用async标识,那么开启线程执行开方法
    2. 当有async标识时,当前线程会把该方法当成同步函数执行,直到运行到await关键字的地方,开启新线程(此时假如中途执行另一个Task标识的方法,不管该方法是不是async,都会同步执行,不会开启新线程, 但是加入把一个task得方法放到变量中,会开启新的线程,这里非常重要)
    3… await关键字表示会开辟新线程来执行后面的方法,但是该线程会等待新线程执行完返回,然后继续执行函数的执行途中是根据await关键字来判断是否需要开辟线程来执行代码(Async void方法调用时不能加await,所以它必定是在主线程中被调用),假如被调用的method前面有await,那么这个method必须包含async关键字,假如一个async标识的方法里面没有await,那么这个方法会被当成同步方法来调用

    “async”关键字会导致调用方法用线程池线程运行吗?

    答: 不会,被async关键字标识的方法不会影响方法是同步还是异步运行并完成,而是,
    它使方法可被分割成多个片段,其中一些片段可能异步运行,这样这个方法可能异步完成。
    这些片段界限就出现在方法内部显示使用”await”关键字的位置处。所以,
    如果在标记了”async”的方法中没有显示使用”await”,那么该方法只有一个片段,
    并且将以同步方式运行并完成。在await关键字出现的前面部分代码和后面部分代码都是
    同步执行的(即在调用线程上执行的,也就是GUI线程,所以不存在跨线程访问控件的问题),
    await关键处的代码片段是在线程池线程上执行。总结为——使用async和await关键字实现的异步方法,
    此时的异步方法被分成了多个代码片段去执行的,而不是像之前的异步编程模型(APM)和EAP那样,
    使用线程池线程去执行一整个方法。
    在这里插入图片描述

    //该任务会运行在主线程中
    task.RunSynchronously()

    TaskStatus
    Created = 0, //该任务已初始化,但尚未被计划。
    WaitingForActivation = 1, //该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。
    WaitingToRun = 2,//该任务已被计划执行,但尚未开始执行。
    Running = 3, //该任务正在运行,但尚未完成。
    WaitingForChildrenToComplete = 4,//该任务已完成执行,正在隐式等待附加的子任务完成。
    RanToCompletion = 5,//已成功完成执行的任务。
    Canceled = 6, //该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的CancellationToken 发出了信号。 有关详细信息,请参阅任务取消。
    Faulted = 7 //由于未处理异常的原因而完成的任务。
    在这里插入图片描述
    threadPool 是默认是后台线程
    threadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:
      ◆ ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
      ◆ ThreadPool不支持线程执行的先后次序;
    Task 是基于线程池的{Task的命名空间:System.Threading.Tasks}。threadPool的语法糖
    ThreadPool和BackgroundWorker的区别在于:BackgroundWorker在WinForm和WPF中还提供了和UI线程交互的能力,而ThreadPool没有这种能力,BackgroundWorker的能力还包括:通知进度、完成回调、取消任务、暂停任务等功能。
    Async、await 、.NET 5.0
    在这里插入图片描述
    在这里插入图片描述
    动态并行(TaskCreationOptions.AttachedToParent) 父任务等待所有子任务完成后 整个任务才算完成

    Thread与Task区别

    任务可以返回结果,没有直接的机制可以从线程返回结果。
    任务通过使用取消令牌来支持取消,但是线程没有。
    一个任务可以同时执行多个线程,线程一次只能运行一个任务。
    可以使用async和await关键字轻松实现异步。
    新的Thread不处理线程池线程,而Task确实使用线程池线程。
    任务是比线程更高层次的概念。
    Thread是基于操作系统级别的线程,而ThreadPool和Task不会创建自己的操作系统线程,二者是由任务调度器(TaskScheduler)执行,默认的调度程序仅仅在ThreadPool上运行,与ThreadPool不同,Task可以在指定时间返回完成结果,并且还可以通过ContinueWith延续任务,以使得任务执行完毕后运行更多操作,如果已完成立即进行回调,也可以调用Wait来同步等待任务完成,如同Thread.Join一样阻塞线程执行,直到任务完成
    由于任务在ThreadPool上运行,因此创建不加任何选项的默认Task并不适用于执行长时间的操作,因为它们可能会将系统默认的线程池给填满导致其他操作的线程阻塞,但是Task提供了LongRunning选项,设置此项后,告诉任务调度器(TaskScheduler)启动新的线程,而不是在ThreadPool上运行
    vartask = Task.Factory.StartNew( => ( “Hello World”), TaskCreationOptions.LongRunning);
    Console.WriteLine(task.Result);

    任务和线程之间的最主要区别之一是异常的传播。在使用线程时,如果我们在长时间运行的方法中获得异常,则不可能在父函数中捕获该异常,但是如果我们在使用任务,则很容易捕获该异常
    在这里插入图片描述
    在这里插入图片描述
    一个async函数中声明多个await关键字时,程序将代码顺序创建并进入异步子线程执行任务实例
    及任务实例返回之后的代码直到下一个await声明处, 最后一个await声明之后的代码会在最后一个异步子线程中执行 ;
    await 依次执行阻塞,跟JavaScript中的 async 的await 一样的 。
    Task.Run的跟Task.Factory.StarNew和new Task相差不多,不同的是前两种是放进线程池立即执行,
    而Task.Run则是等线程池空闲后在后台线程执行。通过task.Result可以取到返回值,若取值的时候,后台线程还没执行完,
    则会等待其执行完毕(会以同步的方式来执行)!

    由执行工作Task对象通常以异步方式执行线程池线程上而不是以同步方式在主应用程序线程中
    TaskCreationOptions
    Task.Factory.StartNew和创建Task类可以带TaskCreationOptions参数而Task.Run不可以带
    //
    // 摘要:
    // 指定应使用默认行为。
    None = 0,
    //
    // 摘要:
    // 提示 System.Threading.Tasks.TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的
    PreferFairness = 1,
    //
    // 摘要:
    // 指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。 它会向 System.Threading.Tasks.TaskScheduler
    // 提示,过度订阅可能是合理的。 可以通过过度订阅创建比可用硬件线程数更多的线程。 它还将提示任务计划程序:该任务需要附加线程,以使任务不阻
    LongRunning = 2,
    //
    // 摘要:
    // 指定将任务附加到任务层次结构中的某个父级。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。 可以使用 System.Th
    // 选项以便将父任务和子任务同步。 请注意,如果使用 System.Threading.Tasks.TaskCreationOptions.DenyChildAttach
    // 选项配置父任务,则子任务中的 System.Threading.Tasks.TaskCreationOptions.AttachedToParent 选项不起作用,并且子任务将作为分离的子
    // 有关详细信息,请参阅附加和分离的子任务。
    AttachedToParent = 4,
    //
    // 摘要:
    // 指定任何尝试作为附加的子任务执行(即,使用 System.Threading.Tasks.TaskCreationOptions.AttachedToParent
    // 选项创建)的子任务都无法附加到父任务,会改成作为分离的子任务执行。 有关详细信息,请参阅附加和分离的子任务。
    DenyChildAttach = 8,
    //
    // 摘要:
    // 防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 System.Threading.
    // 当前计划程序。
    HideScheduler = 16
    父子任务(AttachedToParent,DenyChildAttach)
    AttachedToParent:将子任务附加到父任务上,表现为:附加到父任务上的所有子任务都结束,父任务才结束
    DenyChildAttach:不允许子任务附加到父任务上
    子任务不附加到父任务

    private static void fun5()
    {
     Task t = Task.Factory.StartNew(() =>
     {
     Console.WriteLine("parent");
     Task.Factory.StartNew(() =>
     {
     Thread.Sleep(1000);
     Console.WriteLine("child");
     });
     });
     t.ContinueWith(x =>
     {
     Console.WriteLine("parent over");
     });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    子任务附加到父任务上,使用AttachedToParent
    private static void fun6()
    {
    Task t = Task.Factory.StartNew(() =>
    {
    Console.WriteLine(“parent”);
    Task.Factory.StartNew(() =>
    {
    Thread.Sleep(1000);
    Console.WriteLine(“child”);
    },TaskCreationOptions.AttachedToParent);
    });
    t.ContinueWith(x =>
    {
    Console.WriteLine(“parent over”);
    });
    }
    在这里插入图片描述
    拒绝子任务附加到父任务上,使用DenyChildAttach
    private static void fun7()
    {
    Task t = Task.Factory.StartNew(() =>
    {
    Console.WriteLine(“parent”);
    Task.Factory.StartNew(() =>
    {
    Thread.Sleep(1000);
    Console.WriteLine(“child”);
    }, TaskCreationOptions.AttachedToParent);
    }, TaskCreationOptions.DenyChildAttach);
    t.ContinueWith(x =>
    {
    Console.WriteLine(“parent over”);
    });
    }
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    Task提供了 task.RunSynchronously() 用于同步执行Task任务
    static void Main(string[] args)
    {
    Task task = new Task(() =>
    {
    Thread.Sleep(100);
    Console.WriteLine(“执行Task结束!”);
    });
    //同步执行,task会阻塞主线程
    task.RunSynchronously();
    Console.WriteLine(“执行主线程结束!”);
    Console.ReadKey();
    }
    Thread阻塞线程的方法
    使用Thread时,我们知道用thread.Join()方法即可阻塞主线程。

    static void Main(string[] args)
     {
    	 Thread th1 = new Thread(() => {
    	 Thread.Sleep(500);
    	 Console.WriteLine("线程1执行完毕!");
     });
     th1.Start();
     Thread th2 = new Thread(() => {
     Thread.Sleep(1000);
     Console.WriteLine("线程2执行完毕!");
     });
     th2.Start();
     //阻塞主线程
     th1.Join();
     th2.Join();
     Console.WriteLine("主线程执行完毕!");
     Console.ReadKey();
     }
     如果注释掉两个Join,执行结果是:先打印【主线程执行完毕】,而添加两个Join方法 后执行结果如下,实现了线程阻塞:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    Task的Wait/WaitAny/WaitAll方法 不会阻塞主线程
    Thread的Join方法可以阻塞调用线程,但是有一些弊端:
    ①如果我们要实现很多线程的阻塞时,每个线程都要调用一次Join方法;
    ②如果我们想让所有的线程执行完毕(或者任一线程执行完毕)时,立即解除阻塞,使用Join方法不容易实现。
    Task提供了Wait/WaitAny/WaitAll 方法,可以更方便地控制线程阻塞。
    task.Wait() 表示等待task执行完毕,功能类似于thead.Join();
    Task.WaitAll(Task[]tasks) 表示只有所有的task都执行完成了再解除阻塞;
    Task.WaitAny(Task[] tasks) 表示只要有一个task执行完毕就解除阻塞,
    Task的延续操作(WhenAny/WhenAll/ContinueWith)
    上边的Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。我们现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。
    task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操 作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作。

    static void Main(string[] args)
     {
     Task task1 = new Task(() => {
     Thread.Sleep(500);
     Console.WriteLine("线程1执行完毕!");
     });
     task1.Start();
     Task task2 = new Task(() => {
     Thread.Sleep(1000);
     Console.WriteLine("线程2执行完毕!");
     });
     task2.Start();
     //task1,task2执行完了后执行后续操作
     Task.WhenAll(task1, task2).ContinueWith((t) => {
     Thread.Sleep(100);
     Console.WriteLine("执行后续操作完毕!");
     });
     Console.WriteLine("主线程执行完毕!");
     Console.ReadKey();
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    小结:我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代
    Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。

    Task和线程池之间的抉择
    接下来我们开始站在理论上了解下“线程池”和“任务”之间的
    关系,我们要做到知其然,还要知其所以然。不管是说线程还是任务,我们都不可避免的要讨论下线程
    池,然而在.net 4.0以后,线程池引擎考虑了未来的扩展性,已经充分利用多核微处理器架构,只要在可
    能的情况下,我们应该尽量使用task,而不是线程池。
    这里简要的分析下CLR线程池,其实线程池中有一个叫做“全局队列”的概念,每一次我们使用
    QueueUserWorkItem的使用都会产生一个“工作项”,然后“工作项”进入“全局队列”进行排队,最后线
    程池中的的工作线程以FIFO(First Input First Output)的形式取出,这里值得一提的是在.net 4.0之
    后“全局队列”采用了无锁算法,相比以前版本锁定“全局队列”带来的性能瓶颈有了很大的改观。那么任务
    委托的线程池不光有“全局队列”,而且每一个工作线程都有”局部队列“。我们的第一反应肯定就是“局部
    队列“有什么好处呢?这里暂且不说,我们先来看一下线程池中的任务分配,如下图:
    在这里插入图片描述
    线程池的工作方式大致如下,线程池的最小线程数是6,线程13正在执行任务13,当有新的任务时,
    就会向线程池请求新的线程,线程池会将空闲线程分配出去,当线程不足时,线程池就会创建新的线程来
    执行任务,直到线程池达到最大线程数(线程池满)。总的来说,只有有任务就会分配一个线程去执行,当
    FIFO十分频繁时,会造成很大的线程管理开销。
    下面我们来看一下task中是怎么做的,当我们new一个task的时候“工作项”就会进去”全局队列”,如
    果我们的task执行的非常快,那么“全局队列“就会FIFO的非常频繁,那么有什么办法缓解呢?当我们的
    task在嵌套的场景下,“局部队列”就要产生效果了,比如我们一个task里面有3个task,那么这3个task
    就会存在于“局部队列”中,如下图的任务一,里面有三个任务要执行,也就是产生了所谓的"局部队列",
    当任务三的线程执行完成时,就会从任务一种的队列中以FIFO的形式"窃取"任务执行,从而减少了线程
    管理的开销。这就相当于,有两个人,一个人干完了分配给自己的所有活,而另一个人却还有很多的活,
    闲的人应该接手点忙的人的活,一起快速完成。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    嵌入式软件行业真的没前途吗?
    Java练习题-获取数组元素最大值
    java毕业设计艾灸减肥管理网站Mybatis+系统+数据库+调试部署
    CSS总结第七天
    Docker容器
    【复盘】记录一次数据库连接资源占用完毕
    互联网大厂技术岗实习/求职经验分享(实习内推+简历+面试+offer)
    固定资产设备管理系统
    解密 JavaScript 中的 this:作用、行为和陷阱
    SpringBoot2.0(Spring读取配置文件常用方法,打war包在Tomcat中启动)
  • 原文地址:https://blog.csdn.net/u013400314/article/details/126620745