主页 > 互联网  > 

C#学习(十三)——多线程与异步


一、什么是线程

程序执行的最小单元 一次页面的渲染、一次点击事件的触发、一次数据库的访问、一次登录操作都可以看作是一个一个的进程

在一个进程中同时启用多个线程并行操作,就叫做多线程 由CPU来自动处理 线程有运行、阻塞、就绪三态

代码示例:

class Program { static void Main(string[] args) { Thread thread = new Thread(() => { Print1(); }); thread.Start(); for(int i = 0; i < 1000; i++) { Console.Write(0); } Console.Read(); } static void Print1() { for (int i = 0;i < 1000; i++) { Console.Write(1); } } }

运行结果为 可以看到,在结果中,0和1的输出是交织在一起的,原因为两个线程交替着被运行,不断反复直到结束。 另外一个常用操作为Sleep();让时间暂停,使得线程进入静默状态。

二、前台线程、后台线程与线程池托管

代码举例:

class Program { static void Main(string[] args) { Thread thread = new Thread(PrintHello); thread.Start(); Console.WriteLine("退出主程序"); } private static void PrintHello(object? obj) { while (true) { Thread.Sleep(1000); Console.WriteLine("Hello from PrintHello!"); } } }

运行会发现,即使主线程运行结束了,子线程依旧在持续运行;持续运行的子线程就称为前台线程。 一般来说,只有等待前台线程运行完毕后,程序才可以进行关闭。 与之对应的是 后台线程,可以通过thread.IsBackground = true;//切换为后台线程将前台 线程切换到后台线程,这样再次运行会发现,当主线程结束后,后台线程就会被强制结束。

一般来说,前台线程用于需要时间比较长的等待业务,比如监听客户端请求,而后台线程适用于时较短的业务比如执行客户端发来的请求,后台进程不会影响程序的终止。 托管在线程池中的线程全部为后台线程。 所有使用new Thread创建的线程默认均为前台线程。

三、线程池

示例代码

for(int i = 0;i < 100; i++) { ThreadPool.QueueUserWorkItem((o) => { Console.WriteLine($"循环次数{i} 线程id {Thread.CurrentThread.ManagedThreadId}"); }); }

可以看到执行结果出现了id重复的状况,原因就是线程池会重复使用已经完成的线程,极大节约硬件资源。 另外,可以看到,for循环有100次,但是从输出结果来看,只执行了十几次,原因为线程池创建的线程均为后台线程,只要主程序退出,线程池的后台线程就会被停止,而主程序main执行的时间很短,因此线程池内线程没有来得及执行就被停止了。

对于重要的并发量小的线程,需要手动创建管理,对于并发量大而又不太重要的线程,最好托管到线程池中。

四、结束线程与CancellationToken

不管程序有多少个进程,进程内部的资源都是被共享的。所以C#对进程的取消代码作了更高层次的抽象,把进程的取消过程封装成为了Token的形式,也就是CancellationToken(取消令牌)。不仅可以使用在多线程中,还可以用于异步操作。

