C# 多线程和异步 (转发)
C#多线程和异步(一)——基本概念和使用方法
阅读目录
一、多线程相关的基本概念
进程(Process):是系统中的一个基本概念。 一个正在运行的应用程序在操作系统中被视为一个进程,包含着一个运行程序所需要的资源,进程可以包括一个或多个线程 。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。
线程(Thread):是 进程中的基本执行单元,是操作系统分配CPU时间的基本单位 ,在进程入口执行的第一个线程被视为这个进程的 主线程 。
多线程能实现的基础:
1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,宏观角度来说是多线程并发 ,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是 多个CPU在同一时刻就可以运行多个线程 。
多线程的优点:
可以同时完成多个任务;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;可以设置每个任务的优先级以优化程序性能。
多线程的缺点:
1、 内存占用 线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多(每个线程都需要开辟堆栈空间,多线程时有时需要切换时间片)。
2、 管理协调 多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程,线程太多会导致控制太复杂。
3、 资源共享 线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
2.2 常用的属性和方法
属性名称 | 说明 |
---|---|
CurrentThread | 获取当前正在运行的线程。 |
ExecutionContext | 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 |
IsBackground | bool,指示某个线程是否为后台线程。 |
IsThreadPoolThread | bool,指示线程是否属于托管线程池。 |
ManagedThreadId | int,获取当前托管线程的唯一标识符。 |
Name | string,获取或设置线程的名称。 |
Priority |
获取或设置一个值,该值指示线程的调度优先级 。 Lowest<BelowNormal<Normal<AboveNormal<Highest |
ThreadState |
获取一个值,该值包含当前线程的状态。 Unstarted、Sleeping、Running 等 |
方法名称 | 说明 |
---|---|
GetDomain() | 返回当前线程正在其中运行的当前域。 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id。 |
Start() | 执行本线程。(不一定立即执行,只是标记为可以执行) |
Suspend() | 挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
Resume() | 继续运行已挂起的线程。 |
Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程。 |
Abort() | 终结线程 |
Join() | 阻塞调用线程,直到某个线程终止。 |
Sleep() | 把正在运行的线程挂起一段时间。 |
2.3 线程同步
所谓同步: 是指在某一时刻只有一个线程可以访问变量 。
c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,实际上是Monitor.Enter(obj),Monitor.Exit(obj)的语法糖。在c#中,lock的用法如下:
lock (obj) { dosomething... }
obj代表你希望锁定的对象,注意一下几点:
1. lock不能锁定空值 ,因为Null是不需要被释放的。 2. 不能锁定string类型 ,虽然它也是引用类型的。因为字符串类型被CLR“暂留”,这意味着整个程序中任何给定字符串都只有一个实例,具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。 3. 值类型不能被lock ,每次装箱后的对象都不一样 ,锁定时会报错 4 避免锁定public类型 如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。
推荐使用 private static readonly类型的对象,readonly是为了避免lock的代码块中修改对象,造成对象改变后锁失效。
2.4 跨线程访问
例子:点击测试按钮,给文本框赋值
代码如下:
1 private void myBtn_Click(object sender, EventArgs e) 2 { 3 Thread thread1 = new Thread(SetValue); 4 thread1.Start(); 5 6 } 7 private void SetValue() 8 { 9 for (int i = 0; i < 10000; i++) 10 { 11 this.myTxtBox.Text = i.ToString(); 12 } 13 }
执行代码会出现如下错误:
出现该错误的原因是:myTxtBox是由主线程创建的,thread1线程是另外一个线程,在.NET上执行的是托管代码, C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件
解决的方法:
public Form1() { InitializeComponent(); } //点击按钮开启一个新线程 private void myBtn_Click(object sender, EventArgs e) { Thread thread1 = new Thread(SetValues); thread1.IsBackground = true; thread1.Start(); } //新线程给文本框赋值 private void SetValues() { Action<int> setVal = (i) => { this.myTxtBox.Text = i.ToString(); }; for (int i = 0; i < 10000; i++) { this.myTxtBox.Invoke(setVal, i); } }
Invoke:在“拥有控件的基础窗口句柄的线程” 即在本例的主线程上执行委托,这样就不存在跨线程访问了 ,因此还是线程安全的。
参考文章:
[1] https://www.cnblogs.com/dotnet261010/p/6159984.html
[2] https://www.cnblogs.com/wwj1992/p/5976096.html
C#多线程和异步(二)——Task和async/await详解
阅读目录
一、什么是异步
同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。
异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。本篇主要介绍Task、async/await相关的内容,其他异步操作的方式会在下一篇介绍。
二、Task介绍
Task是在ThreadPool的基础上推出的,我们简单了解下ThreadPool。ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销,看一个ThreadPool的栗子吧
ThreadPool相对于Thread来说可以减少线程的创建,有效减小系统开销;但是ThreadPool不能控制线程的执行顺序,我们也不能获取线程池内线程取消/异常/完成的通知,即我们不能有效监控和控制线程池中的线程。
1 Task创建和运行
我们知道了ThreadPool的弊端:我们不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知。net4.0在ThreadPool的基础上推出了Task,Task拥有线程池的优点,同时也解决了使用线程池不易控制的弊端。
首先看一下怎么去创建并运行一个Task,Task的创建和执行方式有如下三种:
static void Main(string[] args) { //1.new方式实例化一个Task,需要通过Start方法启动 Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //2.Task.Factory.StartNew(Action action)创建和启动一个Task Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100); Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); //3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task Task task3 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("执行主线程!"); Console.ReadKey(); }
执行结果如下:
我们看到先打印"执行主线程",然后再打印各个任务,说明了Task不会阻塞主线程。上边的栗子Task都没有返回值,我们也可以创建有返回值的Task<TResult>,用法和没有返回值的基本一致,我们简单修改一下上边的栗子,代码如下:
static void Main(string[] args) { ////1.new方式实例化一个Task,需要通过Start方法启动 Task<string> task = new Task<string>(() => { return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}"; }); task.Start(); ////2.Task.Factory.StartNew(Func func)创建和启动一个Task Task<string> task2 =Task.Factory.StartNew<string>(() => { return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}"; }); ////3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task Task<string> task3= Task.Run<string>(() => { return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}"; }); Console.WriteLine("执行主线程!"); Console.WriteLine(task.Result); Console.WriteLine(task2.Result); Console.WriteLine(task3.Result); Console.ReadKey(); }
注意task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:
上边的所有栗子中Task的执行都是异步的,不会阻塞主线程。有些场景下我们想让Task同步执行怎么办呢?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(); }
执行结果如下:
2 Task的阻塞方法(Wait/WaitAll/WaitAny)
1 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方法后执行结果如下,实现了线程阻塞:
2 Task的Wait/WaitAny/WaitAll方法
2 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执行完毕就解除阻塞,看一个栗子:
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都执行完毕再执行主线程 //执行【task1.Wait();task2.Wait();】可以实现相同功能 Task.WaitAll(new Task[]{ task1,task2}); Console.WriteLine("主线程执行完毕!"); Console.ReadKey(); }
执行结果如下:
如果将栗子中的WaitAll换成WaitAny,那么任一task执行完毕就会解除线程阻塞,执行结果是:先打印【线程1执行完毕】,然后打印【主线程执行完毕】,最后打印【线程2执行完毕】
3 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(); }
执行结果如下,我们看到WhenAll/WhenAny方法不会阻塞主线程,当使用WhenAll方法时所有的task都执行完毕才会执行后续操作;如果把栗子中的WhenAll替换成WhenAny,则只要有一个线程执行完毕就会开始执行后续操作,这里不再演示。
上边的栗子也可以通过 Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction) 和 Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction) 来实现 ,修改上边栗子代码如下,执行结果不变。
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(); //通过TaskFactroy实现 Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) => { Thread.Sleep(100); Console.WriteLine("执行后续操作"); }); Console.WriteLine("主线程执行完毕!"); Console.ReadKey(); }
4 Task的任务取消(CancellationTokenSource)
1 Thread取消任务执行
在Task前我们执行任务采用的是Thread,Thread怎么取消任务呢?一般流程是:设置一个变量来控制任务是否停止,如设置一个变量isStop,然后线程轮询查看isStop,如果isStop为true就停止,代码如下:
static void Main(string[] args) { bool isStop = false; int index = 0; //开启一个线程执行任务 Thread th1 = new Thread(() => { while (!isStop) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次执行,线程运行中..."); } }); th1.Start(); //五秒后取消任务执行 Thread.Sleep(5000); isStop = true; Console.ReadKey(); }
2 Task取消任务执行
Task中有一个专门的类 CancellationTokenSource 来取消任务执行,还是使用上边的例子,我们修改代码如下,程序运行的效果不变。
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); int index = 0; //开启一个task执行任务 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次执行,线程运行中..."); } }); task1.Start(); //五秒后取消任务执行 Thread.Sleep(5000); //source.Cancel()方法请求取消任务,IsCancellationRequested会变成true source.Cancel(); Console.ReadKey(); }
CancellationTokenSource的功能不仅仅是取消任务执行,我们可以使用 source.CancelAfter(5000) 实现5秒后自动取消任务,也可以通过 source.Token.Register(Action action) 注册取消任务触发的回调函数,即任务被取消时注册的action会被执行。 看一个栗子:
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); //注册任务取消的事件 source.Token.Register(() => { Console.WriteLine("任务被取消后执行xx操作!"); }); int index = 0; //开启一个task执行任务 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次执行,线程运行中..."); } }); task1.Start(); //延时取消,效果等同于Thread.Sleep(5000);source.Cancel(); source.CancelAfter(5000); Console.ReadKey(); }
执行结果如下,第5次执行在取消回调后打印,这是因为,执行取消的时候第5次任务已经通过了while()判断,任务已经执行中了:
最后看上一篇跨线程的栗子,点击按钮启动一个任务,给tetxtbox赋值,我们把Thread改成Task,代码如下:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void mySetValueBtn_Click(object sender, EventArgs e) { Task.Run(() => { Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); }; for (int i = 0; i < 1000000; i++) { myTxtbox.Invoke(setValue,i); } }); } }
运行界面如下,赋值的task不会阻塞UI线程:
三、异步方法(async/await)
在C#5.0中出现的 async和await ,让异步编程变得更简单。我们看一个获取文件内容的栗子:
class Program { static void Main(string[] args) { string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result; //调用同步方法 //string content = GetContent(Environment.CurrentDirectory + @"/test.txt"); Console.WriteLine(content); Console.ReadKey(); } //异步读取文件内容 async static Task<string> GetContentAsync(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //ReadAync方法异步读取内容,不阻塞线程 Console.WriteLine("开始读取文件"); int len = await fs.ReadAsync(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } //同步读取文件内容 static string GetContent(string filename) { FileStream fs = new FileStream(filename, FileMode.Open); var bytes = new byte[fs.Length]; //Read方法同步读取内容,阻塞线程 int len = fs.Read(bytes, 0, bytes.Length); string result = Encoding.UTF8.GetString(bytes); return result; } }
test.txt内容是【hello world!】执行结果为:
上边的栗子也写出了同步读取的方式,将main函数中的注释去掉即可同步读取文件内容。我们可以看到异步读取代码和同步读取代码基本一致。async/await让异步编码变得更简单,我们可以像写同步代码一样去写异步代码。注意一个小问题:异步方法中方法签名返回值为Task<T>,代码中的返回值为T。上边栗子中GetContentAsync的签名返回值为Task<string>,而代码中返回值为string。牢记这一细节对我们分析异步代码很有帮助。
异步方法签名的返回值有以下三种:
① Task<T>:如果调用方法想通过调用异步方法获取一个T类型的返回值,那么签名必须为Task<TResult>;
② Task:如果调用方法不想通过异步方法获取一个值,仅仅想追踪异步方法的执行状态,那么我们可以设置异步方法签名的返回值为Task;
③ void:如果调用方法仅仅只是调用一下异步方法,不和异步方法做其他交互,我们可以设置异步方法签名的返回值为void,这种形式也叫做“调用并忘记”。
小结:到这里Task,async/await的简单使用已经基本结束了,一些高级特性等到工作遇到了再去研究。通过上边的介绍,我们知道async/await是基于Task的,而Task是对ThreadPool的封装改进,主要是为了更有效的控制线程池中的线程(ThreadPool中的线程,我们很难通过代码控制其执行顺序,任务延续和取消等等);ThreadPool基于Thread的,主要目的是减少Thread创建数量和管理Thread的成本。async/await Task是C#中更先进的,也是微软大力推广的特性,我们在开发中可以尝试使用Task来替代Thread/ThreadPool,处理本地IO和网络IO任务是尽量使用async/await来提高任务执行效率。
C#多线程和异步(三)——一些异步编程模式
一、任务并行库
任务并行库(Task Parallel Library)是BCL中的一个类库,极大地简化了并行编程,Parallel常用的方法有For/ForEach/Invoke三个静态方法。在C#中for/foreach循环使用十分普遍,如果迭代不依赖与上次迭代的结果时,把迭代放在 不同的处理器上并行处理 将很大地提高运行效率,Parallel.For和Parallel.ForEach就是为这个目的而设计的。
看一个Parallel.For/ForEach的栗子:
static void Main(string[] args) { //Parallel.For 计算0到6的平方 Parallel.For(1, 6, i => { Console.WriteLine($"{i}的平方是{i*i}"); }); //Parallel.ForEach 计算每个字符串的长度 string[] strs = { "We", "hold", "these", "truths" }; Parallel.ForEach(strs, i => Console.WriteLine($"{i}有{i.Length}个字节")); Console.ReadKey(); }
运行结果:
如果我们想并行执行多个任务,可以使用 Parallel.Invoke(Action[] actions) 方法,看一个栗子:
static void Main(string[] args) { Parallel.Invoke( () => { Console.WriteLine($"并行执行任务1,线程Id为{Thread.CurrentThread.ManagedThreadId}"); }, () => { Console.WriteLine($"并行执行任务2,线程Id为{Thread.CurrentThread.ManagedThreadId}"); } ); Console.ReadKey(); }
执行结果如下:
二、计时器(Timer)
计时器提供了一种 定期重复运行异步方法 的方式,当计时器到期后,系统从线程池中的线程上开启一个回调方法,把state作为参数,并开始运行。
Timer最常用的构造函数如下:
Timer(TimeCallback callback,object state,uint dueTime, uint period)
callback是一个返回值为void的委托,state为传入callback的参数,dueTime为第一次调用前的时间,period为两次调用的时间间隔
一个栗子:
1 class Program 2 { 3 int count = 0; 4 void Run(object state) 5 { 6 Console.WriteLine("{0},已经调用了{1}次了", state, ++count); 7 } 8 static void Main(string[] args) 9 { 10 Program p = new Program(); 11 //2000毫秒后开始调用,每次间隔1000毫秒 12 Timer timer = new Timer(p.Run, "hello", 2000, 1000); 13 Console.WriteLine("Timer start"); 14 15 Console.ReadLine(); 16 } 17 }
执行结果:
三、委托执行异步
委托执行异步是早期执行异步的一种方式,特别是早几年进行网络编程时用的比较多。现在我们完全可以使用更优秀的其他异步编程模式去替代它。有时候我们会查看早期的代码,我们在这里简单介绍下委托执行异步的方法。使用委托执行异步,使用的是引用方法,如果一个委托对象在调用列表中只有一个方法(这个方法就是引用方法),它就可以异步执行这个方法。委托类有两个方法 BeginIvoke和EndInvoke 。
BeginInvoke :执行BeginInvoke方法时,会线程池中获取一个独立线程来执行引用方法,并立即返回一个实现IAsyncResult接口的对象的(该对象包含了线程池中线程运行异步方法的状态),调用线程不阻塞,而引用方法在线程池的线程中并行执行。
EndInvoke : 获取异步方法调用返回的值,并释放资源,该方法把异步方法的返回值作为自己的返回值。
委托执行异步编程的3种模式:
等待一直到完成(wait-until-done):在发起了异步方法,原始线程执行到EndInvoke时就中断并且等异步方法完成完成后再继续。
轮询(polling):原始线程定期检查发起的线程是否完成(通过IAsyncResult.IsCompleted属性判断),如果没有则继续进行原始线程中的任务。
回调(callback):原始线程一直执行,无需等待或检查发起的线程是否完成,在发起的线程中的引用方法完成之后,发起线程会调用回调方法,由回调方法在调用EndInvoke之前处理异步方法的结果。
3.1 等待一直到完成模式
原始线程执行到EndInvoke,如果异步任务没有完成就一直等待
1 delegate int MyDel(int first,int second);//委托声明 2 class Program 3 { 4 static int Sum(int x, int y) 5 { 6 Thread.Sleep(1000); 7 return x + y; 8 } 9 static void Main(string[] args) 10 { 11 MyDel del = Sum; 12 //调用异步操作(第三个参数是回调函数,第四个参数是额外的值) 13 IAsyncResult iar = del.BeginInvoke(3, 5, null, null); 14 15 //doSomehing... 16 17 //执行EndInvoke,如果引用方法Sum没有执行完成,主线程就等待其完成 18 int result = del.EndInvoke(iar); 19 Console.WriteLine(result); 20 } 21 }
3.2 轮询模式
定期查询任务是否完成:
1 delegate int MyDel(int first,int second);//委托声明 2 class Program 3 { 4 static int Sum(int x, int y) 5 { 6 Thread.Sleep(1000); 7 return x + y; 8 } 9 static void Main(string[] args) 10 { 11 MyDel del = Sum; 12 IAsyncResult iar = del.BeginInvoke(3, 5, null, null); 13 14 //通过iar.IsCompleted定期查询完成状态 15 while (!iar.IsCompleted)//IsCompleted表示调用的异步操作是否完成 16 { 17 //doSomething 18 Thread.Sleep(300); 19 Console.WriteLine("no done"); 20 } 21 int result = del.EndInvoke(iar); 22 Console.WriteLine(result); 23 Console.ReadKey(); 24 } 25 }
3.3 回调模式
原始线程执行委托的BeginInvoke后就不管新线程的事了,委托中的引用方法执行完成后,在回调函数中获取结果并处理,执行委托的EndInvoke方法
1 delegate int MyDel(int first,int second);//委托声明 2 class Program 3 { 4 static int Sum(int x, int y) 5 { 6 Thread.Sleep(1000); 7 return x + y; 8 } 9 10 //回调方法的签名和返回值类型必须和AsyncCallBack委托类型一致 11 //输入参数为IAsyncResult,返回值是Void类型 12 static void CallWhenDone(IAsyncResult iar){ 13 AsyncResult ar = (AsyncResult)iar; 14 MyDel del = (MyDel)ar.AsyncDelegate; 15 int result = del.EndInvoke(iar); 16 Console.WriteLine("回调函数执行EndInvoke"); 17 Console.WriteLine("result:{0}", result); 18 Console.WriteLine("回调函数完成"); 19 } 20 21 static void Main(string[] args) 22 { 23 MyDel del = Sum; 24 //执行BeginInvoke方法后原始线程就不用管了,在自定义的回调函数(CallWhenDone)中执行EndInvoke方法 25 IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, null); 26 Console.WriteLine("开启新线程,异步任务完成后执行回调函数"); 27 //doSomething 28 Console.WriteLine("回调执行不阻塞原始线程"); 29 Console.ReadKey(); 30 } 31 }
执行结果:
还有一些其他的异步编程模式如BackgroundWorker等,这里不再过多介绍。
一点补充(Windbg)
1 cpu占用过高
我们使用多线程时有时会遇到cpu占用过高、内存爆满的情况,快速定位异常线程是多线程开发中必须熟悉的技能。cpu占用过高一般是由死循环造成的,看下边一个简单的栗子,Run方法内部有死循环,程序运行后会 占用大量的cpu资源:
namespace MyApp { class Program { static void Main(string[] args) { Run(); Run2(); Console.ReadKey(); } //死循环,会造成cpu内存占用过高 static void Run() { Thread th = new Thread(() => { while (true) { Console.WriteLine("hello windbg"); } }); th.Start(); } //不会占用太高的cpu资源 static void Run2() { Thread th = new Thread(() => { while (true) { Thread.Sleep(1000); Console.WriteLine("hello windbg2"); } }); th.Start(); } } }
程序运行后cpu资源占用过高,怎么去定位呢?这里采用Windbg简单演示cpu占用过高的异常定位,下载地址:Windbg下载。安装完成后,界面如下所示:
1.生成Dump文件
这里MyApp生成为x64位的Release版本,点击MyApp.exe文件运行,打开【任务管理器】,找到MyApp,右键选择【创建转储文件】即可生成dump文件。
2.Windbg分析dump文件
打开Windbg,选择【文件】->【Open dump file】->找到上一步生成的dump文件即可。
执行以下命令加载符号和sos库
.sympath SRV*c:\localsymbols*http://msdl.microsoft.com/download/symbols .reload
.load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.DLL
通过命令 !threads 查看线程:
死循环会长期占有cpu,通过 !runaway 查看各个线程的运行时间:
我们看到 4eac线程的运行时间最长,通过命令 ~~[4eac] ; !clrstack 查看线程堆栈信息:
我们看到异常定位在MyApp的Program类的第24行,查看我们的代码,找到这个位置,发现这里是一个while(true)死循环,定位结束。
2 内存爆满
内存爆满也是异常遇到的问题,如大量拼接字符串会占用较大的内存,看下边的一个栗子,程序代码如下:
class Program { static void Main(string[] args) { Console.WriteLine("开始执行.."); GetBigString(); Console.ReadKey(); } //大字符串拼接 static void GetBigString() { String str = ""; for (int i = 0; i < 10000000; i++) { str+=$"hello{i}"; } Console.WriteLine(str); } }
内存爆满最常见原因是大量创建某个类型的变量,问题定位方法和上边定位cpu占用高的定位差不多。首先生成dump文件,然后用Windbg打开,加载符号和sos库,然后执行 !dumpheap –stat 查看各个类型的数量和尺寸,我们看到string类型数量和占用的资源很多:
通过 !DumpHeap /d -mt 00007ff8878c74c0 查看当前的方法表,如下:
点开一个地址,具体内容如下:
通过字符串内容是【hello0hello1...】和string类型数量多、尺寸大,我们再去在代码中查找很容易定位到问题代码。
小结:Windbg可以查看clr级别内容,在开发中对我们优化代码和异常定位有不错的帮助,这里只是简单介绍Windbg的基本用法,有兴趣的小伙伴可以研究下官方教程。