如何编写“等待”方法?


70

我终于寻找到了异步及的await关键字,我有种“搞定”,但所有的例子我见过的异步调用.NET Framework中的方法,例如这一次,它调用HttpClient.GetStringAsync()

我不太清楚的是这种方法中发生了什么,以及如何编写自己的“ awaitable”方法。它就像将要异步运行的代码包装在Task中并返回该代码一样简单吗?


2
或编写返回aTaskTask<T>当然的async方法。异步可以很好地组合。
乔恩·斯基特

2
我只是在学习C#。我正在使用GetStringAsync()查看相同的示例,尽管有经验丰富的Java多线程,但我和OP有着完全相同的问题。这是一个很大的问题,应该在MSDN文章中解决,否则该文章非常完整。
皮特2015年

Answers:


86

就这么简单

Task.Run(() => ExpensiveTask());

使其成为一种等待的方法:

public Task ExpensiveTaskAsync()
{
    return Task.Run(() => ExpensiveTask());
}

这里重要的是返回任务。该方法甚至不必标记为异步。(只需再读一点,便可以进入图片)

现在这可以称为

async public void DoStuff()
{
    PrepareExpensiveTask();
    await ExpensiveTaskAsync();
    UseResultsOfExpensiveTask();
}

请注意,此处的方法签名为async,因为该方法可能会将控制权返回给调用者,直到ExpensiveTaskAsync()返回为止。同样,在这种情况下,昂贵意味着像Web请求之类的耗时。要将繁重的计算发送到另一个线程,通常最好使用“旧”方法,即System.ComponentModel.BackgroundWorker用于GUI应用程序或System.Threading.Thread


17
正确的做法是,最自然,最常用的方法是编写返回Task或的方法Task<>。但是从技术上讲,您还可以编写一个返回的方法,YourOwnType前提是该方法YourOwnType具有一个公共的无参数非静态实例方法,GetAwaiter()该方法称为适当的返回类型(在其他位置查找详细信息)。所以await是有点像foreach,它的工作原理上,有一个合适的公共方法的任何类型。
Jeppe Stig Nielsen 2014年

1
很高兴知道!尽管如果您决定使用这种方法,可能对代码的可读性没有帮助。
Janis F

2
@JeppeStigNielsenforeach要求IEnumerableawait当接口更适合该语言时,我会感到失望,因为它使用鸭子类型。它“供编译器使用”的事实是一个不好的借口。
2014年

2
@Gusdor为什么,foreach不需要任何接口!只需尝试以下类:class ForeachMe { public StrangeType GetEnumerator() { return new StrangeType(); } } class StrangeType { public bool MoveNext() { return true; } public DateTime Current { get { return DateTime.Now; } } }使用它们,代码foreach (var x in new ForeachMe()) { Console.WriteLine(x); }就可以正常工作。
Jeppe Stig Nielsen

2
@Gusdor权威来源是官方C#语言规范中的foreach语句部分(旧版本)。在最新版本中找到相同的部分。这是明确规定的;该类型无需实现IEnumerableIEnumerable<>
2014年

15

我将如何编写自己的“等待”方法?它像包装要在a中异步运行的代码Task并将其返回一样简单吗?

那是一种选择,但是它很可能不是您想要做的,因为它实际上并没有为您提供异步代码的许多优点。有关更多详细信息,请参见Stephen Toub的“我应该为异步方法公开异步包装程序吗?”。

通常,方法是不可接受的,类型是不可接受的。如果你希望能够写类似await MyMethod(),则MyMethod()必须返回TaskTask<T>或自定义await能力类型。使用自定义类型是一种罕见的高级方案。使用Task,您有几种选择:

  • 使用async和编写方法await。这对于异步编写动作,但不能用于最内层await调用。
  • 创建Task对方法使用一个Task,像Task.Run()Task.FromAsync()
  • 使用TaskCompletionSource。这是最通用的方法,可用于await根据将来发生的任何事情创建有能力的方法。

13

...我将如何编写自己的“等待”方法。

返回aTask不是唯一的方法。您可以选择创建一个自定义的等待者(通过实现GetAwaiterINotifyCompletion),在这里可以很方便地阅读:“等待任何东西”。返回自定义等待者的.NET API示例:Task.Yield()Dispatcher.InvokeAsync

在这里这里有一些自定义侍者的帖子,例如:

// don't use this in production
public static class SwitchContext
{
    public static Awaiter Yield() { return new Awaiter(); }

    public struct Awaiter : System.Runtime.CompilerServices.INotifyCompletion
    {
        public Awaiter GetAwaiter() { return this; }

        public bool IsCompleted { get { return false; } }

