当前位置:网站首页 / .NET CORE / 正文

ay的C#8.0和net5高级编程笔记13-使用多任务提高性能和可伸缩性

时间:2021年04月28日 | 作者 : aaronyang | 分类 : .NET CORE | 浏览: 982次 | 评论 0

使用多任务提高性能和可伸缩性

理解进程、线程和任务

进程拥有资源(内存+线程)线程逐句执行代码。默认 每个进程只有一个线程,执行多任务会导致问题。线程还负责跟踪当前经过身份验证的用户,以及语言和区域的规则。

Windows和大多其他OS使用了抢夺式多任务处理,从而模拟了并行执行任务。这种机制,可将CPU时间分配给各个线程,一个接一个地为每个线程分配时间片,当前线程在时间片结束时挂起,然后CPU允许另一个线程运行时间片。

Windows切换线程,会保存线程的上下文,并重新加载线程队列中先前保存的下一个线程的上下文。这需要时间和资源才能完成。

线程有 Priority和ThreadState属,此外还有ThreadPool类,用于管理后台工作线程。

更多: 链接

image.png

线程可能需要竞争并等待对共享资源的访问,比如变量、文件和数据库对象。

线程不是越多越好,要看你什么场景了。



sizeof() 测量类型的 大小

System.Diagnostics有很多监控代码的有用类型  

 比如Stopwatch统计耗时

    Restart()    将经过的的时间重置为0,然后启动计时器

    Stop() 停止

    Elapsed 时间存储为TimeSpan格式

    ElapsedMilliseconds 时间存储为 毫秒为单位的long类型

Process

    VirtualMemorySize64 显示为进程分配的虚拟内存量(字节为单位)

    WorkingSet64            显示为进程分配的物理内存量(字节为单位)


接下来新建一个Chapter13文件夹,然后打开vs2019开始新建一个MonitoringLib的net5类库,新建一个MonitoringApp的net5的控制台,控制台 引用 这个MonitoringLib库.不会新建的参考这篇博客

编写代码:测试10000个整数类型的数组

using System;
using static System.Console;
using static System.Diagnostics.Process;
using System.Diagnostics;

namespace MonitoringLib
{
    public static class Recorder
    {
        static Stopwatch timer = new Stopwatch();
        static long bytesPhysicalBefore = 0;
        static long bytesVirtualBefore = 0;

        public static void Start()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            bytesPhysicalBefore = GetCurrentProcess().WorkingSet64;
            bytesVirtualBefore = GetCurrentProcess().VirtualMemorySize64;
            timer.Start();
        }

        public static void Stop()
        {
            timer.Stop();
            long bytesPhysicalAfter = GetCurrentProcess().WorkingSet64;
            long bytesVirtualAfter = GetCurrentProcess().VirtualMemorySize64;
            Console.WriteLine("{0:N0} 物理内存字节使用了.",bytesPhysicalAfter-bytesPhysicalBefore);
            Console.WriteLine("{0:N0} 虚拟内存字节使用了." ,bytesVirtualAfter-bytesVirtualBefore);
            Console.WriteLine("{0} 时间",timer.Elapsed);
            Console.WriteLine("{0:N0} 毫秒", timer.ElapsedMilliseconds);
        }
        
    }
}

控制台  

===============www.ayjs.net=================

using MonitoringLib;
using System;
using System.Linq;

namespace MonitoringApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("处理中,请稍等...");
            Recorder.Start();

            var largeArray = Enumerable.Range(1, 10_000).ToArray();
            System.Threading.Thread.Sleep(new Random().Next(5,10)*1000);

            Recorder.Stop();
        }
    }
}

image.png


测试 50000个int,然后使用string和StringBuilder类 用逗号连接起来

