同步 VS 异步
- 同步操作会返回调用者之前完成它的工作。
- 异步操作会在返回调用者之后去做它的大部分工作。
- 异步的方法为少见 , 会启用并发,因为它的工作会与调用者并行执行。
- 异步方法通常很快(立即) 就会返回调用者 , 所以叫非阻塞方法。
- 目前见到的大部分的异步方法都是通用目的:
Thread.Start Task.Run 可以将Continuation 附加到Task的方法。
什么是异步编程?
- 异步编程的原则是将长时间运行的函数写成异步的。
- 传统的做法是 将长时间的运行的函数写成同步,然后从新的线程或者Task 进行调用,从而按需引用。
- 上述异步方式的不同之处在于,他是从长时间运行函数的 内部启用并发,还有两点好处:
IO-bound 并发可不使用线程来实现。可以提高可拓展性和执行效率。 富客户端在worker 线程会使用更少的代码, 简化了线程安全性。
异步编程的两种用途
-
编写高效处理大量并发IO 的应用程序(典型的:服务器应用)
挑战并是线程安全(因为共享状态通常是最小化的), 而是执行效率,特别是网络请求并不会消耗一个线程。 -
在富客户端应用简化线程安全。 如果调用图中任何一个操作是是长时间运行, 那么整个call graph 必须运行在 worker 线程上, 以保证UI响应。
得到一个横跨多个方法的单一并发操作(颗粒度)。 需要为call graph 中每个方法考虑线程安全。 异步的call graph , 直到需要才开启一个线程,通常比较浅(IO-bound 操作完全不需要)
经验之谈:
- 为了获取上述好处,下列操作建议异步编程。
IO-bound 和 Compute-bound 操作。
执行操作时间超过了50ms。 - 另一边面过细的颗粒度会损害性能,因为异步操作也会有开销。
异步编程和Continuation
- Task 非常适合异步编程,因为他们支持Continuation (他对异步非常重要)
TaskCompletionSource 的例子。 - 对于 Compute-bound 方法,Task.Run 会初始化绑定线程并发。
把task 返回调用者 创建异步方法。
异步编程的区别:目标是在调用图比较浅的位置。
富客户端应用中,高级方法可以保留在UI线程和访问控制及共享状态上, 不会出现线程安全问题。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp4
{
class Program
{
static void Main(string[] args)
{
DisplayPrimeCount();
//Task.Run(()=> { DisplayPrimeCount(); });
Console.ReadKey();
}
static void DisplayPrimeCount()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(GetPrimesCount(i * 100000 + 2, (i + 1) * 100000 - 1) + " primes between " + (i * 100000) + " and "+((i + 1) * 100000 - 1));
}
Console.WriteLine("Done");
}
static int GetPrimesCount(int start, int count)
{
return ParallelEnumerable.Range(start, count).Count(Ret =>
Enumerable.Range(2, (int)Math.Sqrt(Ret) - 1).All(i => Ret % i > 0)
);
}
}
}
for text two
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp4
{
class Program
{
static void Main(string[] args)
{
// DisplayPrimeCount();
Task.Run(()=> { DisplayPrimeCount(); });
Console.ReadKey();
}
static void DisplayPrimeCount()
{
//for (int i = 0; i < 10; i++)
//{
// Console.WriteLine(GetPrimesCount(i * 100000 + 2, 1000000) + " primes between " + (i * 100000) + " and "+((i + 1) * 100000 - 1));
//}
for (int i = 0; i < 10; i++)
{
var awaiter = GetPrimesCountAsync(i * 100000 + 2, (i + 1) * 100000 - 1).GetAwaiter();
awaiter.OnCompleted(() => {
Console.WriteLine(awaiter.GetResult() + " primes between " + (i * 100000) + " and " + ((i + 1) * 100000 - 1));
});
}
Console.WriteLine("Done");
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task<int>.Run(()=> ParallelEnumerable.Range(start, count).Count(Ret =>
Enumerable.Range(2, (int)Math.Sqrt(Ret) - 1).All(i => Ret % i > 0)
));
}
}
}
语言对异步的支持是非常重要的
- 需要对task 的执行进行序列化, 例如 taskB 依赖于taskA 的执行结果,。
- 为此 , 必须在Continuation 内部触发下一次循环,
- async 和 await 对于不想复杂的实现异步是非常重要的,命令式循环结构不要和continuation 混合在一起, 应为他们依赖当前本地状态。
- 另一种实现, 函数式写法(linq查询) , 他们也是 响应式编程(rx)的基础。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp4
{
class Program
{
static void Main(string[] args)
{
// DisplayPrimeCount();
Task.Run(()=> { DisplayPrimeCount(); });
Console.ReadKey();
}
static void DisplayPrimeCount()
{
DisplayPrimeCountFrom(0);
}
static void DisplayPrimeCountFrom(int start)
{
TaskAwaiter<int> awaiter= GetPrimesCountAsync(start * 1000000 + 2, 1000000).GetAwaiter();
awaiter.OnCompleted(() => {
Console.WriteLine(awaiter.GetResult()+"\t"+start);
if (start++<10)
{
DisplayPrimeCountFrom(start);
}
});
}
static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task<int>.Run(()=> ParallelEnumerable.Range(start, count).Count(Ret =>
Enumerable.Range(2, (int)Math.Sqrt(Ret) - 1).All(i => Ret % i > 0)
));
}
}
}
异步函数
async和 await 关键字 可以让你写出和同步代码一样的简介异步代码。
awaiting
await 关键字简化了附加continuation 的过程。
其结构如下: var result = await expression; statement(s);
它的作用相当于:
var awaiter= expression . GetAwaiter();
awaiter.OnCompleted(()=>{
var result=awaiter.GetResult();
statemnet(S);
});
async 修饰符
- async 修饰符会让编译器把await 当做关键字而不是标识符(C#5.0 以前可能会使用await 作为修饰符)
- async修饰符 只能适用于方法(包括lambda表达式)。 该方法可以返回void task task
- async 修饰符对方法签名和pubic 元素数据没有影响(和 unsafe 一样) 他只会影响方法内部。
- 在接口内部使用async是没有任何意义的。 使用async 来重载非async 的方法是合法的(只要方法签名一致)使用async 修饰符的方法就是“异步函数”
异步方法如何执行
- 遇到await 表达式,执行(正常情况下) 会返回调用者,就像iterator里边的 yield return。 再返回前 运行时会附加一个continuation 从停止的地方继续执行。
- 如果发生故障,那么异常会重新抛出。如果一切正常,那么他的返回值就会赋值给await 表达式。
可以 await 什么?
- 你await 的表达式通常是一个task。
- 也可以满足下列条件的任意对象: 有GetAwaiter方法,他返回一个awaiter。 返回适合类型的GetResult方法。 有一个bool类型的IsCompleted属性。
捕获本地状态:
- await 表达式的最牛之处 就是他几乎可以出现在任何地方。
- 特别的, 在异步方法内,await表达式可以替换任何表达式。
除了lock 和 unsafe 上下文。
await 之后在哪个线程上执行
- await 表达式之后,编译器依赖于continuation(通过阿waiter模式) 来继续执行。
- 如果在富客户端应用的UI线程上,同步上下文会保证后续是在原线程上执行。否则, 就会在task 结束的线程上继续执行。
UI上的await
- 本例子中,只有GetPrimesCounAsync中的代码在worker 线程上执行,
- Go中的代码会租用UI线程上的时间。可以说:Go是在消息循环中 伪并发 的执行。也就是说:他和UI线程处理其他事件是穿插执行的。
- 因为这种伪并发, 唯一能发生强占的时刻就是在await 期间。 这其实简化了线程安全, 防止重复进入即可。
- 这种并发发生在调用栈较浅的地方(Task.Run 调用的代码)
- 为了从该模型获益,真正的并发代码要避免访问共享状态或UI控件。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
void Go()
{
for (int i = 0; i < 100; i++)
{
textBox1.Text += GetPrimesCount(i + 100000, 100000).ToString()+Environment.NewLine;
}
}
private int GetPrimesCount(int v1, int v2)
{
return ParallelEnumerable.Range(v1, v2).Count(ret =>
{
return Enumerable.Range(2, (int)Math.Sqrt(ret) - 1).All(n => ret % n > 0);
});
}
private void Form1_Load(object sender, EventArgs e)
{
button1.Click += (_sender, _e) => Go();
}
}
}
上面的例子是同步方法容易卡死UI主界面。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
async void Go()
{
button1.Enabled = false;
for (int i = 0; i < 100; i++)
{
textBox1.Text += await GetPrimesCountAsync(i + 1000000, 100000) + Environment.NewLine;
}
button1.Enabled = true;
}
private Task<int> GetPrimesCountAsync(int v1, int v2)
{
return Task<int>.Run(() => ParallelEnumerable.Range(v1, v2).Count(ret =>
{
return Enumerable.Range(2, (int)Math.Sqrt(ret) - 1).All(n => ret % n > 0);
}));
}
private void Form1_Load(object sender, EventArgs e)
{
button1.Click += (_sender, _e) => Go();
}
}
}
与颗粒度的并发相比
- 例如使用BackgroundWorker (例子, Task.Run)。 整个同步调用图都在worker 线程上。 必须在代码中到处使用Dispatcher.BeginInvoke
- 循环本身在worker 线程上。 引入 race condition 。 若实现取消和过程报告,会使得线程安全问题更容易发生,在方法中新添加任何的代码也是同样的效果。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
async void Go()
{
button1.BeginInvoke(new Action(() => { button1.Enabled = false; }));
for (int i = 0; i < 100; i++)
{
int retNu= await GetPrimesCountAsync(i + 1000000, 100000) ;
textBox1.BeginInvoke(new Action(() => { textBox1.Text += retNu + Environment.NewLine; }));
}
button1.BeginInvoke(new Action(() => { button1.Enabled = true; }));
}
private Task<int> GetPrimesCountAsync(int v1, int v2)
{
return Task<int>.Run(() => ParallelEnumerable.Range(v1, v2).Count(ret =>
{
return Enumerable.Range(2, (int)Math.Sqrt(ret) - 1).All(n => ret % n > 0);
}));
}
private void Form1_Load(object sender, EventArgs e)
{
button1.Click += (_sender, _e) =>Task.Run(()=> Go());
}
private void button1_Click(object sender, EventArgs e)
{
}
}
}