什么是新的C#异步功能的非网络示例?

时间:2010-10-28 21:17:11

标签: c# c#-5.0 async-await

微软刚刚宣布了new C# Async feature。到目前为止,我见过的每个例子都是关于从HTTP异步下载的东西。当然还有其他重要的异步事物吗?

假设我没有编写新的RSS客户端或Twitter应用程序。 C#Async对我有什么好处?

编辑我有一个啊哈!看着Anders' PDC session的那一刻。在过去,我一直致力于使用“观察者”线程的程序。这些线程正在等待某些事情发生,比如观察要更改的文件。他们没有做工作,他们只是空闲,并在发生事情时通知主线程。这些线程可以用新模型中的等待/异步代码替换。

8 个答案:

答案 0 :(得分:25)

哦,这听起来很有趣。我还没有玩CTP,只是回顾一下白皮书。在看到Anders Hejlsberg's talk之后,我想我可以看到它是如何有用的。

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

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

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();
}

阅读whitepaper,它实际上有很多实际的例子,比如编写并行任务等等。

我迫不及待地想在CTP中开始玩这个游戏,或者当.NET 5.0最终解决这个问题时。

答案 1 :(得分: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线程上排队,因此没有任何阻塞,每个任务在作业完成后在正确的点恢复点击处理程序。

答案 2 :(得分: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手势(拖放,鼠标手势......)很有用 - 尽管你必须添加对取消当前手势的支持。

答案 3 :(得分:4)

CTP中有一些不使用网络的样本和演示,甚至一些不进行任何I / O的样本和演示。

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

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

答案 4 :(得分: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/

答案 5 :(得分:2)

当您进行异步操作时,async扩展名在某些情况下非常有用。异步操作具有明确的 start 完成。< / em>异步操作完成后,可能会出现结果或错误。 (取消被视为一种特殊的错误)。

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

  • 保持用户界面的响应能力。只要你有长时间运行的操作(无论是CPU绑定还是I / O绑定),都要使它异步。
  • 扩展您的服务器。在服务器端明智地使用异步操作可以帮助您的服务器扩展。例如,异步ASP.NET页面可以使用async操作。但是,这是not always a win;您需要首先评估可伸缩性瓶颈。
  • 在库或共享代码中提供干净的异步API。 async非常适合重复使用。

当你开始采用async做事方式时,你会发现第三种情况变得更加普遍。 async代码最适合其他async代码,因此异步代码在代码库中“增长”。

有两种类型的并发,其中async 不是最好的工具:

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

并行操作最好用PLINQ或Parallel表示,因为它们有很多内置的分区支持,有限的并发性等。并行操作可以很容易地通过运行它来等待包装。 ThreadPool帖子(Task.Factory.StartNew)。

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

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

答案 6 :(得分: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");
            }
        }
    }
}

输出是:

  

开始派生

     

开始基地

     

您好

     

Mid 1 Derived

     

结束基地

     

世界

     

EndDerived

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

答案 7 :(得分:0)

这里有一个article,介绍如何在涉及UI和多个操作的非联网场景中使用“异步”语法。