【问题标题】:Async Await Few Confusions异步等待几个混淆
【发布时间】:2015-11-06 00:39:49
【问题描述】:

在体验 4.5 的新 async 和 Await 功能时,我想在继续之前澄清一些困惑。我一直在阅读不同的文章以及关于 SO 的不同问题,它帮助我理解了 Async 和 Await 的工作原理。我将尝试将我的理解和困惑放在这里,如果有人编码教育我和其他正在寻找相同事物的人,我将不胜感激。我正在用非常简单的措辞讨论这个问题。

所以使用 Async 是为了让编译器知道被 Async 标记的方法包含 Await 操作(Long 操作)。最新框架包含用于异步操作的不同新内置方法。

connection.OpenAsync、ExecuteScalarAsync 等内置异步函数与 Await 关键字一起使用。我不知道这些异步方法的内部工作原理,但我强烈的猜测是他们在后台使用任务。

我是否可以将此作为一般规则,即 Await 将与任何实现 Task 的方法一起使用。因此,如果我需要创建自己的执行长时间操作的方法,那么我会将其创建为 Task 并在调用它时使用 Await 关键字?

第二个最重要的事情是,将方法创建为 Async 或将其创建为任务的经验法则是什么。例如,

public void SampleMain()
{
    for (int i = 1; i <= 100; i++)
    {
         DataTable dt = ReadData(int id);
    }
}

public DataTable ReadData(int id)
{ 
     DataTable resultDT = new DataTable();

     DataTable dt1 = new DataTable();
     // Do Operation to Fill DataTable from first connection string
     adapter.Fill(dt1);

     DataTable dt2 = new DataTable();
     // Do Operation to Fill DataTable from first connection string
     adapter.Fill(dt2); 

     // Code for combining datatable and returning the resulting datatable
     // Combine DataTables
     return resultDT;
}

public string GetPrimaryConnectionString()
{
     // Retrieve connection string from some file io operations
     return "some primary connection string";
}

public string GetSecondaryConnectionString()
{
     // Retrieve connection string from some file io operations
     return "some secondaryconnection string";
}

现在这是一个非常简单的场景,它是基于我过去工作过的一些真实世界应用程序创建的。所以我只是想知道如何使整个过程异步。

我是否应该将 GetPrimaryConnectionString 和 GetSecondaryConnectionString 作为任务并在 ReadData 中等待它们。 ReadData 也会是一个任务吗?如何在 SampleMain 函数中调用 ReadData?

另一种方法是在 SampleMain 中为 ReadData 创建一个任务并运行该任务并跳过将其他方法转换为任务。这是好方法吗?会是真正的异步吗?

【问题讨论】:

  • 您的意思是“将方法创建为任务”的示例比您添加到帖子中的一些随机同步代码更有用。
  • @Alexei Levenkov 感谢您的回复。我完全不明白你的评论。我介绍的是一个示例原始场景,代码尽可能少,以使读者了解如何使用 Async 和 Await 使整个过程异步。这根本不是随机的。
  • @M.kazem Akhgary。好的。因此无需更改 GetPrimaryConnectionString 和 GetSecondaryConenctionString 方法。如果我只是将 ReadData 设为异步,那么所有这些过程都是异步的吗?

标签: c# asynchronous async-await task-parallel-library


【解决方案1】:

所以使用 Async 以便编译器知道由 Async 标记的方法 包含等待操作

使用async 是为了让编译器有指示从该方法中创建一个状态机。 async 方法可以没有 await,并且仍然可以工作,尽管它将完全同步执行。

内置的 Async 函数,如 connection.OpenAsync, ExecuteScalarAsync 等与 Await 关键字一起使用。我不知道 这些异步方法的内部工作,但我强烈的猜测是 他们正在使用任务的引擎盖。

Task 是对未来工作的承诺。有几种方法可以创建Task。但是,Task 并不是唯一可以表示异步操作的东西。如果你愿意,你可以自己创建一个 awaitable,它只需要它来实现一个 GetAwaiter 方法,该方法返回一个实现 INotifyCompletion 的类型。

如果你想知道一个方法是如何在框架中实现的,you can view the source。在这种特殊情况下,他们使用TaskCompletionSource&lt;T&gt;

我应该让 GetPrimaryConnectionString 和 GetSecondaryConnectionString 作为任务并在 ReadData 中等待它们。将要 ReadData 也是一个任务吗?如何在 SampleMain 中调用 ReadData 功能?

检索连接字符串本身并没有什么异步的。您通常(并非总是)将 async-await 与自然异步 IO 操作一起使用。在这种特殊情况下,唯一实际的异步操作是ReadData,如果您想使其异步,可以使用SqlDataReader,它公开了异步方法。

一个例子,取自ADO.NET teams blog

public static async Task<Product> GetProductAndReviewsAsync(
            int productID, int reviewsToGet)

{
    using (SqlConnection connection = new SqlConnection(ConnectionString))
    {
        await connection.OpenAsync();
        const string commandString = GetProductByIdCommand + ";" 
                                    + GetProductReviewsPagedById;

        using (SqlCommand command = new SqlCommand(commandString, connection))
        {
            command.Parameters.AddWithValue("productid", productID);
            command.Parameters.AddWithValue("reviewStart", 0); 
            command.Parameters.AddWithValue("reviewCount", reviewsToGet);
            using (SqlDataReader reader = await command.ExecuteReaderAsync())
            {
                if (await reader.ReadAsync())
                {
                    Product product = GetProductFromReader(reader, productID);
                    if (await reader.NextResultAsync())
                    {
                        List<Review> allReviews = new List<Review>();
                        while (await reader.ReadAsync())

                        {
                            Review review = GetReviewFromReader(reader);
                            allReviews.Add(review);
                        }
                        product.Reviews = allReviews.AsReadOnly();
                        return product;
                    }
                    else
                    {
                        throw new InvalidOperationException(
                            "Query to server failed to return list of reviews");
                    }
                }
                else
                {
                    return null;
                }
            }
        }
    }
}

