2018/12/03 彻底搞懂同步异步
中午占坑,晚上补。
概念:
同步(synchronize):发送一个请求,等待返回,然后再发送下一个请求
异步(asynchronous):发送一个请求,不等待返回,随时可以再发送下一个请求
同步和异步的日常应用?用于帮助更直观地理解什么是同步什么是异步。
Ajax同步请求和异步请求。
浏览器给服务器发送同步请求,浏览器需要等待服务器回复后才可以继续执行,在得到回复之前它会一直等待,不能进行任何其他操作。所以,同步调用会让视图暂时卡死。
浏览器给服务器发送异步请求,浏览器主线程不需要等待回复(由异步请求的线程等待),这时候可以继续进行其它操作,待异步线程获得回复后,异步线程的回调方法再进行回复的处理。
当然,发送异步请求的不一定是主线程,也可能是子线程,所以使用异步请求不一定是为了解决界面等待卡顿,它可能就是为了不阻塞本线程的执行。
异步的实质原理?用于理解同步和异步的本质区别。
一直使用.net封装好的异步机制来创建异步请求,因为.net对异步封装了很多层,并且加入了await语法糖,所以在使用时,甚至连线程类都不需要接触,所以虽然用的很多,但一直对异步的原理和具体的执行细节不太明白,稀里糊涂,
很多情景不知道应不应该用,用了有没有用,今天自己手动实现一个底层的异步类,通过分析这个底层的异步类,可以直观地认识我们的异步到底做了什么,异步执行时经历了哪些步骤,执行顺序如何,以后就再也不怕异步啦!
以下是异步类的源码,就是一个AsyncHelper:(当然在实际开发中用线程池实现更合理)
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 var p = new Person(); 6 var asyc = new AsyncHelper(); 7 asyc.Callback += Asyc_Callback; 8 9 Console.WriteLine($"1: 线程Id-{Thread.CurrentThread.ManagedThreadId}"); 10 asyc.Start(() => { Console.WriteLine($"3: 线程Id-{Thread.CurrentThread.ManagedThreadId}"); }); 11 Console.WriteLine($"2: 线程Id-{Thread.CurrentThread.ManagedThreadId}"); 12 Console.Read(); 13 } 14 private static void Asyc_Callback(object sender, EventArgs e) 15 { 16 Console.WriteLine($"4: 线程Id-{Thread.CurrentThread.ManagedThreadId}"); 17 } 18 } 19 public class AsyncHelper 20 { 21 public event EventHandler Callback; 22 public void Start(Action action) 23 { 24 new Thread(() => 25 { 26 action?.Invoke(); 27 Callback?.Invoke(this, new EventArgs()); 28 Console.WriteLine($"5: 线程Id-{Thread.CurrentThread.ManagedThreadId}"); 29 }).Start(); 30 } 31 }
执行结果:
分析:
执行顺序:可以看到执行顺序为第一列 1-2-3-4-5
程序启动后,程序进程中只有一个主线程,所以在主线程中顺序执行代码,先执行了第9行,然后执行第10行,第10行做了什么呢?第10行的方法中创建了一个新线程并启动了该线程。
虽然主线程中创建了一个新线程并启动了它,但新线程并不会立即执行,因为主线程的时间片还没有执行完那,所以新线程处于就绪状态在就绪的线程队列中排队,而主线程继续执行,就先执行了第11行,
然后遇到console.Read(),主线程阻塞,等待输入,,,直到时间片用完了,就绪队列执行,轮到创建的新线程,新线程开始顺序执行它的代码段,所以程序开始执行第26行内的内容,然后27行,28行,结束,线程销毁,,,不关心。
所以,此处搞懂了为什么异步会直接返回,因为异步只是创建了线程啊,它的代码段还没有任何执行(一直以为立刻返回是客户端请求服务器后,服务器立刻给他一个返回,实际上客户端的异步调用仅在客户端创建了线程而已,请求还没有发起)。
执行线程:可以看到前面两步都是主线程Thread-1,后面三步都是新创建的子线程Thread-3执行
这个从代码中可以很直观地看出来,因为异步调用的方法和异步调用完成后回调的方法都是在新的线程执行的(既然是执行完后的回调,肯定是写在执行完之后啊,所以回调和异步请求在同一个线程上)。
所以,注意,异步调用的回调函数和异步方法在同一个线程中,即回调不是发生在主线程(所以在回调中直接调用窗体控件会报错)。
现在再来重新理解一下.net的异步语法糖await:
它的实现原理和上述基本相同,程序之所以遇到await会直接返回,是因为程序执行到await处,会自动创建一个异步类AsyncHelper,然后将await后的方法放到该类的线程中,await后面的代码段会放入类的回调函数中,
因此程序遇到await后可以直接返回函数调用处,异步方法处理完成后可以继续执行await后的代码段,,,就是这样,也不是很神奇对不对???但是一直在用await语法糖,谁知道它干了什么,总以为借助了什么神力。。。
理解这些之后,可以放心地使用await语法糖了,毕竟方便。
(补充:await后的代码段在回调中,虽然是在子线程执行,但可以通过在执行该处时委托给主线程执行,让await后的代码段与普通代码段无差别运行,比如可以直接更新视图,实际上,await就是这么干的)
以后遇到异步调用,就把它理解为在异步调用处仅仅创建了一个新线程,然后继续执行后面的代码,至于异步调用的方法何时执行,那就是新线程何时分配到时间片的问题了。
应用场景:
什么情况使用同步?
什么情况使用异步?
如果一个操作不耗时,可以直接使用同步,如果一个操作比较耗时,而又不想等待,可以使用异步处理。但是要注意,使用异步表示接下来的操作对异步的请求结果不敏感。
明天,彻底搞懂进程和线程。