新的C#异步特性有什么好的非网络示例?

20

微软刚刚宣布了新的C#异步特性。到目前为止我看到的所有例子都是关于从HTTP异步下载东西。肯定还有其他重要的异步任务吧?

假设我不是在写一个新的RSS客户端或Twitter应用程序。对我来说,C# Async有什么有趣之处呢?

编辑 当我观看Anders的PDC会议时,我突然有了灵感。过去我曾经参与过使用"监视器"线程编写的程序。这些线程等待某些事情发生,比如观察文件是否更改。它们并没有执行任务,只是空闲地等待,并在事件发生时通知主线程。在新模型中,这些线程可以被await/async代码所替代。


更普遍地说,异步何时有用?几乎任何一般的异步示例都应该与C# 5中所展示的内容相关。 - Larsenal
当你说“非联网”时,你是真的意味着“非 I/O”吗?因为实际上所有的 I/O 都可能会阻塞。 - Gabe
假设我正在处理来自网络的数据流,但是通过第三方终端,并且我不知道终端内部有什么或者它如何从网络获取数据,我只是使用它的dll,并且所有数据异步地以魔法般的方式出现在我的程序中。无需了解网络的任何信息。但是我需要了解async/await。 - Gennady Vanin Геннадий Ванин
8个回答

25

哦,这听起来很有趣。我现在还没有使用CTP,只是在审查白皮书。在看了Anders Hejlsberg的演讲之后,我认为我可以看出它如何能够证明其有用。

据我所知,async使编写异步调用更易于阅读和实现。就像现在编写迭代器一样容易(而不是手动编写功能)。这对于阻塞进程至关重要,因为在解除阻塞之前,不能执行任何有用的工作。如果您正在下载文件,则在获取该文件之前无法执行任何有用操作,浪费线程。考虑一下如何调用一个函数,您知道它将阻塞不确定的时间并返回一些结果,然后处理它(例如,在文件中存储结果)。你会怎么写呢?这里有一个简单的例子:

static object DoSomeBlockingOperation(object args)
{
    // block for 5 minutes
    Thread.Sleep(5 * 60 * 1000);

    return args;
}

static void ProcessTheResult(object result)
{
    Console.WriteLine(result);
}

static void CalculateAndProcess(object args)
{
    // let's calculate! (synchronously)
    object result = DoSomeBlockingOperation(args);

    // let's process!
    ProcessTheResult(result);
}

好的,我们已经实现了。但是等等,计算需要数分钟才能完成。如果我们想要一个交互式应用程序,并在计算进行时执行其他操作(如呈现UI),怎么办?这不好,因为我们同步调用函数,并且必须等待它完成,从而有效地冻结了应用程序,因为线程正在等待解除阻塞。

答案是,异步调用昂贵的函数。这样,我们不必等待阻塞操作完成。但是我们该如何做到这一点?我们会异步调用函数并注册回调函数,以便在解除阻塞时调用,以便我们可以处理结果。

static void CalculateAndProcessAsyncOld(object args)
{
    // obtain a delegate to call asynchronously
    Func<object, object> calculate = DoSomeBlockingOperation;

    // define the callback when the call completes so we can process afterwards
    AsyncCallback cb = ar =>
        {
            Func<object, object> calc = (Func<object, object>)ar.AsyncState;
            object result = calc.EndInvoke(ar);

            // let's process!
            ProcessTheResult(result);
        };

    // let's calculate! (asynchronously)
    calculate.BeginInvoke(args, cb, calculate);
}
  • 注意:我们可以启动另一个线程来执行此操作,但这意味着我们要生成一个仅等待被解除阻塞的线程,然后执行一些有用的工作。这将是浪费。

现在调用是异步的,我们不必担心等待计算完成和处理,它是异步完成的。它会在能够完成时完成。与直接异步调用代码的替代方法是使用任务(Task):

static void CalculateAndProcessAsyncTask(object args)
{
    // create a task
    Task<object> task = new Task<object>(DoSomeBlockingOperation, args);

    // define the callback when the call completes so we can process afterwards
    task.ContinueWith(t =>
        {
            // let's process!
            ProcessTheResult(t.Result);
        });

    // let's calculate! (asynchronously)
    task.Start();
}

