0%

真会C#?——异步编程3

Continuation

  • Continuation表示当Task结束操作时,继续执行其他的一些操作,它通常是通过回调的方式来实现的。当Task一结束,就会开始执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Program
{
static void Main(string[] args)
{
Task<int> task = Task.Run(() =>
Enumerable.Range(2, 3000000).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
TaskAwaiter<int> awaiter = task.GetAwaiter();
//OnCompleted()代表当task结束时运行
awaiter.OnCompleted(() =>
{
//获取Task的返回结果
var result = awaiter.GetResult();
Console.WriteLine(result);
});
Console.ReadLine();
}
}
  • 在task上调用GetAwaiter会返回一个awaiter对象,它的OnCompleted方法中的委托会在task结束或发生故障时执行。可以将Continuation附加到已经结束的task上面。
  • 其实使用task.Result也可以获取task的返回值,但使用GetResult的好处是,如果task发生故障,那么异常会被直接抛出,而不是包装在AggregateException里面,这样catch就不用判断异常的类型。

ContinueWith

  • 使用它的效果和使用awaiter.OnCompleted的效果一样。不同的是OnCompleted没有返回值,而ContinueWith返回原来Task实例,可以用它来附加更多的Continuation,但这样就必须直接处理AggregateException。它要求传入一个原来Task实例类型的参数的委托。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Program
{
static void Main(string[] args)
{
Task<int> task = Task.Run(() =>
Enumerable.Range(2, 3000000).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
var continueWith = task.ContinueWith(task =>
{
int result = task.Result;
Console.WriteLine(result);
});
Console.ReadLine();
}
}

TaskCompletionSource

  • TaskCompletionSource是创建Task的另外一种方式,它可以让你在稍后开始和结束的任意操作中创建Task,提供了一个可手动执行的“从属”Task,即可指示操作何时结束或发生故障
  • 一般推荐用它来创建IO-Bound类的工作,一是可以获得所有Task的优点,二是不需要在操作时阻塞线程。
  • 下面是TaskCompletionSource的简易源码和它的使用方法。Snipaste_2020-10-02_21-40-56
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Program
{
static void Main(string[] args)
{
// var tcs = new TaskCompletionSource<int>();
// new Thread(() =>
// {
// Thread.Sleep(5000);
// tcs.SetResult(42);
// })
// {
// IsBackground = true
// }.Start();
//
// var task = tcs.Task;
// Console.WriteLine(task.Result);
var task = Run(() =>
{
Thread.Sleep(5000);
return 42;
});
Console.WriteLine(task.Result);
}

//调用此方法相当于调用Task.Factory.StartNew
//并使用TaskCreationOptions.LongRunning选项来创建非线程池的线程
static Task<TResult> Run<TResult>(Func<TResult> func)
{
var tcs = new TaskCompletionSource<TResult>();
new Thread(() =>
{
try
{
tcs.SetResult(func());
}
catch (Exception e)
{
tcs.SetException(e);
}
}).Start();
return tcs.Task;
}
}
  • TaskCompletionSource的真正用处是用它创建Task并不占用线程。

  • 使用TaskCompletionSource自定义一个延迟函数,使用dotnet core新添加的Task.Delay可以达到相同的效果,但两者的区别是:一个是不会占用线程,它会等待Continuation开始后,才占用线程;而使用Task.Delay相当于异步版本的Thread.Sleep,它会并发地去执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Program
{
static void Main(string[] args)
{
//使用TaskCompletionSource来进行延迟并不会占用新线程
//5秒之后,Continuation开始的时候,才占用新线程
Delay(5000)
.GetAwaiter()
.OnCompleted(() => Console.WriteLine("Print After Delay."));
//Task.Delay相当于异步版本的Thread.Sleep,所以下面两个任务是并发进行的
Task.Delay(5000).GetAwaiter().OnCompleted(() => Console.WriteLine("Print After Task.Delay.GetAwaiter"));
Task.Delay(10000).ContinueWith(ant => Console.WriteLine("Print After Task.Delay.ContinueWith"));
Console.ReadKey();
}

static Task Delay(int milliseconds)
{
var tcs = new TaskCompletionSource<object>();
var timer = new Timer();
timer.Elapsed += delegate
{
timer.Dispose();
tcs.SetResult(null);
};
timer.Start();
return tcs.Task;
}
}

同步和异步。。。

并发 并行 同步 异步 多线程

  • 它们之间的区别:
    1. 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。其中两种并发关系分别是同步和互斥
    2. 互斥:进程间相互排斥的使用临界资源的现象,就叫互斥。
    3. 同步:进程之间的关系不是相互排斥临界资源的关系,而是相互依赖的关系。进一步的说明:就是前一个进程的输出作为后一个进程的输入,当第一个进程没有输出时第二个进程必须等待。具有同步关系的一组并发进程相互发送的信息称为消息或事件。其中并发又有伪并发和真并发,伪并发是指单核处理器的并发,真并发是指多核处理器的并发。
    4. 并行:在单处理器中多道程序设计系统中,进程被交替执行,表现出一种并发的外部特种;在多处理器系统中,进程不仅可以交替执行,而且可以重叠执行。在多处理器上的程序才可实现并行处理。从而可知,并行是针对多处理器而言的。并行是同时发生的多个并发事件,具有并发的含义,但并发不一定并行,也亦是说并发事件之间不一定要同一时刻发生。
    5. 多线程:多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码。多线程可以实现线程间的切换执行。
    6. 异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
  • 异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。

同步和异步

  • 同步操作会在返回调用者之前完成它的工作,即在没有得到响应之前会一直等待,直到完成工作,把响应返回给调用者。

  • 异步操作会在返回调用者之后完成它的(大部分)工作,即调用者调用之后,这个调用就直接返回了,没有返回结果(这时这个调用由task的Continuation负责),后面可以继续调用其他方法,而调用的结果会通过状态或通知来通知调用者。

    同步模型

    异步模型

  • 使用异步会启用并发,因为它的工作会与调用者并发执行,异步方法通常会很快就返回到调用者,所以也叫非阻塞方法。

  • 目前有以下几种异步方法:

    1. Thread.Start
    2. Task.Run
    3. Continuation

异步编程

  • 异步编程:就是将长时间运行的函数写成异步的;传统的方法是将长时间运行的函数写成同步的,然后按需引入并发
  • 异步编程相对于传统同步编程有两个好处,一是大大减少了代码量;二是IO-bound并发可不适用线程来实现,可提高可扩展性和执行效率。
  • 异步编程的用途是可编写高效处理大量并发IO的应用程序(服务器应用),它并不负责线程安全,而是关注执行效率,比如:每一个网络请求并不会消耗一个线程。
  • 以下两种操作建议使用异步编写:
    1. IO-bound和Computed-bound操作
    2. 执行时间超过50毫秒的操作
  • 另一方面过细粒度的并发可能会损害性能,因为异步操作也是开销。

异步函数

  • async和await关键字可以让我们写出简洁的异步代码;

await

  • await关键字简化了附加Continuation的过程,它的效果如下:
1
2
3
4
5
6
7
8
9
//简写形式
var result = await xxx;
yyy(x);
//相当于下面:
var awaiter = xxx.GetAwaiter();
awaiter.OnCompleted(()=>{
var result = awaiter.GetResult();
yyy(x);
})
  • 可以await什么?一般await的表达式返回的是一个task,但满足下列条件的任意对象都可以被await:

    1. 拥有GetAwaiter方法,他返回一个awaiter(实现了INotifyCompletion.OnCompleted接口)。
    2. 拥有GetResult方法。
    3. 一个bool类型的IsCompleted属性。
  • 注意:

    1. await都是和async一起使用的,await是在函数的调用前面加上的,表示它是一个Continuation,若在方法体中使用了await,就需要在方法返回值的前面加上async
    2. 被await标记的语句的返回值的类型必须是Task,可以使用Task<TResult>来指定Continuation的结果。

async

  • 使用了async修饰符的方法就是异步函数async修饰符会让编译器把await当作关键字而不是标识符,它只能应用于方法的返回值前(包括lambda表达式),用async修饰的方法可以返回voidTaskTask<TResult>
  • async修饰符对方法的签名或访问修饰符(public…)没有影响,它只会影响方法内部。所以在接口中使用async是没有意义的
  • 使用async来重载非async方法是合法的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Program
{
static async Task Main(string[] args)
{
await DisplayPrimesCountAsync();
}

static async Task DisplayPrimesCountAsync()
{
var result = await GetPrimesCountAsync(2, 1000000);
Console.WriteLine(result);
}

static Task<int> GetPrimesCountAsync(int start, int count)
{
return Task.Run(() =>
ParallelEnumerable.Range(start, count).Count(n =>
Enumerable.Range(2, (int) Math.Sqrt(n) - 1)
.All(i => n % i > 0)));
}
}

异步函数如何执行

  • 当遇到await表达式时,执行会迅速地返回给调用者,就像yield return一样,在返回前运行时会附加一个Continuation到await地task上,为保证task结束时,执行会跳回原方法,从停止地地方继续执行。
  • 如果发生故障,则异常会被重新抛出,如果正常运行,则task的返回值就会赋给await表达式。
  • Lambda表达式中也可以写成异步的形式,写法和异步函数类似。

学习资料:B站杨旭

-------------本文结束感谢您的阅读-------------