【讨论】:

  • 对我来说,答案现在与示例代码版本完美结合。谢谢
  • 在您与我共享的示例代码中,在 Task 之类的任务中提到了一个明确的可为空对象。这需要明确吗?
  • @AdnanYaseen 来自异步操作的任何返回类型都需要返回 TaskTask&lt;T&gt;。我删除了Product?,以免让您感到困惑。
【解决方案2】:

如何使整个过程异步

如果有 adapter.Fill 的异步版本,那么只需在 ReadData 中为它提供 await,这反过来也成为 async,您可以在调用者方法中等待它:

// in async button click event
button.Enabled = false;
var dt = await ReadData(int id);
button.Enabled = true;
... // do something with dt

public async Task<DataTable> ReadData(int id)
{ 
     ...
     var job1 = adapter.AsyncFill(dt1);
     var job2 = adapter.Fill(dt2); 
     // wait for all of them to finish
     Task.WaitAll(new[] {job1, job2});
     ...
     return Task.FromResult(resultDT); // dump approach
}

如果没有异步版本,那么必须创建它们(使用Task):

// in async button click event
button.Enabled = false;
// run synchronous task asynchronously
var dt = await Task.Run(() => ReadData(int id));
button.Enabled = true;
... // do something with dt

async/await 在 UI 方面表现出色,否则(如果不涉及 UI)只需创建任务并在那里运行同步操作。

【讨论】:

  • async-await 与 UI 无关。当您有非阻塞 IO 操作时使用它确实有帮助。但是像这样使用Task.Run 将无法扩展。
  • @Sinatr 感谢您提供示例代码,帮助我更好地理解。只是一个快速的问题。您提到如果不涉及 UI,那么我必须创建任务并同步运行。如果我没有涉及 UI 并且必须运行并行任务怎么办?
  • @AdnanYaseen,如果您将 for 展开到 TPL 中,我认为您不会有任何收获。瓶颈是硬盘驱动器或网络。如果您进行一些繁重的处理(这与接收数据所需的时间相当),那么它肯定会有所帮助。我自己从来没有做过这样的计算,因此我的答案是关于 UI。
  • @YuvalItzchakov,你的意思是创建几个任务然后异步运行,那么Task.AwaitAll当然比等待每个要好,或者你的意思是“将不是规模”?
  • 没有。我的意思是异步 IO 的强大之处在于它可以扩展,因为您不会为操作阻塞任何线程。您的示例所做的恰恰相反,它阻塞了等待 IO 的线程。
【解决方案3】:

使用 async-await 的唯一原因是您的主线程可能会在另一个线程执行长度操作时执行一些有用的操作。如果主线程要启动另一个线程,只等待另一个线程完成,最好让主线程做动作。

主线程经常做的事情之一是保持 UI 响应。

你是对的,在后台 async-await 使用任务,因此你看到一个异步函数返回一个任务。

规则:

  • 如果函数返回 void,则异步版本返回 Task。如果函数将返回 TResult,则异步版本应返回 Task&lt;TResult>。
  • 有一个例外:异步事件处理程序返回 void。
  • await Task 的返回值为 void。 await Task&lt;TResult> 的返回值为 TResult。
  • 只有异步函数可以调用其他异步函数。
  • 如果您有非异步函数,您仍然可以使用异步函数。但是你不能使用等待。使用异步函数的 Task 返回值和 System.Threading.Tasks.Task 方法等待结果。
  • 如果你有一个异步函数并且想在一个单独的线程中启动一个非异步函数,使用:

    private int SlowCalculation(int a, int b) { System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5)); 返回 a + b; }

    私有异步任务CalculateAsync(int a, int b) { 任务 myTask = Task.Run( () => SlowCalculation(a, b); // 当 SlowCalcuation 计算缓慢时,做其他有用的事情 // 过了一会儿你需要答案 int sum = 等待我的任务; 返回总和; }

看到 await Task&lt;int> 的返回是 int。

有些人曾经使用 Task.ContinueWith 之类的函数。因为不再需要 await 语句。 Await 确保任务完成。 await 之后的语句是您通常在 ContinueWith 中执行的操作。

在 Task.ContinueWith 中,您可以说:“仅当任务失败时才执行此操作”。与此等效的 async-await 是 try-catch。

记住:如果你的线程没有什么有用的事情可做(比如保持你的 UI 响应),不要使用 async-await

在 async-await 中启动几个任务并等待它们完成如下:

private async Task MyAsyncFunction(...)
{
    var tasks = new List<Task<int>>();
    for (int i=0; i<10; ++i)
    {
        tasks.Add(CalculateAsync(i, 2*i);
    }
    // while all ten tasks are slowly calculating do something useful
    // after a while you need the answer, await for all tasks to complete:
    await Task.WhenAll(tasks);
    // the result is in Task.Result:
    if (task[3].Result < 5) {...}
}

Task.Waital 的异步等待版本是 Task.WhenAll。 WhenAll 返回一个 Task 而不是 void,因此您可以等待它。即使在等待时,主线程也会保持响应。

使用Task.WaitAll时主线程不是这样,因为你不用等待。

【讨论】:

    猜你喜欢
    • 2019-01-23
    • 1970-01-01
    • 2020-09-14
    • 1970-01-01
    • 2020-05-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多