Bagaimana saya bisa membuat `menunggu ...` bekerja dengan `yield return` (yaitu di dalam metode iterator)? (How can I make `await …` work with `yield return` (i.e. inside an iterator method)?)


問題描述

Bagaimana saya bisa membuat menunggu ... bekerja dengan yield return (yaitu di dalam metode iterator)? (How can I make await … work with yield return (i.e. inside an iterator method)?)

I have existing code that looks similar to:

IEnumerable<SomeClass> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(connectionString))
    using (SqlCommand cmd = new SqlCommand(sql, conn)
    {
        conn.Open();
        SqlDataReader reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            SomeClass someClass = f(reader); // create instance based on returned row
            yield return someClass;
        }
    } 
}

It seems I could benefit by using reader.ReadAsync(). However, if I just modify the one line:

        while (await reader.ReadAsync())

the compiler informs me that await can only be used in methods marked with async, and suggests I modify the method signature to be:

async Task<IEnumerable<SomeClass>> GetStuff()

However, doing that makes GetStuff() unusable because:

  

The body of GetStuff() cannot be an iterator block because Task<IEnumerable<SomeClass>> is not an iterator interface type.

I'm sure I am missing a key concept with the async programming model.

Questions:

  • Can I use ReadAsync() in my iterator? How?
  • How can I think about the async paradigm differently so that I understand how it works in this type of situation?

‑‑‑‑‑

參考解法

方法 1:

The problem is what you're asking doesn't actually make much sense. IEnumerable<T> is a synchronous interface, and returning Task<IEnumerable<T>> isn't going to help you much, because some thread would have to block waiting for each item, no matter what.

What you actually want to return is some asynchronous alternative to IEnumerable<T>: something like IObservable<T>, dataflow block from TPL Dataflow or IAsyncEnumerable<T>, which is planned to be added to C# 8.0/.Net Core 3.0. (And in the meantime, there are some libraries that contain it.)

Using TPL Dataflow, one way to do this would be:

ISourceBlock<SomeClass> GetStuff() {
    var block = new BufferBlock<SomeClass>();

    Task.Run(async () =>
    {
        using (SqlConnection conn = new SqlConnection(connectionString))
        using (SqlCommand cmd = new SqlCommand(sql, conn))
        {
            await conn.OpenAsync();
            SqlDataReader reader = await cmd.ExecuteReaderAsync();
            while (await reader.ReadAsync())
            {
                SomeClass someClass;
                // Create an instance of SomeClass based on row returned.
                block.Post(someClass);
            }
            block.Complete();
        } 
    });

    return block;
}

You'll probably want to add error handling to the above code, but otherwise, it should work and it will be completely asynchronous.

The rest of your code would then consume items from the returned block also asynchronously, probably using ActionBlock.

方法 2:

No, you can't currently use async with an iterator block. As svick says, you would need something like IAsyncEnumerable to do that. 

If you have the return value Task<IEnumerable<SomeClass>> it means that the function returns a single Task object that, once completed, will provide you with a fully formed IEnumerable (no room for Task asynchrony in this enumerable). Once the task object is complete, the caller should be able to synchronously iterate through all the items it returned in the enumerable. 

Here is a solution that returns Task<IEnumerable<SomeClass>>. You could get a large part of benefit of async by doing something like this:

async Task<IEnumerable<SomeClass>> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(""))
    {
        using (SqlCommand cmd = new SqlCommand("", conn))
        {
            await conn.OpenAsync();
            SqlDataReader reader = await cmd.ExecuteReaderAsync();
            return ReadItems(reader).ToArray();
        }
    }
}

IEnumerable<SomeClass> ReadItems(SqlDataReader reader)
{
    while (reader.Read())
    {
        // Create an instance of SomeClass based on row returned.
        SomeClass someClass = null;
        yield return someClass;
    }
}

...and an example usage:

async void Caller()
{
    // Calls get‑stuff, which returns immediately with a Task
    Task<IEnumerable<SomeClass>> itemsAsync = GetStuff();
    // Wait for the task to complete so we can get the items
    IEnumerable<SomeClass> items = await itemsAsync;
    // Iterate synchronously through the items which are all already present
    foreach (SomeClass item in items)
    {
        Console.WriteLine(item);
    }
}

Here you have the iterator part and the async part in separate functions which allows you to use both the async and yield syntax. The GetStuff function asynchronously acquires the data, and the ReadItems then synchronously reads the data into an enumerable. 

Note the ToArray() call. Something like this is necessary because the enumerator function executes lazily and so your async function may otherwise dispose the connection and command before all the data is read. This is because the using blocks cover the duration of the Task execution, but you would be iterating it after the task is complete.

This solution does not use ReadAsync, but it does use OpenAsync and ExecuteReaderAsync, which probably gives you most of the benefit. In my experience it is the ExecuteReader that will take the most time and have the most benefit being async. By the time I've read the first row, the SqlDataReader has all the other rows already and ReadAsync just returns synchronously. If this is the case for you as well then you won't get significant benefit by moving to a push‑based system like IObservable<T> (which will require significant modifications to the calling function).

For illustration, consider an alternative approach to the same issue:

