WPF桌面端开发-异步任务Task的取消

Task

Task 类的表示单个操作不返回一个值,通常以异步方式执行。

Task 对象是一个的中心思想 基于任务的异步模式 首次引入.NET Framework 4 中。 因为由执行工作 Task 对象通常以异步方式执行在线程池线程上而不是以同步方式在主应用程序线程,可以使用 Status 属性,以及 IsCanceled, ,IsCompleted, ,和 IsFaulted 属性,以确定任务的状态。

ThreadPool相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。

比如:

  • ThreadPool不支持线程的取消、完成、失败通知等交互性操作;
  • ThreadPool不支持线程执行的先后次序;

以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,FCL中提供了一个功能更强大的概念:Task。

Task在线程池的基础上进行了优化,并提供了更多的API。

在FCL4.0中,如果我们要编写多线程程序,Task显然已经优于传统的方式。

创建任务

1
2
3
4
Task.Run(() => {Thread.Sleep(3000);});
Task.Factory.StartNew(() => {Thread.Sleep(3000);});
new Task(() => {Thread.Sleep(3000);}).Start();
Task.Run(() => {Thread.Sleep(3000);}).ContinueWith(t => { });

示例

1
2
3
4
5
6
Console.WriteLine("线程ID-1:" + Thread.CurrentThread.ManagedThreadId.ToString());
Task.Run(() => {
Console.WriteLine("线程ID-2:"+Thread.CurrentThread.ManagedThreadId.ToString());
}).ContinueWith(t => {
Console.WriteLine("线程ID-3:" + Thread.CurrentThread.ManagedThreadId.ToString());
});

注意

1
2
3
线程ID-1:1
线程ID-2:4
线程ID-3:4

Task.Factory.StartNew()Task.Run() 都可以用于启动任务并将其调度到线程池中可用的线程上执行,但它们在一些方面有所不同。

首先,Task.Run() 是使用起来更简单的方法,它的 API 设计更加简洁明了。它的返回值是一个 Task 对象,该对象表示异步操作的状态和结果。而 Task.Factory.StartNew() 需要传递一个 Func<object, TResult> 委托,并返回一个 Task<TResult> 对象。这增加了代码的复杂度,不够简洁。

其次,Task.Run() 默认情况下使用 TaskScheduler 中的 ThreadPoolTaskScheduler 来调度任务。它还针对常见情况进行了优化,例如长时间运行但不阻塞线程的操作,它会尝试在当前线程池中的线程上运行任务,以充分利用线程池中的线程资源,并避免产生额外的线程调度开销。而 Task.Factory.StartNew() 则没有提供这种优化。

最后,Task.Factory.StartNew() 还可以提供更丰富的选项来控制任务的行为,例如设置任务的调度优先级、取消标记、异常处理等。

Task.Run() 不提供这些选项,因为它的设计目的是提供一种简单且易于使用的方法来启动任务。

总体来说,如果你只需要启动一个简单的任务并将其调度到线程池中执行,Task.Run() 是一个不错的选择。

而如果你需要更多的控制和选项来管理任务的执行行为,或需要自定义调度器来控制任务的调度行为,那么 Task.Factory.StartNew() 可能更适合你的需求。

同步延时及异步延时

同步延时,卡界面

1
2
3
4
5
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
Thread.Sleep(2000);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

异步延时,不卡界面

1
2
3
4
5
6
7
Stopwatch stopwatch2 = new Stopwatch();
stopwatch.Start();
Task.Delay(2000)
.ContinueWith(t => {
stopwatch.Stop();
Console.WriteLine(stopwatch2.ElapsedMilliseconds);
});

同步+异步延时,不卡界面

1
2
3
4
5
6
7
Stopwatch stopwatch3 = new Stopwatch();
stopwatch.Start();
Task.Run(() => {
Thread.Sleep(2000);
stopwatch.Stop();
Console.WriteLine(stopwatch3.ElapsedMilliseconds);
});

Task+async/await

官网文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

异步方法旨在成为非阻止操作。 异步方法中的 await 表达式在等待的任务正在运行时不会阻止当前线程。 相反,表达式在继续时注册方法的其余部分并将控件返回到异步方法的调用方。

