你应该如何使用C#5的async
来表示一系列异步任务?例如,如果我们想从服务器下载编号文件并在我们获取它时返回每个文件,我们如何实现这样的方法?
public async IEnumerable<File> DownloadPictures() {
const string format = "http://example.com/files/{0}.png";
for (int i = 0; i++; ) {
yield return await DownloadFile(string.Format(format, i));
}
}
答案 0 :(得分:5)
真正的序列不能直接与async
/ await
一起使用,因为任务只返回一个值。您需要实际的可枚举类型,例如Ix-Async(或AsyncEx)中的IAsyncEnumerator<T>
。 IAsyncEnumerator<T>
中描述了{{1}}的设计。
答案 1 :(得分:5)
在我看来,您需要与BlockingCollection<T>
非常相似的内容,它使用Task
和await
来代替阻止。
具体而言,您可以添加的内容不会阻塞或等待。但是当您尝试删除当前没有项目的项目时,您可以await
直到某个项目可用。
公共界面可能如下所示:
public class AsyncQueue<T>
{
public bool IsCompleted { get; }
public Task<T> DequeueAsync();
public void Enqueue(T item);
public void FinishAdding();
}
FinishAdding()
是必要的,以便我们知道何时结束出列。
有了这个,您的代码可能如下所示(m_queue
为AsyncQueue<File>
):
var tasks = Enumerable.Range(0, 10)
.Select(i => DownloadAndEnqueue(i))
.ToArray();
Task.WhenAll(tasks).ContinueWith(t => m_queue.FinishAdding());
…
static async Task DownloadAndEnqueue(string url)
{
m_queue.Enqueue(await DownloadFile(url));
}
它不像你想象的那样好,但它应该有效。
AsyncQueue<T>
的实施?有两个队列。一个是完成的工作,尚未出现。另一个是已经出列的Task
s(实际上是TaskCompletionSource<T>
),但是还没有任何结果。
当您出队并且队列中有一些已完成的工作时,只需从那里返回工作(使用Task.FromResult()
)。如果队列为空,请创建新的Task
,将其添加到另一个队列并返回。
当你排队完成一些已完成的工作并且队列中有一些Task
时,删除一个并使用我们现在拥有的结果完成它。如果Task
队列为空,请将工作添加到第一个队列。
有了这个,您可以根据需要多次出列和入队,并且它可以正常工作。如果您知道没有任何新作品,请致电FinishAdding()
。如果有任何等待Task
,它们将抛出异常。
换句话说:
public class AsyncQueue<T>
{
private readonly object m_lock = new object();
private bool m_finishedAdding = false;
private readonly Queue<T> m_overflowQueue = new Queue<T>();
private readonly Queue<TaskCompletionSource<T>> m_underflowQueue =
new Queue<TaskCompletionSource<T>>();
public bool IsCompleted
{
get { return m_finishedAdding && m_overflowQueue.Count == 0; }
}
public Task<T> DequeueAsync()
{
Task<T> result;
lock (m_lock)
{
if (m_overflowQueue.Count > 0)
result = Task.FromResult(m_overflowQueue.Dequeue());
else if (!m_finishedAdding)
{
var tcs = new TaskCompletionSource<T>();
m_underflowQueue.Enqueue(tcs);
result = tcs.Task;
}
else
throw new InvalidOperationException();
}
return result;
}
public void Enqueue(T item)
{
lock (m_lock)
{
if (m_finishedAdding)
throw new InvalidOperationException();
if (m_underflowQueue.Count > 0)
{
var tcs = m_underflowQueue.Dequeue();
tcs.SetResult(item);
}
else
m_overflowQueue.Enqueue(item);
}
}
public void FinishAdding()
{
lock (m_lock)
{
m_finishedAdding = true;
while (m_underflowQueue.Count > 0)
{
var tcs = m_underflowQueue.Dequeue();
tcs.SetException(new InvalidOperationException());
}
}
}
}
如果您想限制工作队列的大小(从而限制生产者,如果它们太快),您也可以Enqueue()
返回Task
,这将需要另一个队列。
答案 2 :(得分:3)
我知道已经有一段时间了,但是我已经写了一些东西来模仿异步枚举here的“收益率回报”。不需要复杂的代码。
你可以像使用它一样:
public IAsyncEnumerable<File> DownloadPictures() {
const string format = "http://example.com/files/{0}.png";
return AsyncEnumerable.Create(async y =>
{
for (int i = 0; i++; ) {
await y.YieldReturn(await DownloadFile(string.Format(format, i)));
}
};
}
我通常不会在这里宣传我自己的代码,但这是C#6.0中明显需要的功能,所以我希望你在C#5.0中发现它很有用,如果你仍然坚持这个。
答案 3 :(得分:-1)
如果您只有有限数量的网址,则可以这样做:
public async Task<IEnumerable<File>> DownloadPictures()
{
const string format = "http://example.com/files/{0}.png";
var urls = Enumerable.Range(0, 999).Select(i => String.Format(format, i));
var tasks = urls.Select(u => DownloadFile(u));
var results = Task.WhenAll(tasks);
return await results;
}
关键是获取任务列表,然后在该列表上调用Task.WhenAll。
答案 4 :(得分:-1)
async
的好处是调用方法可以并行调用多个阻塞操作,并且只在需要返回值时才阻塞。通过使用返回类型yield/return
,IEnumerable<Task>
在此方案中可以实现相同的功能。
public IEnumerable<Task<File>> DownloadPictures() {
const string format = "http://example.com/files/{0}.png";
for (int i = 0; i++; ) {
yield return DownloadFileAsync(string.Format(format, i));
}
}
以与async/await
类似的方式,调用方法现在可以继续执行,直到它需要下一个值,此时可以在下一个任务上调用await/.Result
。以下扩展方法演示了这一点:
public static IEnumerable<T> Results<T>(this IEnumerable<Task<T>> tasks)
{
foreach (var task in tasks)
yield return task.Result;
}
如果调用方法想要确保创建所有IEnumerable
Task
并且并行运行,那么诸如以下的扩展方法可能是有益的(这和上述方法,可能已经在标准的lib中了:
public static IEnumerable<T> ResultsParallel<T>(this IEnumerable<Task<T>> tasks)
{
foreach (var task in tasks.ToArray())
yield return task.Result;
}
请注意,并行运行的关注责任如何转移到调用方法,就像调用async/await
一样。如果担心创建Task
s阻止,可以创建如下的扩展方法:
public static Task<IEnumerable<T>> ResultsAsync<T>(this IEnumerable<Task<T>> tasks)
{
var startedTasks = new ConcurrentQueue<Task<T>>();
var writerTask = new Task(() =>
{
foreach (var task in tasks)
{
startedTasks.Enqueue(task);
}
});
writerTask.Start();
var readerTask = new Task<IEnumerable<T>>(() =>
{
return ResultsSequential(startedTasks, () => writerTask.IsCompleted);
});
readerTask.Start();
return readerTask;
}
private static IEnumerable<T> ResultsSequential<T>(ConcurrentQueue<Task<T>> tasks, Func<bool> isDone)
{
while (true)
{
Task<T> task;
if (isDone.Invoke())
{
if (tasks.TryDequeue(out task))
{
yield return task.Result;
}
else
{
yield break;
}
} else if (tasks.TryDequeue(out task))
{
yield return task.Result;
}
}
}
此实施效率不高。有效的实施是too large to fit in the margin。