IEnumerable<Task<SomeClass>> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(""))
    {
        using (SqlCommand cmd = new SqlCommand("", conn))
        {
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader();
            while (true)
                yield return ReadItem(reader);
        }
    }
}

async Task<SomeClass> ReadItem(SqlDataReader reader)
{
    if (await reader.ReadAsync())
    {
        // Create an instance of SomeClass based on row returned.
        SomeClass someClass = null;
        return someClass;
    }
    else
        return null; // Mark end of sequence
}

...and an example usage:

async void Caller()
{
    // Synchronously get a list of Tasks
    IEnumerable<Task<SomeClass>> items = GetStuff();
    // Iterate through the Tasks
    foreach (Task<SomeClass> itemAsync in items)
    {
        // Wait for the task to complete. We need to wait for 
        // it to complete before we can know if it's the end of
        // the sequence
        SomeClass item = await itemAsync;
        // End of sequence?
        if (item == null) 
            break;
        Console.WriteLine(item);
    }
}

In this case, GetStuff returns immediately with an enumerable, where each item in the enumerable is a task that will present a SomeClass object when it completes. This approach has a few flaws. Firstly, the enumerable returns synchronously so at the time it returns we actually don't know how many rows are in the result, which is why I made it an infinite sequence. This is perfectly legal but it has some side effects. I needed to use null to signal the end of useful data in the infinite sequence of tasks. Secondly, you have to be careful about how you iterate it. You need to iterate it forwards, and you need to wait for each row before iterating to the next row. You must also only dispose of the iterator after all the tasks have completed so that the GC doesn't collect connection before it's finished being used. For these reasons this is not a safe solution, and I must emphasize that I'm including it for illustration to help answer your second question.

方法 3:

Speaking strictly to async iterator's (or there possibility) within the context of a SqlCommand in my experience I've noticed that the synchronous version of the code vastly outperforms it's async counterpart. In both speed and memory consumption. 

  

Perhaps, take this observation with a grain of salt as the scope of the testing was limited to my machine and local SQL Server instance.

Don't get me wrong, the async/await paradigm within the .NET environment is phenomenally simple, powerful and useful given the right circumstances. After much toiling however, I'm not convinced database access is a proper use case for it. Unless of course you're needing to execute several commands simultaneously, in which case you can simply use TPL to fire off the commands in unison.

My preferred approach rather is to take the following considerations:

  • Keep the units of SQL work small, simple and compose‑able (i.e. make your SQL executions "cheap").
  • Avoid doing work on the SQL Server that can be push upstream to the app‑level. A perfect example of this is sorting.
  • Most importantly, test your SQL code at scale and review Statistics IO output/execution plan. A query which runs quickly at 10k record, may (and probably will) behave entirely differently when there a 1M records.

You could make the argument that in certain reporting scenarios, some of the above requirements just aren't possible. However, in the context of reporting services is asynchronous‑ity (is that even a word?) really needed? 

There's a fantastic article by Microsoft evangelist Rick Anderson about this very topic. Mind you it's old (from 2009) but still very relevant.

方法 4:

As of C# 8, this can be accomplished with IAsyncEnumerable

Modified code:

async IAsyncEnumerable<SomeClass> GetStuff()
{
    using (SqlConnection conn = new SqlConnection(connectionString))
    using (SqlCommand cmd = new SqlCommand(sql, conn)
    {
        conn.Open();
        SqlDataReader reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            SomeClass someClass = f(reader); // create instance based on returned row
            yield return someClass;
        }
    } 
}

Consume it like this:

await foreach (var stuff in GetStuff())
    ...

(by Eric J.svickMikepimEric J.)

參考文件

  1. How can I make await … work with yield return (i.e. inside an iterator method)? (CC BY‑SA 3.0/4.0)

#yield-return #async-await #generator #ado.net #C#






相關問題

Bagaimana saya bisa membuat `menunggu ...` bekerja dengan `yield return` (yaitu di dalam metode iterator)? (How can I make `await …` work with `yield return` (i.e. inside an iterator method)?)

收益回報使用 (yield return usage)

無法將“<>d__6”類型的對象轉換為“System.Object[]”類型 (Unable to cast object of type '<>d__6' to type 'System.Object[]')

使用 yield return 時 GetEnumerator() 方法會發生什麼? (What happens to GetEnumerator() method when yield return is used?)

抓取回調函數 (Scrapy callback function)

我可以在 VB.NET 中為 IEnumerable 函數實現收益返回嗎? (Can I implement yield return for IEnumerable functions in VB.NET?)

使用具有代碼訪問安全性的 C# 迭代器方法時出現問題 (Problem using C# iterator methods with code access security)

如何使用收益返回和遞歸獲得每個字母組合? (How do I get every combination of letters using yield return and recursion?)

是否可以使用 'yield' 來生成 'Iterator' 而不是 Scala 中的列表? (Is it possible to use 'yield' to generate 'Iterator' instead of a list in Scala?)

yield return 除了 IEnumerable 之外還有其他用途嗎? (Does yield return have any uses other than for IEnumerable?)

這個函數可以用更有效的方式編寫嗎? (Can this function be written in more efficient way?)

當我在代碼中引入產量時,它在 python 中不起作用 (when i introduced yield in code it doesn't work in python)







留言討論