客户端测试代码:

            #region DEMO2
            var number2 = Enumerable.Range(1, 50_000).ToArray();
            Recorder.Start();
            Console.WriteLine("使用string");
            string s="";
            for (int i = 0; i < number2.Length; i++)
            {
                s += number2[i] + ",";
            }
            Recorder.Stop();

            Recorder.Start();
            Console.WriteLine("使用stringbuilder");
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < number2.Length; i++)
            {
                sb.Append(number2[i]);
                sb.Append(",");
            }
            Recorder.Stop();
            #endregion


image.png

string的大约使用了14MB的物理内存和 31M的虚拟内存,耗时1.428秒

stringbuilder 大约8k物理内存,没有使用虚拟内存,耗时1.429毫秒,差距不大


异步执行任务

为了理解多个任务同时运行,控制台添加3个方法。

先看正常写法

   static void MethodA()
        {
            Console.WriteLine("执行方法A");
            Thread.Sleep(3000);
            Console.WriteLine("执行完毕A");
        }

        static void MethodB()
        {
            Console.WriteLine("执行方法B");
            Thread.Sleep(2000);
            Console.WriteLine("执行完毕B");
        }
        static void MethodC()
        {
            Console.WriteLine("执行方法C");
            Thread.Sleep(1000);
            Console.WriteLine("执行完毕C");
        }

执行:

    #region DEMO3
            var timer = Stopwatch.StartNew();

            //同步执行就是,一个一个的执行
            MethodA();
            MethodB();
            MethodC();

            Console.WriteLine("{0:N0} 毫秒", timer.ElapsedMilliseconds);

            #endregion


image.png


关于Thread,.NET第一个版本就可以用呢

.NET Framework4.0 引入了Task,Task类是线程包装器,可以更容易的创建和管理线程。通过管理任务中包装的多个线程,可以实现代码的异步执行。

每个Task实例都有Status和CreationOptions属性,还有ContinueWith方法,该方法可以使用TaskContinuationOptions枚举进行自定义,也可以使用TaskFactory类进行管理

image.png

DEMO4

            #region DEMO4
            var timer4 = Stopwatch.StartNew();

            //异步执行就是,同时执行
            Task taskA = new Task(MethodA);
            taskA.Start();
            Task taskB = Task.Factory.StartNew(MethodB);
            Task taskC = Task.Run(MethodC);


            Console.WriteLine("{0:N0} 毫秒", timer4.ElapsedMilliseconds);

            #endregion

            Console.ReadKey();

image.png

如果不加ReadKey()代码,控制台到最后一行,程序就退出了。

更多信息

你不知道啥时候执行完的


等待任务

有时候 需要等待任务完成后才能继续。为此,可以对Task实例 调用Wait方法,或者对任务数组调用WaitAll或者WaitAny静态方法

t.Wait() 等待名为t的Task实例完成执行

Task.WaitAny(Task[]) 等待数组中的任何任务完成执行

Task.WaitAll(Task[]) 等待数组中的所有任务完成执行


测试代码:

           #region DEMO5
            var timer5 = Stopwatch.StartNew();
            Task taskA = new Task(MethodA);
            taskA.Start();
            Task taskB = Task.Factory.StartNew(MethodB);
            Task taskC = Task.Run(MethodC);
            Task[] tasks = { taskA , taskB, taskC};
            Task.WaitAll(tasks);


            Console.WriteLine("{0:N0} 毫秒", timer5.ElapsedMilliseconds);

            #endregion

image.png

这个不需要最后的ReadKey()代码了,C先执行完。

这3个线程同时执行他们的代码,并且任意顺序启动。实际使用的CPU对结果有很大的影响。


继续执行另一项任务

考虑到其中一个任务通常依赖于另一个任务的输出,为了处理以上场景,需要定义延续任务。

创建一个方法模拟web调用,返回一个金额,第一个方法返回的结果需要传入第二个方法的输入。

    static decimal CallWeb()
        {
            Console.WriteLine("开始调用web服务");
            Thread.Sleep((new Random()).Next(2000, 4000));
            Console.WriteLine("结束调用web服务");
            return 199.98M;
        }
        static string CallStoreProcedure(decimal amout)
        {
            Console.WriteLine("开始调用CallStoreProcedure");
            Thread.Sleep((new Random()).Next(2000, 4000));
            Console.WriteLine("结束调用CallStoreProcedure");
            return "有12个产品大于"+amout+"金额";
        }

