在C#5中表示异步序列

时间:2011-10-03 19:07:56

标签: c# asynchronous

你应该如何使用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));
    }
}

5 个答案:

答案 0 :(得分:5)

真正的序列不能直接与async / await一起使用,因为任务只返回一个值。您需要实际的可枚举类型,例如Ix-Async(或AsyncEx)中的IAsyncEnumerator<T>IAsyncEnumerator<T>中描述了{{1}}的设计。

答案 1 :(得分:5)

在我看来,您需要与BlockingCollection<T>非常相似的内容,它使用Taskawait来代替阻止。

具体而言,您可以添加的内容不会阻塞或等待。但是当您尝试删除当前没有项目的项目时,您可以await直到某个项目可用。

公共界面可能如下所示:

public class AsyncQueue<T>
{
    public bool IsCompleted { get; }

    public Task<T> DequeueAsync();

    public void Enqueue(T item);

    public void FinishAdding();
}

FinishAdding()是必要的,以便我们知道何时结束出列。

有了这个,您的代码可能如下所示(m_queueAsyncQueue<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/returnIEnumerable<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