现在我们已经异步调用了函数。但是,为了实现这一点,我们需要委托/任务能够异步调用它,需要一个回调函数来处理结果,然后再调用该函数。我们将一个两行的函数调用变成了更多的内容,只是为了异步调用某些东西。不仅如此,代码中的逻辑也比以前更复杂。虽然使用任务有助于简化过程,但我们仍然需要做一些工作才能实现它。我们只想异步运行,然后处理结果。为什么我们不能这样做呢?现在我们可以这样做:
// need to have an asynchronous version
static async Task<object> DoSomeBlockingOperationAsync(object args)
{
    //it is my understanding that async will take this method and convert it to a task automatically
    return DoSomeBlockingOperation(args);
}

static async void CalculateAndProcessAsyncNew(object args)
{
    // let's calculate! (asynchronously)
    object result = await DoSomeBlockingOperationAsync(args);

    // let's process!
    ProcessTheResult(result);
}

现在这只是一个简单的例子,包含简单的操作(计算、处理)。想象一下,如果每个操作不能方便地放入单独的函数中,而是有数百行代码。这会增加很多复杂性,仅为了获得异步调用的好处。

白皮书中提供了另一个实际的例子,即在UI应用程序中使用它。 修改为使用上面的示例:

private async void doCalculation_Click(object sender, RoutedEventArgs e) {
    doCalculation.IsEnabled = false;
    await DoSomeBlockingOperationAsync(GetArgs());
    doCalculation.IsEnabled = true;
}

如果您做过任何UI编程(无论是WinForms还是WPF),并尝试在处理程序中调用昂贵的函数,您会知道这很方便。使用后台工作线程来完成此操作并不会有太大帮助,因为后台线程将一直等待,直到可以工作。
假设您有一种控制某些外部设备的方法,比如打印机。如果设备出现故障,您希望能够重新启动它。自然而然地,打印机需要一些时间才能启动并准备好进行操作。您可能需要考虑重新启动无效并尝试再次重新启动。您别无选择,只能等待它。但是,如果您使用异步方式进行操作,则不必等待。
static async void RestartPrinter()
{
    Printer printer = GetPrinter();
    do
    {
        printer.Restart();

        printer = await printer.WaitUntilReadyAsync();

    } while (printer.HasFailed);
}

想象一下不使用异步操作编写循环。


我还有一个例子。假设你需要在一个函数中执行多个阻塞操作,并希望异步调用。你会选择什么?

