与巨大的数据流异步

时间:2014-07-25 23:41:21

标签: c# .net task-parallel-library async-await

我们使用IEnumerables从数据库中返回大量数据集:

public IEnumerable<Data> Read(...)
{
    using(var connection = new SqlConnection(...))
    {
        // ...
        while(reader.Read())
        {
            // ...
            yield return item;
        }
    }
}

现在我们想使用异步方法来做同样的事情。但是,async没有IEnumerables,因此我们必须将数据收集到列表中,直到加载整个数据集:

public async Task<List<Data>> ReadAsync(...)
{
    var result = new List<Data>();
    using(var connection = new SqlConnection(...))
    {
        // ...
        while(await reader.ReadAsync().ConfigureAwait(false))
        {
            // ...
            result.Add(item);
        }
    }
    return result;
}

这将消耗服务器上的大量资源,因为所有数据必须在返回之前在列表中。 IEnumerables处理大数据流的最佳且易于使用的异步替代方法是什么?我想避免在处理时将所有数据存储在内存中。

5 个答案:

答案 0 :(得分:24)

最简单的选择是使用TPL Dataflow。您需要做的就是配置一个处理处理的ActionBlock(如果您愿意,可以并行处理),并将“发送”项逐个异步地逐个添加到其中。 我还建议设置一个BoundedCapacity,当处理无法处理速度时,它会限制读者从数据库中读取数据。

var block = new ActionBlock<Data>(
    data => ProcessDataAsync(data),
    new ExecutionDataflowBlockOptions
    {
        BoundedCapacity = 1000,
        MaxDegreeOfParallelism = Environment.ProcessorCount
    });

using(var connection = new SqlConnection(...))
{
    // ...
    while(await reader.ReadAsync().ConfigureAwait(false))
    {
        // ...
       await block.SendAsync(item);
    }
}

您也可以使用Reactive Extensions,但这是一个比您可能需要的更复杂,更健壮的框架。

答案 1 :(得分:9)

  

这将消耗服务器上的大量资源,因为全部   返回前数据必须在列表中。什么是最好和最容易的   使用IEnumerables的异步替代来处理大数据   流?我想避免将所有数据存储在内存中   处理

如果您不想立即将所有数据发送到客户端,您可以考虑使用Reactive Extensions (Rx)(在客户端上)和SignalR(在客户端和服务器上)来处理此

SignalR将允许异步发送数据到客户端。 Rx允许将LINQ应用于数据项的异步序列,因为它们已到达客户端。但是,这会改变客户端 - 服务器应用程序的整个代码模型。

示例(Samuel Jack的博客文章):

相关问题(如果不是重复):

答案 2 :(得分:9)

大多数情况下,当处理async / await方法时,我发现更容易解决问题,并使用函数(Func<...>)或操作(Action<...>)而不是ad-hoc代码,特别是IEnumerableyield

换句话说,当我认为“异步”时,我试图忘记功能“返回值”的旧概念,否则它是如此明显并且我们非常熟悉。

例如,如果您将初始同步代码更改为此(processor是最终将对您使用一个数据项执行的操作的代码):

public void Read(..., Action<Data> processor)
{
    using(var connection = new SqlConnection(...))
    {
        // ...
        while(reader.Read())
        {
            // ...
            processor(item);
        }
    }
}

然后,异步版本编写起来非常简单:

public async Task ReadAsync(..., Action<Data> processor)
{
    using(var connection = new SqlConnection(...))
    {
        // note you can use connection.OpenAsync()
        // and command.ExecuteReaderAsync() here
        while(await reader.ReadAsync())
        {
            // ...
            processor(item);
        }
    }
}

如果您可以通过这种方式更改代码,则不需要任何扩展或额外的库或IAsyncEnumerable内容。

答案 3 :(得分:7)

正如其他一些海报所提到的,这可以用Rx实现。使用Rx,该函数将返回可以订阅的IObservable<Data>,并在它可用时将数据推送到订阅者。 IObservable也支持LINQ并添加了一些自己的扩展方法。

<强>更新

