C#多线程总结
多线程的使用对于程序员来说是必不可少的一项技能,多线程会用的程序员很多,大部分程序员都不敢说自己玩的贼6,
比如博主自己,多线程玩得不6就需要不断充能。这次总结一下学习多线程的学习心得。
说单线程跟多线程之前先了解一下什么是并行,什么是并发,这两个概念一定得搞懂。
并行:多个任务并列进行。例如:五千米长跑,信号枪响后,运动员同时并列跑。
并发:CUP切片执行。例如:电风扇的扇叶的旋转,在其中一片叶子上做好标记,速度慢的时候可以看到哪片扇叶先到某个位置,
当旋转速度达到一定速度后就分不清顺序了,感觉多个扇叶到达某个位置的速度都是一样的。电脑同时运行QQ,运行vs 2019,
在写代码时候QQ可以同时接收消息,就是因为计算机执行速度足够快,CUP快速的调度,1秒中QQ跟vs 2019切换了上亿次,
给人感觉就是同时在运行。在微观的角度上,CPU的一个核只能执行一个任务,相当于只能运行一个程序,因为切换速度快,在宏观上就感觉不出来。
多线程应用场景非常广泛,那么它为什么就应用广泛呢?相当于单线程有何有点?我在这里举个栗子说说自己的理解。
举例:博主比较喜欢在看电影电视剧时吃点小零食喝点饮料,比如最近的庆余年特别火爆,博主也是甚是喜爱。
可以想象一下博主一边吃着炸鸡一边喝着啤酒看着带有喜感的王启年并翘着小脚丫的样子,还在加班的你们是不是有种想打死博主的冲动。
一边吃着炸鸡、喝啤酒、翘着脚丫子、看着电视剧都是在同一时间端同时进行的,那么这就是多线程。
吃完炸鸡才能喝啤酒,喝完啤酒才能翘脚丫子,翘完脚丫子才能看电视剧,这样一件事做完才能做下一件事的例子就是单线程。
在C#中,创建线程的方式有Thread、Action(委托)、ThreadPool(线程池)、Task、Parallel等多种方式,接下来就一样使用这几种方式来创建线程以及它们之间的区别。
1.使用Thread的类创建线程
Thread是CLR2.0(framework 2.0)才出现的。
public void Dosomething() { Console.WriteLine($"此处做点啥,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); } public void TheardEstablish() { { //framework2.0创建多线程的方式 //ThreadStart 是一个无参数的委托 //创建方式1: ThreadStart threadStart = this.Dosomething; Thread t = new Thread(threadStart); t.Start();//运行当前线程 //创建方式2: Thread thread = new Thread(() => { Console.WriteLine($"此处做点啥,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); thread.Start();//运行当前线程 thread.Join();//等待线程结束 } }
Console.WriteLine($"当前主线程start.....,线程ID:{Thread.CurrentThread.ManagedThreadId}"); Thread thread = new Thread(() => { Thread.Sleep(5000); Console.WriteLine($"Thread start.......,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); thread.Start();//运行当前线程 //thread.Join();//等待线程结束,主线程处于等待状态,界面会卡住 //Console.WriteLine($"Thread end.......,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); //不卡界面的方式,重新开启一个线程用于等待线程 Thread thread2 = new Thread(() => { thread.Join(); Console.WriteLine($"Thread end.......,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); thread2.Start(); Console.WriteLine($"当前主线程end.....,线程ID:{Thread.CurrentThread.ManagedThreadId}");
for (int i = 0; i < 20; i++) { int k = i;//范围变量 new Thread(() => { Console.WriteLine($"执行第{k}次,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }).Start(); }
2.Action(委托)创建线程
所有的线程都是基于委托实现的
//线程执行完成后的回调 AsyncCallback callback =(o)=> { Console.WriteLine($"Thread end.......,参数:{o.AsyncState},当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }; Action action = new Action(() => { Thread.Sleep(5000); Console.WriteLine($"Thread start.......,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); //action.Invoke();//等待,使用的是同步方式,主线程处于等待状态 //action.BeginInvoke(null, null);//不等待,异步方式,不阻塞 action.BeginInvoke(callback, "我是参数");//第二个是传给回调的参数,是个object类型
3.ThreadPool(线程池)创建线程
//ThreadPool.SetMaxThreads(4, 4);//设置当前应用程序支持线程并发时的最大数,最小是4 //使用线程池的方式创建线程可以更好的利于线程,创建的线程可以重复利于,性能相当于Thread创建的方式更好 //原理:一次性创建多个线程放到线程池中,使用线程池里面的线程去执行任务,任务执行完成后再把线程放回去,需要执行任务后先判断线程池有没有 //空闲的线程,如果没有会等待其它线程执行完成后再从线程池取空闲的线程来执行任务 for (int i = 0; i < 20; i++) { int k = i; ThreadPool.UnsafeQueueUserWorkItem((o) => { Thread.Sleep(500); Console.WriteLine($"执行操作00{k},参数:{o},当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }, $"我是参数{k}"); }
4.Parallel创建线程
//Parallel 创建线程执行任务时,如果主线程闲置也会参与执行任务 Parallel.Invoke(() => { Thread.Sleep(500); Console.WriteLine($"执行第1次,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }, () => { Thread.Sleep(500); Console.WriteLine($"执行第2次,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }, () => { Thread.Sleep(500); Console.WriteLine($"执行第3次,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }, () => { Thread.Sleep(500); Console.WriteLine($"执行第4次,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }, () => { Thread.Sleep(500); Console.WriteLine($"执行第5次,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); new Thread(() =>//这里包一层,可以不让主线程参与,可以解决占用主线程的问题。(没有什么问题是包一层解决不了的,解决不了再包一层) { int[] taskNums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };//设置任务数 var data = Parallel.For(1, taskNums.Length, new ParallelOptions() { MaxDegreeOfParallelism = 3//一次性执行任务最大数,可以控制并发的线程数,需要控制并发数的时候非常有用 }, (i) => { Thread.Sleep(1000); Console.WriteLine($"执行第{i}次,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); }).Start();
5.Task 创建线程
//Task task=Task.Run(() => //{ // Thread.Sleep(500); // Console.WriteLine($"开启一个线程任务,start.....,线程ID:{Thread.CurrentThread.ManagedThreadId}"); //}); //task.Wait();//主线程等待任务完成 //Task task = new Task(()=> // { // Thread.Sleep(500); // Console.WriteLine($"开启一个线程任务,start.....,线程ID:{Thread.CurrentThread.ManagedThreadId}"); // }); //task.Start();//启动线程 //task.Wait();//主线程等待任务完成 List<Task> tasks=new List<Task>(); TaskFactory taskFactory=new TaskFactory(); for (int i = 0; i < 20; i++) { int k = i; tasks.Add(taskFactory.StartNew(() => { Thread.Sleep(500); Console.WriteLine($"执行操作{k},当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); })); } //Task.WaitAll(tasks.ToArray());//等待所有任务执行完成 //Console.WriteLine("所有任务都执行完成了......"); int result =Task.WaitAny(tasks.ToArray());//等待其一任意一个任务完成成功后执行 Console.WriteLine($"任务{result}优先完成......");
以上就是多种创建线程的方式,接下来来了解线程内部出现异常如何处理。
为了了解线程内部异常如何处理先看如下代码:
List<Task> tasks = new List<Task>(); TaskFactory taskFactory = new TaskFactory(); try { for (int i = 0; i < 20; i++) { int k = i; tasks.Add(taskFactory.StartNew(() => { Thread.Sleep(500); if (k == 10 || k == 15) { throw new Exception($"线程{k}抛出了异常"); } Console.WriteLine($"执行操作{k},当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); })); } Task.WaitAll(tasks.ToArray());//等待任务全部执行完成 } catch (AggregateException e) { foreach (var ex in e.InnerExceptions) { Console.WriteLine($"异常信息:{ex.Message}"); } } catch (Exception e) { Console.WriteLine(e); }
线程内部异常可以通过,try catch 的方式捕获,但是当异常发生时后续任务都被执行了。接下来看优化后的代码:
List<Task> tasks = new List<Task>(); TaskFactory taskFactory = new TaskFactory(); CancellationTokenSource cts=new CancellationTokenSource(); try { for (int i = 0; i < 20; i++) { int k = i; tasks.Add(taskFactory.StartNew(() => { Thread.Sleep(500); if (k == 10 || k == 15) { cts.Cancel(); throw new Exception($"线程{k}抛出了异常"); } Console.WriteLine($"执行操作{k},当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }, cts.Token)); } Task.WaitAll(tasks.ToArray());//等待任务全部执行完成 } catch (AggregateException e) { foreach (var ex in e.InnerExceptions) { Console.WriteLine($"异常信息:{ex.Message}"); } } catch (Exception e) { Console.WriteLine(e); } finally { cts.Dispose(); }
以上结果就是我们需要的。
我们做异常处理最终的目的就是为了记录异常信息,日后好排查,其实最好处理就是让线程不抛异常,在线程内部做try catch 处理,如下图所示:
随着多线程技术使用得越来越广泛,当多个线程访问共享变量时就会存在线程安全问题,接下来就来了解如何处理线程安全问题。
为了更好的观察多线程安全问题,请看下面两个例子:
{ for (int i = 0; i < 10; i++) { Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); Task.Run(() => { Console.WriteLine($"正在执行第{i}次操作,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); } }
{ int num = 0; int num_1 = 0; for (int i = 0; i < 10000; i++) { num+=1; } List<Task> tasks = new List<Task>(); for (int i = 0; i < 10000; i++) { tasks.Add(Task.Run(() => { num_1+=1; })); } Task.WaitAll(tasks.ToArray());//等待所有线程执行完成 Console.WriteLine($"num的值为:{num},num_1的值为:{num_1}"); }
第一个案例出现多线程问题是因为for循环执行太快了,第一个Task.Run开始运行时候for循环已经走完了。
第二个案例出现的问题是因为多个线程操作同一变量,导致了内容被覆盖问题。
如何解决上面的问题,众所周知都认为可以是lock锁,被锁的内容只能由一个线程接入,操作完成后下一个线程才能进入。修改后的代码如下:
{ for (int i = 0; i < 10; i++) { int k = i; Task.Run(() => { Console.WriteLine($"正在执行第{k}次操作,当前线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); } } { int num = 0; int num_1 = 0; object lockObj=new object(); for (int i = 0; i < 10000; i++) { num+=1; } List<Task> tasks = new List<Task>(); for (int i = 0; i < 10000; i++) { tasks.Add(Task.Run(() => { lock (lockObj) { num_1 += 1; } })); } Task.WaitAll(tasks.ToArray());//等待所有线程执行完成 Console.WriteLine($"num的值为:{num},num_1的值为:{num_1}"); }
加lock锁就可以解决线程安全问题。
当然还有另外一种方式,就是使用C#内部的一些类型安全变量,这些类型安全变量在【System.Collections.Concurrent】下
使用lock锁有一些需要特别注意,lock括号内的值不能是null,string类型的值,(string 是一个特别的引用类型),不建议使用lock(this)这种写法(这是一个坑)
关于lock(this)写法之前遇到一个经典的面试题,代码如下:
public class LockDemo { private int num = 0; public void Test() { lock (this) { this.num++; if (this.num < 10) { Console.WriteLine($"当前次数为{this.num}次"); this.Test(); } else { Console.WriteLine("结束......"); } } } }
问执行Test方法是会不会出现死锁问题,当时不太清楚说会,正确的答案是不会,看着答案给我第一感觉是不是题目有问题,自己敲了一边代码,确实可以运行。最后的结论是线程自己不能锁住自己。
async/await 的使用
async/await是farmework 4.5出现的语法糖。如何使用如代码下:
public void TestDemoAsync() { Console.WriteLine($"当前主线程start.....,线程ID:{Thread.CurrentThread.ManagedThreadId}"); //AwaitDemo(); ThreadDemo(); Console.WriteLine($"当前主线程end.....,线程ID:{Thread.CurrentThread.ManagedThreadId}"); } /// <summary> /// /// </summary> public void ThreadDemo() { Task.Run(() => { Thread.Sleep(1000); Console.WriteLine($"dosomeThing .......,线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine($"在dosomeThing之后执行.....,线程ID:{Thread.CurrentThread.ManagedThreadId}"); } /// <summary> /// async 跟await 一般都是配套使用 /// </summary> /// <returns></returns> public async Task AwaitDemo() { await Task.Run(() => { Thread.Sleep(1000); Console.WriteLine($"dosomeThing .......,线程ID:{Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine($"在dosomeThing之后执行.....,线程ID:{Thread.CurrentThread.ManagedThreadId}"); }
分别执行AwaitDemo(),ThreadDemo()两个方法,执行结果如下
从执行结果可以看出,没有添加await关键字的Task.Run 后面一句代码是没有阻塞的,直接被主线程执行了,添加了await关键字的Task.Run后面一句代码被阻塞了,子线程执行完成后才由主线程执行。
从中可以总结出被await关键字修饰的代码,当主线程遇到await时直接跳过Task.Run后面的代码,当子线程执行完成后,主线程再执行Task.Run后面的代码,可以达到这种效果是因为底层使用了状态机这种机制。
以上就是学习多线程的一些总结,为以后复习提供参考价值。