将异步方法的调用排队

6

我有一个名为Refresh的异步操作。如果在第一个刷新完成之前进行第二个刷新调用,我需要将其排队。这是我目前的代码:

public async Task Refresh(RefreshArgs refreshArgs)
{
    await EnqueueRefreshTask(refreshArgs);
}

private Queue<RefreshArgs> refreshQueue =
    new Queue<RefreshArgs>();

private async Task EnqueueRefreshTask(RefreshArgs refreshArgs)
{
    refreshQueue.Enqueue(refreshArgs);

    await ProcessRefreshQueue();
}

private Task currentRefreshTask = null;

private async Task ProcessRefreshQueue()
{
    if ((currentRefreshTask == null) || (currentRefreshTask.IsCompleted))
    {
        if (refreshQueue.Count > 0)
        {
            var refreshArgs = refreshQueue.Dequeue();

            currentRefreshTask = DoRefresh(refreshArgs);
            await currentRefreshTask;

            await ProcessRefreshQueue();
        }           
    }
}

private async Task DoRefresh(RefreshArgs refreshArgs)
{
    // Lots of code here, including calls to a server that are executed with await.
    // Code outside my control may make another Refresh(args) call while this one is still processing.
    // I need this one to finish before processing the next.
}

这个方法可以实现,但是我不确定是否使用Tasks来做这个任务是最好的方式。有什么建议吗?

更新:

我尝试使用ActionBlock:

public async Task Refresh(RefreshArgs refreshArgs)
{
    if (refreshActionBlock == null)
    {
        var executionDataflowBlockOptions = new ExecutionDataflowBlockOptions();
        executionDataflowBlockOptions.MaxMessagesPerTask = 1;
        executionDataflowBlockOptions.TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

        refreshActionBlock = new ActionBlock<RefreshArgs>(args => DoRefresh(args), executionDataflowBlockOptions);
    }

    await refreshActionBlock.SendAsync(refreshArgs);
}

这将队列化DoRefresh,并允许其在UI线程中运行(这是我需要的)。问题在于SendAsync不等待DoRefresh的工作。

SendAsync:“异步地向目标消息块提供消息,允许延迟”。我只等待发送,而不是操作本身。

这样做并不能按预期工作:

await Refresh(RefreshArgs.Something);
// other code goes here. It expects the first refresh to be finished.
await Refresh(RefreshArgs.SomethingElse);
// other code goes here. It expects the second refresh to be finished.

在ActionBlock中,第二次刷新会被排入队列,但在完成刷新之前,等待的操作已经继续执行了。我需要它们在DoRefresh工作完成后返回。


2
你可以用 ActionBlock 替换掉所有这些。 - Stephen Cleary
"ActionBlock" 是我想到的。在处理异步序列时,TPL DataFlow(其中包括 ActionBlock)是一个不错的选择。 - spender
我认为这更适合放在代码审查网站上。 - code4life
请注意,Queue 不是线程安全的;您应该使用 BlockingCollection 代替。 - Servy
还有ConcurrentQueue - http://msdn.microsoft.com/en-us/library/dd267265.aspx - James Manning
@Servy 我认为这并不重要,因为所有的代码似乎都在 UI 线程上执行。 - svick
3个回答

3
我认为最简单的方法是使用AsyncLock。你可以从Stephen Cleary的库AsyncEx获取一个,或者阅读Stephen Toub的文章自己构建它
当你拥有AsyncLock后,实现Refresh()就很简单了:
public async Task Refresh(RefreshArgs refreshArgs)
{
    using (await m_lock.LockAsync())
    {
        // do your async work here
    }
}

这将确保Refresh()依次执行(而不是交错执行),并且从Refresh()返回的Task只有在Refresh()实际完成后才会完成。您可以使用TPL Dataflow中的ActionBlock来执行相同的操作,但您还需要使用TaskCompletionSource,而且比AsyncLock版本更加复杂。

2

您在最有用的原语 AsyncLock 前停下了。Stephen Toub 在他的系列文章 第6部分 中讲解了这个内容。 - svick

0

这很容易 :) 只需使用 "switch-case" !! 现在,让我们用一个小样例来展示如何处理。 样例场景:我们想要下载2张图片,按照队列一个接一个地下载。

命名空间 Test {

public partial class SampleOfQueue : Form
{

    public SampleOfQueue() { InitializeComponent(); }
    DBContext db = new DBContext();
    int Queue;

    private void btnStartApp_Click(object sender, EventArgs e)
    {
        Queue = 0;
        DownloadQueue();
    }

    private void DownloadQueue()
    {
        switch (Queue)
        {
            case 0:
                {
                    DownloadFile("http://images.forbes.com/media/lists/53/2009/tom-cruise.jpg", "Tom Cruise 1", "");
                    Queue += 1; break;
                }
            case 1:
                {
                    DownloadFile("https://upload.wikimedia.org/wikipedia/commons/6/69/Tom_Cruise_Collateral.jpg", "Tom Cruise 2", "");
                    Queue += 1; break;
                }
            case 2:
                {
                    // Other....
                    Queue += 1; break;
                }
            default: break;
        }

    }

    public void DownloadFile(string urlAddress, string ImageName, string ImageFarsiName)
    {
        WebClient webClient = new WebClient();
        webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);

        Uri URL = urlAddress.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ? new Uri(urlAddress) : new Uri(urlAddress);
        webClient.DownloadFileAsync(URL, "d:\\x.jpg");

    }

    private void Completed(object sender, AsyncCompletedEventArgs e)
    {
        DownloadQueue();
        if (Queue == 3) { MessageBox.Show("finish"); }
    }

}

}

在VS 2015 Windows表单中使用C# 4.6进行测试。 这是我自己的创新方法 :) 希望能有所帮助 <3 祝好运。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接