asyncawait 关键字不会创建其他线程。 因为异步方法不会在其自身线程上运行,因此它不需要多线程(这里不是说不需要多线程,而是异步的方法本来就要用其他线程)。 只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。 可以使用 Task.Run 将占用大量 CPU 的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。

对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。 具体而言,此方法比 BackgroundWorker 类更适用于 I/O 绑定操作,因为此代码更简单且无需防止争用条件。 结合 Task.Run 方法使用时,异步编程比 BackgroundWorker 更适用于 CPU 绑定操作,因为异步编程将运行代码的协调细节与 Task.Run 传输至线程池的工作区分开来。

异步控制流的跟踪导航

img

异步对可能会被屏蔽的活动(如 Web 访问)至关重要。 对 Web 资源的访问有时很慢或会延迟。 如果此类活动在同步过程中被屏蔽,整个应用必须等待。 在异步过程中,应用程序可继续执行不依赖 Web 资源的其他工作,直至潜在阻止任务完成。

下表显示了异步编程提高响应能力的典型区域。 列出的 .NET 和 Windows 运行时 API 包含支持异步编程的方法。

应用程序区域 包含异步方法的 .NET 类型 包含异步方法的 Windows 运行时类型
Web 访问 HttpClient Windows.Web.Http.HttpClient SyndicationClient
使用文件 JsonSerializer StreamReader StreamWriter XmlReader XmlWriter StorageFile
使用图像 MediaCapture BitmapEncoder BitmapDecoder

由于所有与用户界面相关的活动通常共享一个线程,因此,异步对访问 UI 线程的应用程序来说尤为重要。 如果任何进程在同步应用程序中受阻,则所有进程都将受阻。 你的应用程序停止响应,因此,你可能在其等待过程中认为它已经失败。

使用异步方法时,应用程序将继续响应 UI。 例如,你可以调整窗口的大小或最小化窗口;如果你不希望等待应用程序结束,则可以将其关闭。

当设计异步操作时,该基于异步的方法将自动传输的等效对象添加到可从中选择的选项列表中。 开发人员只需要投入较少的工作量即可使你获取传统异步编程的所有优点。

async异步方法的返回类型

异步函数的返回类型只能为: void、Task、Task

  • Task<TResult>: 代表一个返回值T类型的操作。

  • Task: 代表一个无返回值的操作。

  • void: 为了和传统的事件处理程序兼容而设计。

await(等待)

await等待的是什么? 可以是一个异步操作(Task)、亦或者是具备返回值的异步操作(Task<TResult>)的值 。

常见用法

1
2
3
4
5
6
7
8
9
10
private async Task<string> GetStr()
{
return await Task.Run(
() =>
{
Thread.Sleep(1000);
return "123";
}
);
}

代码示例

元素显示3秒后消失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private async void Button_Click_2(object sender, RoutedEventArgs e)
{
tip_outer.Visibility = Visibility.Visible;
new Thread(
o =>
{
int seconds = 3;//通知持续3s后消失
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(seconds));
//Invoke到主进程中去执行
Dispatcher.Invoke(
new Action(() =>
{
tip_outer.Visibility = Visibility.Hidden;
})
);
}
).Start();
}

就可以改为

1
2
3
4
5
6
7
private async void Button_Click_2(object sender, RoutedEventArgs e)
{
tip_outer.Visibility = Visibility.Visible;
Task task = Task.Run(() => { Thread.Sleep(3000); });
await task;
tip_outer.Visibility = Visibility.Hidden;
}

或者

1
2
3
4
5
6
private async void Button_Click_2(object sender, RoutedEventArgs e)
{
tip_outer.Visibility = Visibility.Visible;
await Task.Delay(3000);
tip_outer.Visibility = Visibility.Hidden;
}

要使用await这个功能,必须是在.NET 4以后的版本才可以用,如果是之前的版本是没法用的。

多个异步操作

