我一直到处寻找在.net 4.5中新的Async和Await功能的真实示例。我想出了以下代码来下载文件列表并限制并发下载的数量。我将感谢任何最佳实践或方法来改进/优化此代码。

我们使用以下语句调用以下代码。

await this.asyncDownloadManager.DownloadFiles(this.applicationShellViewModel.StartupAudioFiles, this.applicationShellViewModel.SecurityCookie, securityCookieDomain).ConfigureAwait(false);


然后我们使用事件将下载的文件添加到ViewModel上的observablecollection(.net 4.5中的新线程安全版本)中。

public class AsyncDownloadManager
    {
        public event EventHandler<DownloadedEventArgs> FileDownloaded;

        public async Task DownloadFiles(string[] fileIds, string securityCookieString, string securityCookieDomain)
          {
            List<Task> allTasks = new List<Task>();
            //Limits Concurrent Downloads 
            SemaphoreSlim throttler = new SemaphoreSlim(initialCount: Properties.Settings.Default.maxConcurrentDownloads);

            var urls = CreateUrls(fileIds);

            foreach (var url in urls)   
            {  
                await throttler.WaitAsync();
                allTasks.Add(Task.Run(async () => 
                {
                    try
                    {
                        HttpClientHandler httpClientHandler = new HttpClientHandler();
                        if (!string.IsNullOrEmpty(securityCookieString))
                        {
                            Cookie securityCookie;
                            securityCookie = new Cookie(FormsAuthentication.FormsCookieName, securityCookieString);
                            securityCookie.Domain = securityCookieDomain;
                            httpClientHandler.CookieContainer.Add(securityCookie);    
                        }                     

                        await DownloadFile(url, httpClientHandler).ConfigureAwait(false);
                    }
                    finally
                    {
                        throttler.Release();
                    }
                }));
            }
            await Task.WhenAll(allTasks).ConfigureAwait(false);
        }

        async Task DownloadFile(string url, HttpClientHandler clientHandler)
        {
            HttpClient client = new HttpClient(clientHandler);
            DownloadedFile downloadedFile = new DownloadedFile();

            try
            {
                HttpResponseMessage responseMessage = await client.GetAsync(url).ConfigureAwait(false);
                var byteArray = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false);

                if (responseMessage.Content.Headers.ContentDisposition != null)
                {
                    downloadedFile.FileName = Path.Combine(Properties.Settings.Default.workingDirectory, responseMessage.Content.Headers.ContentDisposition.FileName);
                }
                else
                {
                    return;
                }

                if (!Directory.Exists(Properties.Settings.Default.workingDirectory))   
                {
                    Directory.CreateDirectory(Properties.Settings.Default.workingDirectory);
                }
                using (FileStream filestream = new FileStream(downloadedFile.FileName, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
                {
                    await filestream.WriteAsync(byteArray, 0, byteArray.Length);
                }
            }
            catch(Exception ex)
            {    
                return; 
            }
            OnFileDownloaded(downloadedFile);
        }

        private void OnFileDownloaded(DownloadedFile downloadedFile)
        {    
            if (this.FileDownloaded != null)
            {
                this.FileDownloaded(this, new DownloadedEventArgs(downloadedFile));
            }
        }    

    public class DownloadedEventArgs : EventArgs
    {
        public DownloadedEventArgs(DownloadedFile downloadedFile)
        {   
            DownloadedFile = downloadedFile;
        }

        public DownloadedFile DownloadedFile { get; set; }
    }



将Async / Await嵌入其他Async / Await方法有什么影响? (将文件流写入Async / Await方法内的磁盘。
是否正确使用了ConfigureAwait(False)还是应该更自由地使用它?
应该为每个单独的任务使用httpclient还是应该共享一个httpclient? 4.一个事件是将下载的文件引用“发送”到视图模型的一种好方法吗?
是await Task.WhenAll(allTask​​s).ConfigureAwait(false);同时运行所有任务的最佳方法是有限的通过SemaphoreSlim节气门。


#1 楼

首先,一些较小的事情(就代码的大小而言):


我认为您应该多使用var,尤其是在清楚对象具有什么类型的情况下,因为只是创建它。
您不应该将响应读取为单字节数组。相反,您应该尽可能使用Stream s(在您的情况下当然可以),因为它们效率更高。
不仅要忽略未知异常。如果您要忽略某些例外情况(例如,当网站返回404时),请明确指定它们。结果。虽然使用Semaphore限制代码工作正常,但我认为您应该让一些用于异步处理集合的库为您做到这一点。这样的库包括Rx和TPL数据流。这样做还可以避免使用事件,因为您的方法将返回“异步集合”(Rx中的IObservable<T>,TPL Dataflow中的ISourceBlock<T>)。

使用TPL Dataflow,您的代码可能看起来像:

public ISourceBlock<DownloadedFile> DownloadFiles(string[] fileIds, string securityCookieString, string securityCookieDomain)
{
    var urls = CreateUrls(fileIds);

    // we have to use TransformManyBlock here, because we want to be able to return 0 or 1 items
    var block = new TransformManyBlock<string, DownloadedFile>(
        async url =>
        {
            var httpClientHandler = new HttpClientHandler();
            if (!string.IsNullOrEmpty(securityCookieString))
            {
                var securityCookie = new Cookie(FormsAuthentication.FormsCookieName, securityCookieString);
                securityCookie.Domain = securityCookieDomain;
                httpClientHandler.CookieContainer.Add(securityCookie);
            }

            return await DownloadFile(url, httpClientHandler);
        }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = Properties.Settings.Default.maxConcurrentDownloads });

    foreach (var url in urls)
        block.Post(url);

    block.Complete();

    return block;
}

private static async Task<DownloadedFile[]> DownloadFile(string url, HttpClientHandler clientHandler)
{
    var client = new HttpClient(clientHandler);
    var downloadedFile = new DownloadedFile();

    try
    {
        HttpResponseMessage responseMessage = await client.GetAsync(url);

        if (responseMessage.Content.Headers.ContentDisposition == null)
            return new DownloadedFile[0];

        downloadedFile.FileName = Path.Combine(
            Properties.Settings.Default.workingDirectory, responseMessage.Content.Headers.ContentDisposition.FileName);

        if (!Directory.Exists(Properties.Settings.Default.workingDirectory))
        {
            Directory.CreateDirectory(Properties.Settings.Default.workingDirectory);
        }

        using (var httpStream = await responseMessage.Content.ReadAsStreamAsync())
        using (var filestream = new FileStream(
            downloadedFile.FileName, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
        {
            await httpStream.CopyToAsync(filestream, 4096);
        }
    }
    // TODO: improve
    catch (Exception ex)
    {
        return new DownloadedFile[0];
    }

    return new[] { downloadedFile };
}


评论


\ $ \ begingroup \ $
感谢您提供的代码。发布样本时,我倾向于不使用var。我当时认为流是一种比字节数组更好的消费方式,但是我担心异步的性质。我看到您的代码将如何完美地处理流。最后-似乎我需要更深入地研究Rx和TPL。我以为他们对Async / Await有点过时了,但似乎我弄错了。两者的新版本。您会为这种情况推荐TPL吗?
\ $ \ endgroup \ $
–布兰德·邦德森(Blane Bunderson)
2012年11月20日在1:03



\ $ \ begingroup \ $
TPL肯定不会因异步方而过时,因为异步实际上是基于TPL(任务类型)构建的,部分原因是它们在做不同的事情(例如,没有Parallel.ForEach的直接异步替代品) ())。但是我专门谈论的是TPL Dataflow,它是.Net 4.5中的新增功能,其编写目的是为了利用异步。而且Rx也不是过时的,它的用法与普通异步方法大不相同(尽管有一些重叠)。
\ $ \ endgroup \ $
– svick
2012年11月20日,1:15



\ $ \ begingroup \ $
您是否没有通过HttpCompletionOption.ResponseHeadersRead使用此GetAsync重载,然后通过ReadAsStreamAsync()使用响应的任何原因?
\ $ \ endgroup \ $
– G. Stoynev
2013年12月18日16:43



\ $ \ begingroup \ $
@ G.Stoynev我刚刚从问题中复制了该代码。我实际上不太了解HttpClient。
\ $ \ endgroup \ $
– svick
13年12月18日在17:23

\ $ \ begingroup \ $
@AceInfinity我认为您的建议很好,但是由于许多建议都在修改我从问题中复制的代码,因此我认为您应该将其作为单独的评论答复发布。
\ $ \ endgroup \ $
– svick
16年4月25日在18:40

#2 楼

抱歉,我没有仔细阅读您的代码,而是针对“新的Async和Await功能的真实示例”:

基本上,在他们的新API中,所有可能很费时的事物(磁盘IO,网络IO等)现在都异步了。您只是无法同步调用它。

我认为这旨在迫使我们编写不阻止UI线程的响应式应用。
例如,您需要下载文件并保存它到磁盘,并在UI中显示忙碌指示器。然后,在ViewModel中,您可以说:

void DoTheJob()
{
  IsBusy=true;
  var text = await DownloadText();
  ProgressText="Saving...";
  await SaveText(text);
  IsBusy=false;
}


我想说的是一个很好的现实示例(我的“字典”应用程序使用这种方法从磁盘)。

与旧的BackgroundWorkers / ThreadPool和Dispatcher.BeginInvoke麻烦相比,这是一种非常酷的UI处理方法。

评论


\ $ \ begingroup \ $
请注意,要编译代码,您需要在方法中添加async修饰符。此外,最好将返回类型更改为Task,以便可以依次等待您的方法。
\ $ \ endgroup \ $
– svick
2012年11月13日20:40