测试代码:

   #region DEMO6
            var timer6 = Stopwatch.StartNew();
            var taskcall = Task.Factory.StartNew(CallWeb).ContinueWith(x => CallStoreProcedure(x.Result));
            Console.WriteLine($"结果:{taskcall.Result}");


            Console.WriteLine("{0:N0} 毫秒", timer6.ElapsedMilliseconds);

            #endregion

image.png


嵌套任务和子任务

嵌套任务是  在另一个任务中创建的任务。

子任务是 必须在允许父任务完成之前完成的嵌套任务。


创建两个方法,其中一个方法用来启动一个任务以运行另一个任务。

           #region DEMO7
            var timer7 = Stopwatch.StartNew();
            var outerTask = Task.Factory.StartNew(OuterMethod);
            outerTask.Wait();
            Console.WriteLine("{0:N0} 毫秒", timer7.ElapsedMilliseconds);

            #endregion


        }

        static void OuterMethod()
        {
            Console.WriteLine("外方法开始执行...");
            Task t = Task.Factory.StartNew(InnerMethod);
            Console.WriteLine("外方法执行结束");
        }
        static void InnerMethod()
        {
            Console.WriteLine("嵌套法开始执行...");
            Thread.Sleep(2000);
            Console.WriteLine("嵌套方法执行结束");
        }

image.png

注意,虽然要等待外部的任务完成,但 内部任务不必也完成。实际上,外部任务可能已经完成,内部都没完成,就结束了。


修改OuterMethod代码如下:

      static void OuterMethod()
        {
            Console.WriteLine("外方法开始执行...");
            Task t = Task.Factory.StartNew(InnerMethod,TaskCreationOptions.AttachedToParent);
            Console.WriteLine("外方法执行结束");
        }

查看结果,注意内部的任务必须在外部任务可以完成之前完成

image.png

里面的任务没有完成,外面的任务必须等待里面的完成才结束



同步访问共享资源

多个线程 访问同一个变量或资源,会导致问题,就要考虑线程安全。

实现线程安全的最简单机制,是使用对象变量作为标志或指示灯,以指示共享资源何时应用了独占锁。


介绍两个可用于同步访问资源的类型

Monitor 用于防止多个线程在同一进程中同时访问资源的标志

Interlocked 用于在CPU级别操作简单数字类型的对象



从多个线程访问资源

新建两个方法,分别异步执行,然后共同抢Message这个变量资源

    static Random r = new Random();
        static string Message;
        static void MethodSyncA()
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(2000);
                Message += "A";
                Console.Write(".");
            }

        }
        static void MethodSyncB()
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(r.Next(2000));
                Message += "B";
                Console.Write(".");
            }

        }
   #region DEMO8
            Console.WriteLine("请等待...");
            Stopwatch stopwatch8 = Stopwatch.StartNew();

            Task ta = Task.Factory.StartNew(MethodSyncA);
            Task tb = Task.Factory.StartNew(MethodSyncB);

            Task.WaitAll(new Task[] { ta, tb });
            Console.WriteLine();
            Console.WriteLine($"{Message}");
            Console.WriteLine("{0:N0} 毫秒", stopwatch8.ElapsedMilliseconds);

            #endregion

image.png

接下来


 应用互斥锁可以防止并发访问

分别在MethodSyncA方法和MethodSyncB方法的for循环外加lock语句

  static object conch = new object();

        static void MethodSyncA()
        {
            lock (conch)
            {
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(2000);
                    Message += "A";
                    Console.Write(".");
                }
            }


        }
        static void MethodSyncB()
        {
            lock (conch)
            {
                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(r.Next(2000));
                    Message += "B";
                    Console.Write(".");
                }
            }

        }


image.png


理解lock语句并避免死锁

