2016-09-28 8 views
6

인터넷에서 파일을 다운로드하는 간단한 콘솔 응용 프로그램을 빌드했습니다.
I had problems with WebClient 나는 HttpClient를 사용하여 내 응용 프로그램을 작성하기로 결정했습니다.진행률보고가있는 Stream.CopyToAsync - 복사 완료 후에도 진행률이보고됩니다.

기본적으로 헤더를 읽으라는 요청을하고 있습니다. ReadAsStreamAsyncCopyToAsync을 사용하여 로컬 파일에 복사하는 스트림이 나타납니다.

나는 IProgress을 지원하는 스트림의 확장 방법을 발견했습니다

public static class StreamExtensions 
{ 
    public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<long> progress, CancellationToken cancellationToken = default(CancellationToken), int bufferSize = 0x1000) 
    { 
     var buffer = new byte[bufferSize]; 
     int bytesRead; 
     long totalRead = 0; 
     while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) 
     { 
      await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken); 
      cancellationToken.ThrowIfCancellationRequested(); 
      totalRead += bytesRead; 
      //Thread.Sleep(10); 
      progress.Report(totalRead); 
     } 
    } 
} 

내 응용 프로그램이 작동을하지만 잘못된 진행 정보를 얻을 수 있습니다.

file1.tmp 60.95% 
file2.tmp 98.09% 
file1.tmp 60.98% 
file2.tmp 98.21% 
file2.tmp 98.17% 
file2.tmp 98.25% 
file1.tmp 61.02% 
file2.tmp 98.41% 
file2.tmp downloaded 
file2.tmp 98.29% 
file2.tmp 98.37% 
file1.tmp 61.06% 
file2.tmp 89.27% 
file2.tmp 89.31% 
file2.tmp 98.33% 
file2.tmp 98.45% 
file2.tmp 98.48% 
file1.tmp 61.10% 
file1.tmp 61.14% 
file2.tmp 98.52% 
file1.tmp 61.22% 
file2.tmp 98.60% 
file2.tmp 98.56% 
file1.tmp 61.30% 
file2.tmp 98.88% 
file2.tmp 90.44% 
file1.tmp 61.53% 
file2.tmp 98.72% 
file1.tmp 61.41% 
file1.tmp 61.73% 
file2.tmp 98.80% 
file1.tmp 61.26% 
file1.tmp 61.49% 
file1.tmp 61.57% 
file1.tmp 61.69% 
... 
file1.tmp 99.31% 
file1.tmp 98.84% 
file1.tmp 98.80% 
file1.tmp 99.04% 
file1.tmp 99.43% 
file1.tmp 99.12% 
file1.tmp 99.00% 
file1.tmp downloaded 
file1.tmp 100.00% 
file1.tmp 98.73% 
file1.tmp 98.88% 
file1.tmp 99.47% 
file1.tmp 99.98% 
file1.tmp 99.90% 
file1.tmp 98.96% 
file1.tmp 99.78% 
file1.tmp 99.99% 
file1.tmp 99.74% 
file1.tmp 99.59% 
file1.tmp 99.94% 
file1.tmp 98.49% 
file1.tmp 98.53% 
ALL FILES DOWNLOADED 
file1.tmp 99.55% 
file1.tmp 98.41% 
file1.tmp 99.62% 
file1.tmp 98.34% 
file1.tmp 99.66% 
file1.tmp 98.69% 
file1.tmp 98.37% 

당신은 내가 파일 2 다운로드되는 정보를 가지고 볼 수 있듯이,하지만 난 여전히 파일 1과 동일 CopyToAsync에서 진행 보고서를 얻고있다 : 예를 들어
이 개 파일을 다운로드 나는 출력 창에이를 참조하십시오. 그 때문에

나는 가끔 이상한 콘솔 출력을 얻을 : 그 디버그 정보를 얻을 후

await streamToReadFrom.CopyToAsync(streamToWriteTo, progress, source.Token,0x2000); 
Debug.WriteLine(filename+" downloaded"); 