static void DoOperationsAsyncOld()
{
    Task op1 = new Task(DoOperation1Async);
    op1.ContinueWith(t1 =>
    {
        Task op2 = new Task(DoOperation2Async);
        op2.ContinueWith(t2 =>
        {
            Task op3 = new Task(DoOperation3Async);
            op3.ContinueWith(t3 =>
            {
                DoQuickOperation();
            }
            op3.Start();
        }
        op2.Start();
    }
    op1.Start();
}

static async void DoOperationsAsyncNew()
{
    await DoOperation1Async();

    await DoOperation2Async();

    await DoOperation3Async();

    DoQuickOperation();
}

阅读白皮书,它实际上有很多实用的例子,如编写并行任务等。

我迫不及待地想开始在CTP或.NET 5.0最终发布时使用它。


我不确定使用async的语法是否完全正确,但你可以理解这个想法。 - Jeff Mercado
在UI方面,我认为使用BackgroundWorker比使用asyncawait更好。 - Brian
6
错误。async 不会启动任何线程,因此你的 DoSomeVeryExpensiveCalculation() 不会在后台执行。请记住,async 用于仅运行短时间然后等待某些东西的操作。如果您想在后台运行长时间的 CPU 密集型操作,则需要显式在后台执行该操作:static Task<object> DoSomeVeryExpensiveCalculationAsync(object args) { return Task.Run(() => DoSomeVeryExpensiveCalculation(args)); } - Daniel
新的语言支持很好,因为它允许您轻松地组合异步操作。它是用于组合而不是自动并行化。(您的doCalculation_Click是组合的一个很好的例子) - Daniel
@Daniel:我已经更新了答案。我必须承认,除了编写网络代码时,我现在很少使用异步调用。希望现在应该是令人满意的。 - Jeff Mercado

17

主要情况是任何涉及高延迟的情况。也就是说,在“请求结果”和“获取结果”之间有很多时间。网络请求是高延迟场景最明显的例子,其次是一般的I/O操作,然后是在另一个核心上CPU绑定的长时间计算。

但是,这项技术可能还可以与其他情况良好地结合。例如,考虑编写FPS游戏的逻辑脚本。假设你有一个按钮单击事件处理程序。当玩家点击按钮时,您希望播放两秒钟的警报声以警示敌人,然后打开门十秒钟。能不能说出类似这样的话:

button.Disable();
await siren.Activate(); 
await Delay(2000);
await siren.Deactivate();
await door.Open();
await Delay(10000);
await door.Close();
await Delay(1000);
button.Enable();
每个任务都在UI线程上排队,因此不会阻塞,每个任务完成后都会在正确的位置恢复单击处理程序。

9
我今天发现了另一个很好的用例:您可以等待用户交互。

例如,如果一个表单有一个按钮,可以打开另一个表单:

Form toolWindow;
async void button_Click(object sender, EventArgs e) {
  if (toolWindow != null) {
     toolWindow.Focus();
  } else {
     toolWindow = new Form();
     toolWindow.Show();
     await toolWindow.OnClosed();
     toolWindow = null;
  }
}

尽管如此,这并不比原来更简单

toolWindow.Closed += delegate { toolWindow = null; }

但我认为这很好地展示了await的作用。一旦事件处理程序中的代码变得非常复杂,await会使编程变得更加容易。想象一下用户需要点击一系列按钮:

async void ButtonSeries()
{
  for (int i = 0; i < 10; i++) {
    Button b = new Button();
    b.Text = i.ToString();
    this.Controls.Add(b);
    await b.OnClick();
    this.Controls.Remove(b);
  }
}

当然,你可以使用普通的事件处理程序来完成这个任务,但这需要你拆开循环并将其转换为更难理解的形式。

请记住,await 可以用于任何在未来某个时刻完成的事物。以下是扩展方法 Button.OnClick(),使上述内容正常工作:

public static AwaitableEvent OnClick(this Button button)
{
    return new AwaitableEvent(h => button.Click += h, h => button.Click -= h);
}
sealed class AwaitableEvent
{
    Action<EventHandler> register, deregister;
    public AwaitableEvent(Action<EventHandler> register, Action<EventHandler> deregister)
    {
        this.register = register;
        this.deregister = deregister;
    }
    public EventAwaiter GetAwaiter()
    {
        return new EventAwaiter(this);
    }
}
sealed class EventAwaiter
{
    AwaitableEvent e;
    public EventAwaiter(AwaitableEvent e) { this.e = e; }

    Action callback;

    public bool BeginAwait(Action callback)
    {
        this.callback = callback;
        e.register(Handler);
        return true;
    }
    public void Handler(object sender, EventArgs e)
    {
        callback();
    }
    public void EndAwait()
    {
        e.deregister(Handler);
    }
}

不幸的是,似乎无法直接将GetAwaiter()方法添加到EventHandler中(允许使用await button.Click;),因为该方法将不知道如何注册/注销该事件。 这有点模板化,但是可以重复使用AwaitableEvent类以处理所有事件(而不仅仅是UI)。通过进行一些小修改并添加一些泛型,您可以允许检索EventArgs:

MouseEventArgs e = await button.OnMouseDown();

我认为这对于一些更复杂的UI手势(如拖放、鼠标手势等)会很有用 - 不过你需要添加取消当前手势的支持。


4
在CTP中有一些示例和演示不使用网络,甚至有些不进行任何I/O操作。
它适用于所有已存在的多线程/并行问题领域。
异步和等待是一种新的(更容易的)方式来构建所有并行代码,无论是CPU密集型还是I/O密集型。最大的改进在于以前需要使用APM(IAsyncResult)模型或事件模型(BackgroundWorker,WebClient)的领域。我认为这就是为什么这些例子现在引领潮流的原因。

3

一个GUI时钟是一个很好的例子;假设你想画一个时钟,每秒更新一次显示的时间。从概念上讲,你需要编写

 while true do
    sleep for 1 second
    display the new time on the clock

使用 await(或 F# 异步)进行异步休眠,您可以编写此代码以在 UI 线程上以非阻塞方式运行。 http://lorgonblog.wordpress.com/2010/03/27/f-async-on-the-client-side/

2

async扩展在某些情况下非常有用,当你有一个异步操作时。异步操作具有明确的开始和完成。当异步操作完成时,它们可能具有结果或错误。(取消被视为一种特殊类型的错误)。

广义上来说,异步操作在三种情况下非常有用:

  • 保持用户界面的响应性。任何时候,当你有一个长时间运行的操作(无论是CPU限制还是I/O限制),请将其设置为异步操作。
  • 扩展你的服务器。在服务器端明智地使用异步操作可能有助于扩展你的服务器。例如,异步ASP.NET页面可以使用async操作。然而,这并不总是有效的;你需要首先评估你的可扩展性瓶颈。
  • 在库或共享代码中提供清洁的异步API。 async非常适合重用。
当你开始采用异步的方式做事情时,你会发现第三种情况变得更加普遍。异步代码最适合与其他异步代码一起使用,所以异步代码在代码库中会“增长”。
有几种并发类型,其中异步不是最好的工具:
- 并行化。并行算法可以使用多个核心(CPU、GPU、计算机)更快地解决问题。 - 异步事件。异步事件独立于程序而发生。它们通常没有“完成”状态。通常,程序将订阅异步事件流,接收一些更新,然后取消订阅。程序可以将订阅和取消订阅视为“开始”和“完成”,但实际的事件流永远不会真正停止。
并行操作最好使用PLINQ或Parallel来表达,因为它们有很多内置的支持,如分区、有限并发等。一个并行操作可以通过在ThreadPool线程中运行它(Task.Factory.StartNew)轻松地包装成一个可等待的操作。
异步事件不太适合映射到异步操作。一个问题是异步操作在完成时只有一个结果。异步事件可能有任意数量的更新。Rx是处理异步事件的自然语言。
从Rx事件流到异步操作有一些映射,但它们都不是所有情况下的理想选择。更自然的方式是消耗异步操作通过Rx,而不是反过来。在我看来,最好的方法是尽可能在你的库和低级代码中使用异步操作,如果你在某个时刻需要Rx,那么从那里开始就使用Rx。

0

这里可能是一个不好的例子,展示了如何不正确地使用新的异步特性(而不是编写新的RSS客户端或Twitter应用程序),即在虚拟方法调用中间进行重载。说实话,我不确定是否有任何方法可以创建超过一个重载点。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace AsyncText
{
    class Program
    {
        static void Main(string[] args)
        {
            Derived d = new Derived();

            TaskEx.Run(() => d.DoStuff()).Wait();

            System.Console.Read();
        }
        public class Base
        {
            protected string SomeData { get; set; }

            protected async Task DeferProcessing()
            {
                await TaskEx.Run(() => Thread.Sleep(1) );
                return;
            }
            public async virtual Task DoStuff() {
                Console.WriteLine("Begin Base");
                Console.WriteLine(SomeData);
                await DeferProcessing();
                Console.WriteLine("End Base");
                Console.WriteLine(SomeData);
            }
        }
        public class Derived : Base
        {
            public async override Task DoStuff()
            {
                Console.WriteLine("Begin Derived");
                SomeData = "Hello";
                var x = base.DoStuff();
                SomeData = "World";
                Console.WriteLine("Mid 1 Derived");
                await x;
                Console.WriteLine("EndDerived");
            }
        }
    }
}

输出结果为:

开始派生

开始基类

你好

中间 1 派生

结束基类

世界

结束派生

在某些继承层次结构(即使用命令模式)中,我偶尔会想要做这样的事情。


0

这里有一篇文章,介绍如何在涉及UI和多个操作的非网络场景中使用“async”语法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接