        public void OnCompleted(Action continuation)
        {
            ThreadPool.QueueUserWorkItem((state) => ((Action)state)(), continuation);
        }

        public void GetResult() { }
    }
}

// ...

await SwitchContext.Yield();

2
// don't use this in production-为什么呢?
hypersw


我不能把它当作一个很好的“为什么”,而是为了我已经在链接中找到的一个具体细节,关于awaits中不可用的finallys及其后果,无论如何这些都不再适用。其余的全部都是投机性的,例如外观不好的代码(Task::StartNew每个部分而不是await YieldTo?,仅当您自己没有尝试过时),或者语义上不清楚(与ConfigureAwait(false)我假设的相反)。
hypersw

我想这全都取决于场景。如果在一个函数中有十几个上下文切换,它会有所帮助。如果您仅启动大型后台任务,那么您会更安全。
hypersw


5

只需将您的方法转换为Task。像@Romiox一样,我通常使用这种扩展方式:

public static partial class Ext
{
    #region Public Methods
    public static Task ToTask(Action action)
    {
        return Task.Run(action);
    }
    public static Task<T> ToTask<T>(Func<T> function)
    {
        return Task.Run(function);
    }
    public static async Task ToTaskAsync(Action action)
    {
        return await Task.Run(action);
    }
    public static async Task<T> ToTaskAsync<T>(Func<T> function)
    {
        return await Task.Run(function);
    }
    #endregion Public Methods
}

现在让我们说你有

void foo1()...

void foo2(int i1)...

int foo3()...

int foo4(int i1)...

...

然后,您可以像@Romiox一样声明您的[异步方法]

async Task foo1Async()
{
    return await Ext.ToTask(() => foo1());
}
async Task foo2Async(int i1)
{
    return await Ext.ToTask(() => foo2(i1));
}
async Task<int> foo3Async()
{
    return await Ext.ToTask(() => foo3());
}
async Task<int> foo4Async(int i1)
{
    return await Ext.ToTask(() => foo4(i1));
}

要么

async Task foo1Async()
{
    return await Ext.ToTaskAsync(() => foo1());
}
async Task foo2Async(int i1)
{
    return await Ext.ToTaskAsync(() => foo2(i1));
}
async Task<int> foo3Async()
{
    return await Ext.ToTaskAsync(() => foo3());
}
async Task<int> foo4Async(int i1)
{
    return await Ext.ToTaskAsync(() => foo4(i1));
}

...

现在,您可以使用async并等待任何fooAsync方法,例如foo4Async

async Task<int> TestAsync () {
    ///Initial Code
    int m = 3;
    ///Call the task
    var X = foo4Async(m);
    ///Between
    ///Do something while waiting comes here
    ///..
    var Result = await X;
    ///Final
    ///Some Code here
    return Result;
}

您的示例中的await关键字在哪里?
Jeson Martajaya

2

如果您不想使用Task,则可以编写一个完全定制的等待对象。此类对象是一种实现方法,该方法GetAwaiter ()返回一个对象实现INotifyCompletion,该对象可以是对象本身。

更多:INotifyCompletion

侍者实现:

  • IsCompleted 获得状态
  • GetResult () 得到结果
  • OnCompleted (Action continuation) 设置延续委托。

等待对象包含一些用于实际有效负载的方法(例如,下面的方法是Run)。

class Program {
    // Need to change the declaration of Main() in order to use 'await'
    static async Task Main () {
        // Create a custom awaitable object
        MyAwaitable awaitable = new MyAwaitable ();

        // Run awaitable payload, ignore returned Task
        _ = awaitable.Run ();

        // Do some other tasks while awaitable is running
        Console.WriteLine ("Waiting for completion...");

        // Wait for completion
        await awaitable;

        Console.WriteLine ("The long operation is now complete. " + awaitable.GetResult());
    }
}

public class MyAwaitable : INotifyCompletion {
    // Fields
    private Action continuation = null;
    private string result = string.Empty;

    // Make this class awaitable
    public MyAwaitable GetAwaiter () { return this; }

    // Implementation of INotifyCompletion for the self-awaiter
    public bool IsCompleted { get; set; }
    public string GetResult () { return result; }
    public void OnCompleted (Action continuation) {
        // Store continuation delegate
        this.continuation = continuation;
        Console.WriteLine ("Continuation set");
    }

    // Payload to run
    public async Task Run () {
        Console.WriteLine ("Computing result...");

        // Wait 2 seconds
        await Task.Delay (2000);
        result = "The result is 10";

        // Set completed
        IsCompleted = true;

        Console.WriteLine ("Result available");

        // Continue with the continuation provided
        continuation?.Invoke ();
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.