:

enter image description here

가 이상적으로 내가 호출 할 때보다 확실하고 싶습니다를 진행률이보고되지 않습니다 (파일 다운로드). await이 내 문제를 해결할 것이라고 생각했지만 그렇지 않습니다.

어떻게 해결할 수 있습니까? 임시 해결 방안으로 진행 상황을보고하기 직전에 Thread.Sleep을 CopyToAsync에 추가합니다.

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.IO; 
using System.Linq; 
using System.Net.Http; 
using System.Threading; 
using System.Threading.Tasks; 

namespace AsyncDownloadTest 
{ 
    class Program 
    { 
     private const string LocalPath = @"D:\TEMP"; 

     static void Main() 
     { 
      try 
      { 
       var filesToDownlad = new List<Tuple<string, string>> 
       { 
        new Tuple<string, string>("file1.tmp", "http://ipv4.download.thinkbroadband.com/10MB.zip"), 
        new Tuple<string, string>("file2.tmp", "http://ipv4.download.thinkbroadband.com/10MB.zip") 
       }; 
       _consolePosition = -1; 
       Console.CursorVisible = false; 

       Parallel.ForEach(filesToDownlad, new ParallelOptions { MaxDegreeOfParallelism = 4 }, doc => 
       { 
        DownloadFile(doc.Item2,doc.Item1).Wait(); 
       }); 
       Debug.WriteLine("ALL FILES DOWNLOADED"); 
       Console.CursorVisible = true;  
      } 
      catch (Exception e) 
      { 
       Console.WriteLine(e); 
       Console.ReadLine(); 
      } 
     } 

     private static readonly object ConsoleLock = new object(); 
     private static int _consolePosition; 

     static readonly CancellationTokenSource source = new CancellationTokenSource(); 

     private static async Task DownloadFile(string url, string filename) 
     { 
      int currenctLineNumber = 0; 
      int currectProgress = 0; 

      try 
      { 
       lock (ConsoleLock) 
       { 
        _consolePosition++; 
        currenctLineNumber = _consolePosition; 
       } 

       long fileSize = -1; 

       IProgress<long> progress = new Progress<long>(value => 
       { 
        decimal tmp = (decimal)(value * 100)/fileSize; 

        if (tmp != currectProgress && tmp > currectProgress) 
        { 
         lock (ConsoleLock) 
         { 
          currectProgress = (int)tmp; 
          Console.CursorTop = currenctLineNumber; 
          Console.CursorLeft = 0; 
          Console.Write("{0,10} - {2,11} - {1,6:N2}%", filename, tmp, "DOWNLOADING"); 
         } 
         Debug.WriteLine("{1} {0:N2}%", tmp, filename); 
        } 
       }); 

       using (HttpClient client = new HttpClient()) 
       { 
        using (HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, source.Token)) 
        { 
         response.EnsureSuccessStatusCode(); 
         if (response.Content.Headers.ContentLength.HasValue) fileSize = response.Content.Headers.ContentLength.Value; 

         if (response.Content.Headers.ContentDisposition != null) 
         { 
          var tmp = response.Content.Headers.ContentDisposition.FileName.Replace("\"", ""); 
          Debug.WriteLine("Real name: {0}",tmp); 
         } 

         using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync()) 
         { 
          using (Stream streamToWriteTo = File.Open(Path.Combine(LocalPath, filename), FileMode.Create, FileAccess.Write)) 
          { 
           await streamToReadFrom.CopyToAsync(streamToWriteTo, progress, source.Token,0x2000); 

           Debug.WriteLine(filename+" downloaded"); 

           lock (ConsoleLock) 
           { 
            Console.CursorTop = currenctLineNumber; 
            Console.CursorLeft = 0; 
            var oldColor = Console.ForegroundColor; 
            Console.ForegroundColor = ConsoleColor.Green; 
            Console.Write("{0,10} - {2,11} - {1,6:N2}%", filename, 100, "SUCCESS"); 
            Console.ForegroundColor = oldColor; 
           } 
          } 
         } 
        } 
       } 
      } 
      catch (Exception e) 
      { 
       Debug.WriteLine(e.Message); 
       lock (ConsoleLock) 
       { 
        Console.CursorTop = currenctLineNumber; 
        Console.CursorLeft = 0; 
        var oldColor = Console.ForegroundColor; 
        Console.ForegroundColor = ConsoleColor.Red; 
        Console.Write("{0,10} - {2,11} - {1,6:N2}%", filename, currectProgress, "ERROR"); 
        Console.ForegroundColor = oldColor; 
       } 
      } 
     } 
    } 

    public static class StreamExtensions 
    { 
     public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<long> progress, CancellationToken cancellationToken = default(CancellationToken), int bufferSize = 0x1000) 
     { 
      var buffer = new byte[bufferSize]; 
      int bytesRead; 
      long totalRead = 0; 
      while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) 
      { 
       await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken); 
       cancellationToken.ThrowIfCancellationRequested(); 
       totalRead += bytesRead; 
       Thread.Sleep(10); 
       progress.Report(totalRead); 
      } 
     } 
    } 
} 
+0

