异步多线程 1 利用委托Action/Func

基础概念

  进程:程序运行时,占据的计算资源的合集,称为进程,如qq、word都是一个进程。

     进程之间不会互相干扰。

  线程:依托于进程,一个进程可以有多个线程,它是程序执行的最小单位,响应操作的最小执行流,如果说qq是一个进程,那么聊天,发图片,截图,加好友这些,都可以算作线程。

  多线程指在一个进程下有多个线程并发执行。

      多线程应用在生活中随处可见,例如迅雷,有没有发现迅雷是可以同时下载东西的?例如同时下载A,B,A下载进度到53.4%,B下载进度到47.1%,有时A速度快些,有时B速度快些,反正能确定的是A,B都在下载内容,而不是一定要等A下载完后,B才可以开始下载,这也是多线程的作用。因此,多线程强调”同时,一起进行“,而不是单一的顺下操作。

  

  多线程Thread类:一个封装的类,是.net对线程对象的封装,通过Thread类去完成的操作,最终是通过向操作系统请求得到的执行流。

  


 

 

直接代码演示一下利用委托Aciton开启多线程

Action.BeginInvoke 异步委托

static void Main(string[] args)
        {
            AsyncMethod1();
            Console.ReadLine();
        }

        private static void AsyncMethod1()
        {
            Console.WriteLine("AsyncMethod1开始线程:"+Thread.CurrentThread.ManagedThreadId.ToString());
            Action<string> action = DoSomething;
            action.Invoke("jack");//同步
            action("rose");//同步
            action.BeginInvoke("ok", null, null);//异步
            Console.WriteLine("AsyncMethod1结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }

        private static void DoSomething(string name)
        {
            Console.WriteLine("开始线程id:" + Thread.CurrentThread.ManagedThreadId.ToString() + " name:" + name);
            long result = 0;
            for (int i = 0; i < 100000000; i++)
            {
                result += i;
            }
            Console.WriteLine("结束线程id:" + Thread.CurrentThread.ManagedThreadId.ToString() + " name:" + name);

        }

 

运行结果是这样

 

 可以看到,同步方法,用的是同一个线程 1,而异步方法,用的是新的线程 3,当输出了“AsyncMethod1结束”后,异步方法才调用完成,其实就是用资源换性能。

 

如果你在异步代码运行时看一下cpu,会发现占用情况比同步方法执行时高多了,但是也不是线性增长的,因为资源总有上限,资源调度也需要成本。

而且异步方法有个很大的问题就是,不同线程的开始和结束是无序的,如果一套顺序的业务流程代码,用异步去执行,很可能乱掉,当然这也有解决方法,只是从异步多线程的原理来看,会无法预测执行顺序。

 

当我们需要在异步多线程方法中加入日志,通常不会去把写日志的方法加在业务操作的方法中,而是利用回调函数,以上例为例。

 private static void AsyncMethod2()
        {
            Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
            Action<string> action = DoSomething;
            AsyncCallback asyncCallback = (ar) =>
            {
                Console.WriteLine(ar.AsyncState.ToString()+":"+ Thread.CurrentThread.ManagedThreadId.ToString());
                if (ar.AsyncState.ToString() == "ok")
                    AddLog("张三");
            };
            action.BeginInvoke("张三", asyncCallback, "ok");//异步
            Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }

private static void AddLog(string name)
        {
            Console.WriteLine(name + "写日志:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }

Action的BeginInvoke的第二个和第三个参数分别是,回调函数,传入的参数。

运行结果是这样

 

 

 可以看到,主线程1执行完成后就释放了,线程3自己去执行Dosomething,并且在执行完后执行AddLog,这样既不会阻塞主线程,又可以监控到异步方法并写日志。

 

IAsyncResult 等待

可以利用BeginInvoke的返回值进行异步操作的进度判断,例如做上传文件的进度提示,异步业务等待提示等等。

改一下AsyncMethod2

private static void AsyncMethod2()
        {
            Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
            Action<string> action = DoSomething;
            AsyncCallback asyncCallback = (ar) =>
            {
                Console.WriteLine(ar.AsyncState.ToString()+":"+ Thread.CurrentThread.ManagedThreadId.ToString());
                if (ar.AsyncState.ToString() == "ok")
                    AddLog("张三");
            };
            IAsyncResult asyncResult = action.BeginInvoke("张三", asyncCallback, "ok");//异步
            while (!asyncResult.IsCompleted)
            {
                Console.WriteLine("AsyncMethod2还在异步调用DoSomething哦!线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
                Thread.Sleep(100);//休息100ms,不然死循环了咋整
            }
            if (asyncResult.IsCompleted)
            {
                Console.WriteLine("AsyncMethod2异步调用DoSomething完成!线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
            }
            Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }

当DoSomething方法还没有执行结束时,循环输出“AsyncMenthod2还在异步调用....”,并且休眠100ms,执行完成后输出“AsyncMenthod2异步调用....完成”。

结果是这样

 

 

 这里的问题是, 等待完成的那个while循环的内部其实是由主线程1控制的,明显看到了1和3两个线程并行,直到3执行完1最后才执行完成,中间1其实一直被占用,并且不够精确,我在循环中设置了100ms的延迟,也就是说无论如何也有个100ms的延迟。

 

信号量WaitOne

private static void AsyncMethod2()
        {
            Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
            Action<string> action = DoSomething;
            AsyncCallback asyncCallback = (ar) =>
            {
                Console.WriteLine(ar.AsyncState.ToString()+":"+ Thread.CurrentThread.ManagedThreadId.ToString());
                if (ar.AsyncState.ToString() == "ok")
                    AddLog("张三");
            };
            IAsyncResult asyncResult = action.BeginInvoke("张三", asyncCallback, "ok");//异步

            #region 信号量,精确
            //发邮件
            //发短信
            //减库存
            //通知业务人员
            asyncResult.AsyncWaitHandle.WaitOne();
            #endregion

           

            Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }

这个例子的意思是,当我异步调用DoSomething的同时,我需要同时再去调用 发邮件,发短信等等别的方法,但是呢,我希望后续的方法要等我这个异步调用完成后再执行,于是用asyncResult.AsyncWaitHandle.WaitOne()来阻塞当前进程,接受到asyncResult发出的信号量才继续。

结果是这样,可以看到线程1是等待DoSomething完成后才完成的,而且跟回调方法无关,回调还是自己跑自己的,可以理解成,异步方法执行完,线程1剩下的代码和回调方法并发执行。

 

 但是这样也有个问题,如果异步方法出了问题,岂不是像同步方法一样,卡死在这里?可以给WaitOne加上参数,让线程1最多阻塞1000ms,1000ms内执行完成,继续走,没执行完成,也不管还是继续走。

asyncResult.AsyncWaitHandle.WaitOne(1000);

结果,因为我这个Dosomething方法执行时间超过了1000ms,所以可以看到线程1等待了1000ms后发现线程3的异步方法还没执行完,不等了,直接走它的,然后异步方法执行完成后,再执行回调函数。

 

 

Func 获得返回值

  了解Aciton的朋友会知道,Action委托没有返回值,如果需要返回值,可以用Func

  

private static void AsyncMethod2()
        {
            Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString());

            #region Func 有返回值
            Func<string, string> func = GetApi;
            IAsyncResult asyncResult = func.BeginInvoke("张三", null, null);
            string result = func.EndInvoke(asyncResult);
            Console.WriteLine("获取到返回值(" + result + ")线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
            #endregion

            Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
        }


private static string GetApi(string name)
        {
            Console.WriteLine("GetApi线程:" + Thread.CurrentThread.ManagedThreadId.ToString());
            long result = 0;
            for (long i = 0; i < 1000000000; i++)
            {
                result += i;
            }
            return "我是Api返回的值:" + name;
        }

结果

 

另外这个返回值也可以放到回调中去使用,例如这样,我改一下func

Func<string, string> func = GetApi;
            IAsyncResult asyncResult = func.BeginInvoke("张三", ar =>
            {
                AddLog(ar.AsyncState.ToString());
                func.EndInvoke(ar);
            }, "ok");

结果

 

 看,返回值在回调中获取,是线程3,而上例在回调外获取,是线程1。

需要注意的是,同一个委托的begininvoke,只能有一个endinvoke,你不能调用多次,否则报错。

 

posted @ 2021-01-02 20:39  luytest  阅读(228)  评论(0编辑  收藏  举报