下面的示例就是等多个异步都返回后再处理其他逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private async Task MyTestAsync()
{
Task<string> fun1 = MyFun1();
Task<string> fun2 = MyFun2();
await Task.WhenAll(fun1, fun2);
string str1 = fun1.Result;
string str2 = fun2.Result;
Console.WriteLine("str1:" + str1);
Console.WriteLine("str2:" + str2);
}

private async Task<string> MyFun1()
{
await Task.Delay(100);
return "123";
}

private async Task<string> MyFun2()
{
await Task.Delay(200);
return "456";
}

帮助理解

1
2
3
4
5
DateTime time0 = DateTime.Now;
await Task.Delay(5000);
await Task.Delay(1000);
await Task.Delay(2000);
Console.WriteLine(DateTime.Now - time0);

这里调用了3个Delay,总等待时间花了8秒。

1
2
3
4
5
6
DateTime time0 = DateTime.Now;
Task t = Task.Delay(5000);
await Task.Delay(1000);
await Task.Delay(2000);
await t;
Console.WriteLine(DateTime.Now - time0);

这里同样调用了3个Delay,但是总等待时间只花了5秒,因为拆分使用后任务做了统筹。

Task任务的取消

1
2
private CancellationTokenSource _cts;
_cts = new CancellationTokenSource();

取消监听

1
_cts.Token.Register(() => Console.WriteLine(@"Cancel Task."));

但是取消的时候,任务并不会取消,只是取消的对象标记为取消

1
2
3
4
5
6
7
8
9
// 延迟执行
try
{
await Task.Delay(3000, _cts.Token);
}
catch
{
return;
}

Task.Delay被取消的时候会抛出TaskCanceledException异常。

单次延迟任务取消

假如我们有这么一个需求,我们要在没有操作的3秒之后执行一个任务,一旦3秒内有操作就重新开始计时。

使用CancellationTokenSource

这种方式能实现,就是在控制台会显示TaskCanceledException异常,即使try-catch也会显示。

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
private CancellationTokenSource _cts;

private void ShowOuter()
{
_pageData.HideOuter = false;
_cts?.Cancel();
HideOuter();
}

private void HideOuter()
{
if (_pageData.IsStart)
{
_cts?.Cancel();
_cts = new CancellationTokenSource();
_cts.Token.Register(() => Console.WriteLine(@"Cancel Task."));
Task.Run(
async () =>
{
// 延迟执行
try
{
await Task.Delay(3000, _cts.Token);
}
catch
{
return;
}
Console.WriteLine(@"延迟任务执行");
Dispatcher.Invoke(() => { _pageData.HideOuter = true; });
}
);
}
}

使用Timer(推荐)

System.Timers.Timer中,Elapsed事件通常是在线程池线程上触发的。

这意味着Elapsed事件处理程序会在一个由线程池提供的线程上执行,而不是在应用程序的主线程上执行。

这样可以避免在主线程上执行长时间运行的操作而导致阻塞。

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
private System.Timers.Timer _mTimer;

private void ShowOuter()
{
_pageData.HideOuter = false;
HideOuter();
}

private void HideOuter()
{
if (!_pageData.IsStart)
{
return;
}
// 先关闭之前的
_mTimer?.Close();
// 创建一个Timer对象,间隔时间为2秒
_mTimer = new System.Timers.Timer(3000);
_mTimer.Elapsed += OnTimerElapsed;
_mTimer.AutoReset = false; // 设置为false,使得Timer只执行一次

// 启动计时器
_mTimer.Start();
}

private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
Dispatcher.Invoke(() => { _pageData.HideOuter = true; });
_mTimer.Dispose(); // 任务完成后释放Timer对象
}

多次执行任务的取消

下面适用于任务需要重复执行,中途会暂停继续和取消。

声明参数

1
2
3
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
ManualResetEvent resetEvent = new ManualResetEvent(true);

定义Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Task task = new Task(async () => {
while (true) {
if (token.IsCancellationRequested) {
return;
}

// 初始化为true时执行WaitOne不阻塞
resetEvent.WaitOne();

// 执行的任务.

// 模拟等待100ms
await Task.Delay(100);
}

}, token);

task.Start();

暂停Task

1
resetEvent.Reset();

继续Task

1
resetEvent.Set();

取消Task

1
tokenSource.Cancel();