我添加了一些通用帮助方法,以便重复使用阅读器,并支持取消。

public static class ObservableEx
    {
        public static IObservable<T> CreateFromSqlCommand<T>(string connectionString, string command, Func<SqlDataReader, Task<T>> readDataFunc)
        {
            return CreateFromSqlCommand(connectionString, command, readDataFunc, CancellationToken.None);
        }

        public static IObservable<T> CreateFromSqlCommand<T>(string connectionString, string command, Func<SqlDataReader, Task<T>> readDataFunc, CancellationToken cancellationToken)
        {
            return Observable.Create<T>(
                async o =>
                {
                    SqlDataReader reader = null;

                    try
                    {                        
                        using (var conn = new SqlConnection(connectionString))
                        using (var cmd = new SqlCommand(command, conn))
                        {
                            await conn.OpenAsync(cancellationToken);
                            reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection, cancellationToken);

                            while (await reader.ReadAsync(cancellationToken))
                            {
                                var data = await readDataFunc(reader);
                                o.OnNext(data);                                
                            }

                            o.OnCompleted();
                        }
                    }
                    catch (Exception ex)
                    {
                        o.OnError(ex);
                    }

                    return reader;
                });
        }
    }

现在大大简化了ReadData的实施。

     private static IObservable<Data> ReadData()
    {
        return ObservableEx.CreateFromSqlCommand(connectionString, "select * from Data", async r =>
        {
            return await Task.FromResult(new Data()); // sample code to read from reader.
        });
    }

<强>用法

你可以通过给它一个IObserver来订阅Observable,但也有一些带有lambdas的重载。随着数据变得可用,将调用OnNext回调。如果存在异常,则会调用OnError回调。最后,如果没有更多数据,则会调用OnCompleted回调。

如果您想取消观察,只需处理订阅。

void Main()
{
   // This is an asyncrhonous call, it returns straight away
    var subscription = ReadData()
        .Skip(5)                        // Skip first 5 entries, supports LINQ               
        .Delay(TimeSpan.FromSeconds(1)) // Rx operator to delay sequence 1 second
        .Subscribe(x =>
    {
        // Callback when a new Data is read
        // do something with x of type Data
    },
    e =>
    {
        // Optional callback for when an error occurs
    },
    () =>
    {
        //Optional callback for when the sequenc is complete
    }
    );

    // Dispose subscription when finished
    subscription.Dispose();

    Console.ReadKey();
}

答案 4 :(得分:2)

我认为Rx绝对是这种情况下的方法,因为可观察的序列是可枚举序列的正式对偶。

正如前面的回答所提到的,你可以从头开始重写你的序列作为一个observable,但是还有几种方法可以继续编写你的迭代器块,然后只是异步展开它们。

1)只需将枚举转换为observable,如下所示:

using System.Reactive.Linq;
using System.Reactive.Concurrency;

var enumerable = Enumerable.Range(10);
var observable = enumerable.ToObservable();
var subscription = observable.Subscribe(x => Console.WriteLine(x));

通过将其通知推送到任何下游观察者,这将使您的可枚举行为类似于observable。在这种情况下,当调用Subscribe时,它将同步阻塞,直到处理完所有数据。如果您希望它完全异步,可以使用以下命令将其设置为其他线程:

var observable = enumerable.ToObservable().SubscribeOn(NewThreadScheduler.Default);

现在,枚举的展开将在一个新线程中完成,而subscribe方法将立即返回。

2)使用另一个异步事件源展开枚举:

var enumerable = Enumerable.Range(10);
var observable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1))
                           .Zip(enumerable, (t, x) => x);
var subscription = observable.Subscribe(x => Console.WriteLine(x));

在这种情况下,我设置了一个计时器来触发每一秒,每当它触发它时,它会向前移动迭代器。现在,计时器可以很容易地被任何事件源替换,以准确控制迭代器何时向前移动。

我发现自己很享受迭代器块的语法和语义(例如try / finally块和dispose会发生什么),所以即使在设计异步操作时我偶尔会使用这些设计。