死锁往往发生在有两个或者多个共享资源时

线程X锁定conchA

线程Y锁定conchB

线程X试图锁定conchB,但被阻塞,因为线程Y已经锁定了conchB

线程Y试图锁定conchA,但被阻塞,因为线程X已经锁定了conchA


防止死锁的有效办法是在 尝试获取锁时指定超时,为此,必须手动使用Monitor类而不是使用lock语句。

修改代码:


image.png

结果跟上一个demo的效果差不多,但是避免了潜在的死锁

只有在能够编写 避免潜在死锁的代码时才使用lock关键字。如果无法避免,则始终使用Monitor.TryEnter语句并且结合 try-finally,这样就可以提供超时,如果出现死锁,其中一个线程就会退出死锁。


===============www.ayjs.net=================



使CPU操作原子化

C#的++不是原子的。递增一个整数需要执行以下三个操作:

1 将值从实例变量加载到寄存器中

2 增加值

3 将值存储在实例变量中


执行前两个操作后,线程可能被抢占。然后,另一个线程可以执行所有这三个操作。当第一个线程继续执行时,将覆盖实例变量的值,第二个线程执行的的增减效果将丢失!

名为Interlocked的类型可以用来对值类型执行原子操作,比如整数和浮点数。

声明另一个共享资源Counter,用于计算发生了多少操作

image.png

然后在for语句内部,在修改字符串之后,安全地增加计数器。

    static int Counter;

        static Random r = new Random();
        static string Message;

        static object conch = new object();

        static void MethodSyncA()
        {
            try
            {
                Monitor.TryEnter(conch, TimeSpan.FromSeconds(15));

                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(2000);
                    Message += "A";
                    Interlocked.Increment(ref Counter);
                    Console.Write(".");
                }

            }
            finally
            {
                Monitor.Exit(conch);
            }



        }
        static void MethodSyncB()
        {
            try
            {
                Monitor.TryEnter(conch, TimeSpan.FromSeconds(15));

                for (int i = 0; i < 5; i++)
                {
                    Thread.Sleep(r.Next(2000));
                    Message += "B";
                    Interlocked.Increment(ref Counter);
                    Console.Write(".");

                }
            }
            finally
            {
                Monitor.Exit(conch);
            }

        }

输出

C#

效果:

image.png

输出了10,增减效果没有错误,换成++尝试

image.png

也没错误,只是说可能会错误

因为在conch保护了由conch锁定所在的代码块中访问的所有共享资源,因此用++运算都可以,没必要用Interlocked



应用其他类型的同步

我的其他博客: 连接链接1


Monitor和Interlocked时互斥锁,他们简单有效,还有更高级的类型

ReaderWriterLockReaderWriterLockSlim

以读取模式运行多个线程,其中一个线程允许写入模式运行,并且独占锁的所有权;而另一个线程允许以 可升级的读取模式进行读取访问,在这个线程中,可以升级到写入模式,而不必放弃对资源的读取访问权限。

Mutex

与Monitor一样,提供对共享资源的独占访问,但也可用于进程间同步

SemaphoreSemaohoreSlim   链接

通过定义插槽来限制可以i并发访问资源或资源池的线程数量

AutoResetEventManualResetEvent

事件等待句柄允许线程通过相互发送信息和等待彼此的信号来同步活动



理解async和await

C#5 引入了两个关键字简化Task类型的使用

async和await

先理解为什么要引入这两个,后面16章和20章实践

 提高控制应用程序的响应能力

控制台存在的限制,只能在标记为async的方法中使用await关键字,C#7及更早版本不允许把Main方法标记为async,C#7.1就支持了

image.png

新建一个AsyncConsole

using System;
using System.Net.Http;
using static System.Console;
using System.Threading.Tasks;

namespace AsyncConsole
{
    class Program
    {
        static async Task Main(string[] args)
        {

            var client = new HttpClient();
            HttpResponseMessage res = await client.GetAsync("http://www.baidu.com");
            WriteLine("百度网站首页有{0}字节",res.Content.Headers.ContentLength);

        }
    }
}