class Program { static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Thread thread = new Thread(() => { PrintHello(cts.Token); }); thread.Start(); //下载文件 Thread.Sleep(5000); //关闭子进程 //cts.Cancel(); cts.CancelAfter(3000);//在下载完成后3s失效 Console.WriteLine("退出主程序"); } private static void PrintHello(CancellationToken tokenSource) { while (!tokenSource.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine("Hello from PrintHello!"); } } } 五、Join与IsAlive

对于子线程执行时间不确定的情况,需要使用Join的方法,加入至主程序执行中,或者使用IsAlive方法进行判断

class Program { static void Main(string[] args) { Thread thread = new Thread(() => { PrintHello(); }); thread.Start(); //方法一 //thread.Join(); //方法二 while(thread.IsAlive) { Console.WriteLine("子线程仍在工作"); Thread.Sleep(100); } Console.WriteLine("退出主程序"); } private static void PrintHello() { int i = 0; while (i++ < 10) { Thread.Sleep(new Random().Next(100, 1000)); Console.WriteLine("Hello from PrintHello!"); } } } 六、资源竞争与线程锁lock

使用线程可以并发的在CPU的核心中执行任务,最大化CPU的利用率,但是并发执行任务也可能产生各种各样的资源竞争问题。

举例:

private static void AddText() { File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(100); File.AppendAllText((@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}"); }

当两个线程同时需要使用同一个文件资源时,产生资源竞争,导致系统崩溃。 因此必须保证同一时刻只能有一个线程访问资源,避免出现资源恶性竞争。 使用线程锁就可以解决

class Program { static object lockedObj = new object(); static void Main(string[] args) { for(int i = 0; i < 10; i++) { var t = new Thread(AddText); t.Start(); } Console.WriteLine("退出主程序"); } private static void AddText() { lock(lockedObj) { File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(100); File.AppendAllText(@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}"); } } } 七、异步

在之前项目中,我们实现的所有操作都是同步进行的,然而当有同时10000个请求发生时,会使得用户有很长的等待,服务器会等待数据库的响应,完成后反馈至用户。 而异步操作要实现,不要等待数据库,继续执行下一个请求,当数据返回数据以后,再回头继续处理上一个请求。 然而对于更高级别的数量请求,仅仅依靠异步也是不够的,因此需要: 异步服务+每个机器多开进程+多个机器组合实现; K8s, Kubernetess容器化分布式部署; .NET Core对容器化非常非常友好、支持度极高

八、异步编程Task

我们使用异步处理并行,使用多线程处理并发。

异步逻辑是要基于方法没有依赖关系的,例如

class Program { static void Main(string[] args) { Calculate(); Console.Read(); } static void Calculate() { //Task->异步,Thread->线程 Task.Run(() => { Calculate1(); }); Task.Run(() => { Calculate2(); }); Task.Run(() => { Calculate3(); }); } static int Calculate1() { var result = 3; Console.WriteLine($"Calculate1: {result}"); Task.Delay(2000); return result; } static int Calculate2() { var result = 4; Console.WriteLine($"Calculate2: {result}"); Task.Delay(3000); return result; } static int Calculate3() { var result = 5; Console.WriteLine($"Calculate3: {result}"); Task.Delay(1000); return result; } }

但如果是具有依赖关系的,例如,Caculate2()需要Caculate1()的结果,Caculate3需要Caculate1()和Caculate2()的结果,那么就需要做如下调整

class Program { static void Main(string[] args) { Calculate(); Console.Read(); } static void Calculate() { //Task->异步,Thread->线程 var task1 = Task.Run(() => { return Calculate1(); }); var awaiter1 = task1.GetAwaiter();//获得异步等待对象 awaiter1.OnCompleted(() => { var result1 = awaiter1.GetResult();//获得异步逻辑的最终计算结果 var task2 = Task.Run(() => { return Calculate2(result1); }); var awaiter2 = task2.GetAwaiter(); awaiter2.OnCompleted(() => { var result2 = awaiter2.GetResult(); var result = Calculate3(result1, result2); Console.WriteLine(result); }); }); } static int Calculate1() { var result = 3; Console.WriteLine($"Calculate1: {result}"); Task.Delay(2000); return result; } static int Calculate2(int a) { var result = a * 2; Console.WriteLine($"Calculate2: {result}"); Task.Delay(3000); return result; } static int Calculate3(int a, int b) { var result = a + b; Console.WriteLine($"Calculate3: {result}"); Task.Delay(1000); return result; } } 九、C#的异步 async/await

可以看到,上面的异步操作代码非常复杂繁琐,接下来使用async/await化解上面操作

同步方法 指程序调用某个方法,需要等待执行完成以后才进行下一步操作 异步方法 指程序调用某个方法的时候,不做任何等待,在处理完成之前就返回该方法,继续执行接下来的操作,即函数在执行完成前就可以先返回调用方,然后继续执行接下来的逻辑完成任务的函数

举例:

public async Task<int> DoSomethingAsync() { //创建一个计算1万毫秒的任务 Task<int> longRunningTask = LongRunningTaskAsync(); //使用await执行这个任务 int result = await longRunningTask; return result; } //假装计算1w毫秒,输出为1 private async Task<int> LongRunningTaskAsync() { await Task.Delay(10000);//延迟10s return 1; }

1.需要使用async关键词 2.返回类型为:void、Task、Task<T> 和 IAsyncEnumerable<T> 3.命名规范:Async结尾 4.需要有await表达式 5.要有返回值 6.async函数只能被async函数调用

共有三个部分: 第一部分异步调用:Task<int> longRunningTask = LongRunningTaskAsync(); 第二部分执行异步:int result = await longRunningTask; 第三部分异步方法:private async Task<int> LongRunningTaskAsync(){}

注:

[ 在函数声明中,async关键字要放到返回类型之前 ][ async函数本身不创建异步操作,只有在调用await的时候才会进行异步操作 ]

下面对之前的异步代码进行优化:

class Program { static void Main(string[] args) { Calculate(); Console.Read(); } static async void Calculate() { var result1 = await Calculate1Async(); var result2 = await Calculate2Async(result1); var result = await Calculate3Async(result1, result2); Console.WriteLine(result); } static async Task<int> Calculate1Async() { var result = 3; Console.WriteLine($"Calculate1: {result}"); await Task.Delay(2000); return result; } static async Task<int> Calculate2Async(int a) { var result = a * 2; Console.WriteLine($"Calculate2: {result}"); await Task.Delay(3000); return result; } static async Task<int> Calculate3Async(int a, int b) { var result = a + b; Console.WriteLine($"Calculate3: {result}"); await Task.Delay(1000); return result; } } 十、Task VS. Thread

异步不是多线程!!! 异步用来处理并行,多线程用于处理并发

class Program { static void Main(string[] args) { TaskTest(); ThreadTest(); Console.Read(); } static void TaskTest() { var sw = new Stopwatch(); sw.Start(); for(int i = 0; i < 100; i++) { Task.Factory.StartNew(() => { }); } sw.Stop(); Console.WriteLine($"Task {sw.ElapsedMilliseconds}"); } static void ThreadTest() { var sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 100; i++) { new Thread(() => { }).Start(); } sw.Stop(); Console.WriteLine($"Thread {sw.ElapsedMilliseconds}"); } }

执行结果为 可以看到Task的执行速度要远高于Thread! 异步并不会创建线程,只是通过主线程来执行,同时开出一条分路来执行其他任务,非同步分别执行。但是在最后会创建一个非常轻量级的Worker Thread,用于通知主程序异步结束,也称为回调Call Back.

标签:

C#学习(十三)——多线程与异步由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“C#学习(十三)——多线程与异步