나는 이것이 'async'를 사용하는 것과 아무런 관련이 없다고 생각하지만, 콘솔에 출력 할 때 코드가 잘못된 행/열을 설정하는 것과 관련이 있다고 생각합니다. '자물쇠'가있는 것이 잘못되었지만 더 이상 조사하지 않았습니다. – Krumelur

+0

@Krumelur가 콘솔 관련 코드를 제거해도 여전히 VS 출력 창에서'file2.tmp downloaded'를 볼 수 있지만 그 후에도 계속 질문을합니다 (두 번째 코드 섹션 그 파일은'file1.tmp 60.95 %'로 표시됩니다.) – Misiu

+1

'Parallel.ForEach'에서'DownloadFile (doc.Item2, doc.Item1) .Wait();'을주의 깊게 살펴보면,'Parallel' 클래스는 호출 스레드 작업자 스레드 중 하나 인 경우 호출하는 스레드에 'SynchronizationContext'가 있으면 프로그램을 교착 상태로 만듭니다. 'Parallel' 대신에 [TPL Dataflow] (https://msdn.microsoft.com/en-us/library/hh228603 (v = vs.110) .aspx)를보고 싶을 수도 있습니다. 비동기 함수가 전달되도록 지원하여'DownloadFile'을'.Wait()'를 호출하는 대신 직접 전달할 수 있습니다. –

답변

6

귀하의 문제가 실제로 여기에 있습니다 : :

다음은 내 현재 코드입니다

new Progress<long> 

Progress<T> 클래스 always invokes its callbacks in a SynchronizationContext -이 경우 스레드 풀 SynchronizationContext입니다. 즉, 진행보고 코드가 Report이면 호출 스레드 풀에 대한 콜백을 대기 중입니다. 따라서 순서가 맞지 않는 것을 볼 수 있습니다 (또는 다운로드가 실제로 완료된 후에도 계속 들어올 수 있습니다).

이 문제를 해결하려면, 당신은 IProgress<T>의 사용자 정의 구현을 만들 수 있습니다 : 영업 이익 요청

//C#6.0 
public sealed class SynchronousProgress<T> : IProgress<T> 
{ 
    private readonly Action<T> _callback; 
    public SynchronousProgress(Action<T> callback) { _callback = callback; } 
    void IProgress<T>.Report(T data) => _callback(data); 
} 
//older version 
public sealed class SynchronousProgress<T> : IProgress<T> 
{ 
    private readonly Action<T> _callback; 

    public SynchronousProgress(Action<T> callback) 
    { 
     _callback = callback; 
    } 

    void IProgress<T>.Report(T data) 
    { 
     _callback(data); 
    } 
} 

그런 다음

IProgress<long> progress = new SynchronousProgress<long>(value => 
+0

답장을 보내 주셔서 감사합니다. 나는 그것이 그것이 일할 것이라고 생각했기 때문에 Progres를 사용했습니다. 어쩌면 액션이나 간단한 대표가 더 좋을까요? 나는 잠시 후에 당신의 해결책을 시도 할 것이지만, 아마 내가하려는 일을하는 더 좋은 방법이 있습니다. – Misiu

+0

@Misiu : 아니오,'IProgress '이 맞다고 생각합니다. –

1

와 라인

IProgress<long> progress = new Progress<long>(value => 

을 대체 난 방법을 보여줍니다 코멘트에서 TPL Dataflow로 그의 프로그램을 할 수 있습니다. 실제로는 다소 단순한 변환입니다. 먼저 NuGet 패키지 System.Threading.Tasks.Dataflow에 대한 참조를 추가하십시오.그럼 그냥

당신이 동기화 컨텍스트와 프로그램으로이 일을하는 경우
static void Main() 
{ 
    try 
    { 
     var filesToDownlad = new List<Tuple<string, string>> 
     { 
      new Tuple<string, string>("file1.tmp", "http://ipv4.download.thinkbroadband.com/10MB.zip"), 
      new Tuple<string, string>("file2.tmp", "http://ipv4.download.thinkbroadband.com/10MB.zip") 
     }; 
     _consolePosition = -1; 
     Console.CursorVisible = false; 

     var downloadBlock = new ActionBlock<Tuple<string, string>>(doc => DownloadFile(doc.Item2, doc.Item1), 
                    new ExecutionDataflowBlockOptions {MaxDegreeOfParallelism = 4}); 

     foreach (var file in filesToDownlad) 
     { 
      downloadBlock.Post(file); 
     } 
     downloadBlock.Complete(); 
     downloadBlock.Completion.Wait(); 


     Debug.WriteLine("ALL FILES DOWNLOADED"); 
     Console.CursorVisible = true; 
    } 
    catch (Exception e) 
    { 
     Console.WriteLine(e); 
     Console.ReadLine(); 
    } 
} 

에 주요 기능을 변경하면 완료를 기다리고 싶어 대신 동기 작업을하는 게시 당신은 할 수

static async Task Example() 
{ 
    try 
    { 
     var filesToDownlad = new List<Tuple<string, string>> 
     { 
      new Tuple<string, string>("file1.tmp", "http://ipv4.download.thinkbroadband.com/10MB.zip"), 
      new Tuple<string, string>("file2.tmp", "http://ipv4.download.thinkbroadband.com/10MB.zip") 
     }; 
     _consolePosition = -1; 
     Console.CursorVisible = false; 

     var downloadBlock = new ActionBlock<Tuple<string, string>>(doc => DownloadFile(doc.Item2, doc.Item1), 
                    new ExecutionDataflowBlockOptions {MaxDegreeOfParallelism = 4}); 

     foreach (var file in filesToDownlad) 
     { 
      await downloadBlock.SendAsync(file); 
     } 
     downloadBlock.Complete(); 
     await downloadBlock.Completion; 


     Debug.WriteLine("ALL FILES DOWNLOADED"); 
     Console.CursorVisible = true; 
    } 
    catch (Exception e) 
    { 
     Console.WriteLine(e); 
     Console.ReadLine(); 
    } 
} 

단, "ALL FILES DOWNLOADED"문제가 일찌감치 해결되지는 않습니다. Stephen's solution to fix that을 사용해야합니다. 이 코드는 호출 스레드에 SynchronizationContext이있을 수있는 상황에서 실행될 경우 잠재적 인 교착 상태를 해결합니다.

+0

이 코드를 게시 해 주셔서 감사합니다. Dataflow는 어려울 것이라고 생각했지만 코드는 분명해 보입니다. DataFlow는 필자가 반드시 알아야 할 것입니다! – Misiu