20

微软刚刚宣布了新的 C# Async 特性。到目前为止,我看到的每个示例都是关于从 HTTP 异步下载内容的。当然还有其他重要的异步事情?

假设我没有编写新的 RSS 客户端或 Twitter 应用程序。C# Async 对我来说有什么有趣的地方?

编辑我有一个啊哈!观看安德斯的 PDC 会议时的片刻。过去,我曾研究过使用“观察者”线程的程序。这些线程等待某事发生,例如等待文件更改。他们不做工作,他们只是闲着,当有事情发生时通知主线程。这些线程可以用新模型中的等待/异步代码替换。

4

8 回答 8

25

哦,这听起来很有趣。我还没有玩 CTP,只是查看白皮书。在看到Anders Hejlsberg 的演讲之后,我想我可以看到它是如何被证明是有用的。

据我了解,异步使编写异步调用更容易阅读和实现。以同样的方式编写迭代器现在更容易(而不是手动编写功能)。这是必不可少的阻塞过程,因为在解除阻塞之前无法完成任何有用的工作。如果您正在下载一个文件,那么您将无法做任何有用的事情,直到您获得该文件而让线程浪费。考虑如何调用一个您知道会阻塞未确定长度的函数并返回一些结果,然后对其进行处理(例如,将结果存储在文件中)。你会怎么写?这是一个简单的例子:

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);
}
  • 注意:当然我们可以启动另一个线程来执行此操作,但这意味着我们正在生成一个线程,它只是坐在那里等待解除阻塞,然后做一些有用的工作。那将是一种浪费。

现在调用是异步的,我们不必担心等待计算完成和处理,它是异步完成的。它会在可能的时候完成。作为直接异步调用代码的替代方法,您可以使用任务:

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 最终推出时开始使用它。

于 2010-10-28T23:50:35.467 回答
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 线程上排队,因此没有任何阻塞,每个任务在其作业完成后在正确的点恢复点击处理程序。

于 2010-10-31T15:15:05.727 回答
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(allowing await button.Click;),因为这样该方法将不知道如何注册/取消注册该事件。这有点样板,但 AwaitableEvent 类可以重用于所有事件(不仅仅是 UI)。稍作修改并添加一些泛型,您就可以允许检索 EventArgs:

MouseEventArgs e = await button.OnMouseDown();

我可以看到这对一些更复杂的 UI 手势(拖放、鼠标手势……)很有用——尽管您必须添加对取消当前手势的支持。

于 2010-10-30T11:39:15.590 回答
4

CTP 中有一些不使用网络的示例和演示,甚至还有一些不做任何 I/O 的示例和演示。

它确实适用于所有多线程/并行问题区域(已经存在)。

Async 和 Await 是一种新的(更简单的)结构化所有并行代码的方法,无论是 CPU 密集型还是 I/O 密集型。最大的改进是在 C#5 之前必须使用 APM (IAsyncResult) 模型或事件模型(BackgroundWorker、WebClient)的领域。我认为这就是为什么这些例子现在引领游行。

于 2010-10-30T18:59:39.223 回答
3

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

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

并且使用await(或使用 F# async)异步睡眠,您可以编写此代码以非阻塞方式在 UI 线程上运行。

http://lorgonblog.wordpress.com/2010/03/27/f-async-on-the-client-side/

于 2010-10-31T20:25:19.733 回答
2

当您进行异步操作时,这些async扩展在某些情况下很有用。异步操作有明确的开始完成。当异步操作完成时,它们可能会产生结果或错误。(取消被视为一种特殊的错误)。

异步操作在三种情况下很有用(广义而言):

  • 保持你的 UI 响应。任何时候你有一个长时间运行的操作(无论是 CPU-bound 还是 I/O-bound),让它异步。
  • 扩展您的服务器。在服务器端明智地使用异步操作可以帮助您的服务器扩展。例如,异步 ASP.NET 页面可以使用async操作。然而,这并不总是一场胜利。您需要首先评估您的可扩展性瓶颈。
  • 在库或共享代码中提供干净的异步 API。async可重用性极佳。

当你开始采用这种async做事方式时,你会发现第三种情况变得越来越普遍。async代码最适合与其他async代码一起使用,因此异步代码通过代码库“增长”。

有几种类型的并发async不是最好的工具:

  • 并行化。并行算法可以使用许多内核(CPU、GPU、计算机)来更快地解决问题。
  • 异步事件。异步事件一直发生,与您的程序无关。他们通常没有“完成”。通常,您的程序将订阅异步事件流,接收一些更新,然后取消订阅。您的程序可以将订阅取消订阅视为“开始”和“完成”,但实际的事件流永远不会真正停止。

并行操作最好使用 PLINQ 或 来表示Parallel,因为它们有很多对分区、有限并发等的内置支持。通过从ThreadPool线程 ( Task.Factory.StartNew) 运行并行操作,可以轻松地将其包装在可等待对象中。

异步事件不能很好地映射到异步操作。一个问题是异步操作在完成时只有一个结果。异步事件可以有任意数量的更新。Rx是处理异步事件的自然语言。

有一些从 Rx 事件流到异步操作的映射,但它们都不适合所有情况。通过 Rx 使用异步操作更自然,而不是相反。IMO,解决这个问题的最好方法是在你的库和低级代码中尽可能多地使用异步操作,如果你在某个时候需要 Rx,那么从那里开始使用 Rx。

于 2011-08-30T14:52:21.370 回答
0

here is an article about showing how to use the 'async' syntax in a non-networked scenario that involves UI and multiple actions.

于 2011-11-21T16:23:21.477 回答
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 派生

端基

世界

最终派生

对于某些继承层次结构(即使用命令模式),我发现自己偶尔想做这样的事情。

于 2011-05-23T10:10:38.317 回答