image.png


改进GUI应用程序的响应能力

构建Web应用程序,web服务和带有GUI的应用程序(WPF或者 xamarin等),程序员的工作会变得更加复杂

原因之一,对于GUI应用程序,有个特殊的界面(UI)线程

在GUI工作时两条规则:

    不要在UI线程上执行长时间运行的任务

    除UI线程外,不要在任何线程上访问UI元素

所以编写代码就比较多,在C#5后续版本,使用async和await关键字。他们允许继续编写代码,就像代码是同步的一样,代码更简洁,但是在底层,C#编译器创建了复杂的状态机,并跟踪正在运行的线程。


DbContext<T>  AddAsync AddRangeAsync FindAsync和SaveChangeAsync

DbSet<T>    AddAsync AddRangeAsync    ForEachAsync    SumAsync    ToListAsync    ToDictionaryAsync    AverageAsync    CountAsync

HttpClient    GetAsync    PostAsync    PutAsync    DeleteAsync    SendAsync

StreamReader ReadAsync    ReadLineAsync    ReadToEndAsync

StreamWriter    WriteAsync    WriteLineAsync    FlushAsync

每当看到一个以Async结尾的方法时,就检查这个方法是否返回Task或者Task<T>,如果是,那么应该使用这个方法而不是使用不以Async作为后缀的方法,记住使用await关键字调用该方法,并使用async关键字进行修饰。


在C#5中,只能在try块中使用await关键字,不能在catch块中使用,在C#6后续版本,在try和catch块都可以使用await关键字。


在C#8.0和NC3.0之前,await关键字只能用于返回标量值的任务。

NS2.1支持的异步流允许async方法返回值的序列。

using System;
using System.Net.Http;
using static System.Console;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace AsyncConsole
{
    class Program
    {
        static async Task Main(string[] args)
        {

            //var client = new HttpClient();
            //HttpResponseMessage res = await client.GetAsync("http://www.baidu.com");
            //WriteLine("百度网站首页有{0}字节",res.Content.Headers.ContentLength);

            await foreach (int number in GetNumbers())
            {
                Console.WriteLine($"Number:{number}");
            }
        }

        static async IAsyncEnumerable<int> GetNumbers()
        {
            var r = new Random();
            System.Threading.Thread.Sleep(r.Next(1000, 2000));
            yield return r.Next(0,101);

            System.Threading.Thread.Sleep(r.Next(1000, 2000));
            yield return r.Next(0, 101);

            System.Threading.Thread.Sleep(r.Next(1000, 2000));
            yield return r.Next(0, 101);
        }

    }
}


image.png


线程和线程化:https://docs.microsoft.com/zh-cn/dotnet/standard/threading/threads-and-threading

深度理解async关键字:https://docs.microsoft.com/zh-cn/dotnet/standard/async-in-depth

await关键字:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/await

.NET并行编程;https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/

同步概率:https://docs.microsoft.com/zh-cn/dotnet/standard/threading/overview-of-synchronization-primitives


我自己的并行编程博客链接:  链接1   链接2

===============www.ayjs.net=================

















推荐您阅读更多有关于“C#8.0core3,”的文章

猜你喜欢

额 本文暂时没人评论 来添加一个吧

发表评论

必填

选填

选填

必填

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

  查看权限

抖音:wpfui 工作wpf

目前在合肥企迈科技公司上班,加我QQ私聊

2023年11月网站停运,将搬到CSDN上

AYUI8全源码 Github地址:前往获取

杨洋(AaronYang简称AY,安徽六安人)AY唯一QQ:875556003和AY交流

高中学历,2010年开始web开发,2015年1月17日开始学习WPF

声明:AYUI7个人与商用免费,源码可购买。部分DEMO不免费

查看捐赠

AYUI7.X MVC教程 更新如下:

第一课 第二课 程序加密教程

标签列表