2015-01-04 3 views
-6

아래 코드는 WPF 응용 프로그램이 멈추는 (데드 록 가능성이 높음) 원인으로 인해 발생합니다. DownloadStringAsTask 메서드가 별도의 (비 UI) 스레드에서 실행되는지 확인했습니다. 당신이 메시지 박스의 행의 주석을 해제하면 흥미롭게도 (단지 반면에 호출하기 전에 (tasks.Any는()), 응용 프로그램이 잘 작동합니다. 사람도 이유를 설명하는 첫 번째 장소에서 애플리케이션 hungs을 할 수 있습니다이 문제를 해결하는 방법을?이로 인해 응용 프로그램이 멈추는 이유는 무엇입니까?

<Window x:Class="WpfApplication1.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     Title="MainWindow" Height="350" Width="525"> 
    <Grid> 
     <Grid.RowDefinitions> 
      <RowDefinition Height="9*" /> 
      <RowDefinition Height="*" /> 
     </Grid.RowDefinitions> 
     <Frame x:Name="frame" Grid.Row="0" /> 
     <StatusBar VerticalAlignment="Bottom" Grid.Row="1" > 
      <StatusBarItem> 
       <TextBlock Name="tbStatusBar" Text="Waiting for getting update" /> 
      </StatusBarItem> 
     </StatusBar> 
    </Grid> 
</Window> 



public partial class MainWindow : Window 
{ 
    List<string> URLsToProcess = new List<string> 
     { 
      "http://www.microsoft.com", 
      "http://www.stackoverflow.com", 
      "http://www.google.com", 
      "http://www.apple.com", 
      "http://www.ebay.com", 
      "http://www.oracle.com", 
      "http://www.gmail.com", 
      "http://www.amazon.com", 
      "http://www.outlook.com", 
      "http://www.yahoo.com", 
      "http://www.amazon124.com", 
      "http://www.msn.com" 
     }; 

    public MainWindow() 
    { 
     InitializeComponent(); 
     ProcessURLs(); 
    } 

    public void ProcessURLs() 
    { 
     var tasks = URLsToProcess.AsParallel().Select(uri => DownloadStringAsTask(new Uri(uri))).ToArray(); 
     //MessageBox.Show("this is doing some magic"); 
     while (tasks.Any()) 
     { 
      try 
      { 
       int index = Task.WaitAny(tasks); 
       this.tbStatusBar.Text = string.Format("{0} has completed", tasks[index].AsyncState.ToString()); 
       tasks = tasks.Where(t => t != tasks[index]).ToArray(); 
      } 
      catch (Exception e) 
      { 
       foreach (var t in tasks.Where(t => t.Status == TaskStatus.Faulted)) 
        this.tbStatusBar.Text = string.Format("{0} has completed", t.AsyncState.ToString()); 
       tasks = tasks.Where(t => t.Status != TaskStatus.Faulted).ToArray(); 
      } 
     } 
    } 

    private Task<string> DownloadStringAsTask(Uri address) 
    { 
     TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(address); 
     WebClient client = new WebClient(); 
     client.DownloadStringCompleted += (sender, args) => 
     { 
      if (args.Error != null) 
       tcs.SetException(args.Error); 
      else if (args.Cancelled) 
       tcs.SetCanceled(); 
      else 
       tcs.SetResult(args.Result); 
     }; 
     client.DownloadStringAsync(address); 
     return tcs.Task; 
    } 
} 

답변

1

여기서 가장 큰 문제는 모든 작업이 완료 될 때까지 생성자는 반환하지 않습니다. 창을 그리는 것과 관련된 창 메시지는 처리되지 않으므로 생성자가 반환 할 때까지 창은 표시되지 않습니다.

여기에는 실제로 "교착 상태"가 없습니다. 대신 오래 기다렸다가 (즉 모든 작업이 완료 될 때까지) 창을 실제로 표시합니다.

MessageBox.Show()에 대한 호출을 추가하면 UI 스레드에 창 메시지 대기열을 처리 할 기회가 제공됩니다. 즉, 일반 모달 대화 상자에는 스레드 메시지 펌프가 포함되어있어 창을 표시하는 것과 관련된 메시지를 포함하여 대기열에있는 메시지를 처리합니다. MessageBox.Show()을 추가하더라도 처리가 진행됨에 따라 창이 업데이트되지 않습니다. UI 스레드를 다시 차단하기 전에 창을 표시 할 수 있습니다.

이 문제를 해결하는 한 가지 방법은 async/await 패턴으로 전환하는 것입니다. 예를 들어 :

public MainWindow() 
{ 
    InitializeComponent(); 
    var _ = ProcessURLs(); 
} 

public async Task ProcessURLs() 
{ 
    List<Task<string>> tasks = URLsToProcess.Select(uri => DownloadStringAsTask(new Uri(uri))).ToList(); 

    while (tasks.Count > 0) 
    { 
     Task<string> task = await Task.WhenAny(tasks); 
     string messageText; 

     if (task.Status == TaskStatus.RanToCompletion) 
     { 
      messageText = string.Format("{0} has completed", task.AsyncState); 
      // TODO: do something with task.Result, i.e. the actual downloaded text 
     } 
     else 
     { 
      messageText = string.Format("{0} has completed with failure: {1}", task.AsyncState, task.Status); 
     } 

     this.tbStatusBar.Text = messageText; 
     tasks.Remove(task); 
    } 

    tbStatusBar.Text = "All tasks completed"; 
} 

나는 async 방법과 ProcessURLs() 방법을 다시 작성했습니다. 즉, 생성자가이를 호출 할 때 첫 번째 await 문까지 동 기적으로 실행됩니다.이 시점에서 현재 스레드가 정상적으로 계속 진행될 수 있습니다.

Task.WhenAny()에 대한 호출이 완료되면 (즉, 모든 작업이 완료되면) 런타임은 UI 스레드에서 계속을 호출하여 ProcessURLs() 메서드 실행을 다시 시작합니다. 이렇게하면 메소드가 UI 객체 (예 : this.tbStatusBar.Text)에 일반적으로 액세스 할 수 있으며 UI 스레드는 완료를 처리 할 수있을만큼 길게 차지합니다.

루프가 위로 돌아오고 Task.WhenAny() 메서드가 다시 호출되면 전체 시퀀스가 ​​반복됩니다 (즉 루프가 작동하는 방식입니다 :)).

일부 다른 노트 :

  • 생성자의 var _ = 비트가 Task 반환 값이 무시 될 때, 그렇지 않으면 발생할 수있는 컴파일러 경고를 억제 할 수있다.
  • IMHO, 이러한 작업을 생성자에서 초기화하지 않는 것이 좋습니다.생성자는 이와 같이 중요한 작업을 수행하는 데 일반적으로 나쁜 장소입니다. 대신 OnActivated() 메서드를 재정 의하여 (예 : async) await 문을 사용하여 ProcessURLs() (즉, async 메서드를 호출하는 관용적 방법) 문을 사용할 수 있습니다. 이렇게하면 다른 처리를 시작하기 전에 창이 완전히 초기화되고 표시됩니다. 이 특정 예에서

은 생성자에서 처리를 시작하면 async/await를 사용하는 같은 UI 관련 물건 수있을 것되지 않기 때문에 아마 정말, 한, 아무것도 다치게하지 않을 것입니다 어쨌든 최소한 생성자가 반환 될 때까지 실행되어야한다. 난 그냥 일반적으로 생성자에서 이런 일을 피하기 위해 노력하고 있습니다.

  • 나는 또한 당신의 작업 컬렉션의 일반적인 처리를 약간 더 적절하다고 생각되는 것으로 수정했다. tasks 컬렉션의 반복 된 다시 초기화를 제거하고 WhenAny() 메서드의 의미를 이용합니다. AsParallel()도 삭제했습니다. 프로세싱의 장기 실행 부분이 이미 비동기 적으로 처리되었으므로 Select() 자체를 병렬화하려는 시도에서 이점이없는 것으로 보입니다.
+0

'대신, 충분히 기다렸다가 (즉, 모든 작업이 완료 될 때까지), 창은 실제로 잘못 표시됩니다. 코드를 실행 해보십시오. – Lanorkin

+0

@ Lanorkin : 코드를 실행했고 설명하는대로 작동했습니다. –

1

요령에 대한 원인은 동기화 및 asnyc 코드를 혼합 WaitAny를 호출하는 것입니다. 스티븐 클리어 리는 작업과 일반적인 문제를 이해하는 데 유용 게시물이 있습니다. 여기 Best Practices in Asynchronous Programming

이 코드를 단순화하고 병렬 사용하는 솔루션입니다 .ForEach

코드

public partial class WaitAnyWindow : Window { 

    private List<string> URLsToProcess = new List<string> 
     { 
      "http://www.microsoft.com", 
      "http://www.stackoverflow.com", 
      "http://www.google.com", 
      "http://www.apple.com", 
      "http://www.ebay.com", 
      "http://www.oracle.com", 
      "http://www.gmail.com", 
      "http://www.amazon.com", 
      "http://www.outlook.com", 
      "http://www.yahoo.com", 
      "http://www.amazon.com", 
      "http://www.msn.com" 
     }; 

    public WaitAnyWindow02() { 
    InitializeComponent(); 
    Parallel.ForEach(URLsToProcess, (x) => DownloadStringFromWebsite(x)); 
    } 

    private bool DownloadStringFromWebsite(string website) { 
    WebClient client = new WebClient(); 
    client.DownloadStringCompleted += (s, e) => 
    { 
     if (e.Error != null) 
     { 
     Dispatcher.BeginInvoke((Action)(() => 
     { 
      this.tbStatusBar.Text = string.Format("{0} didn't complete because {1}", website, e.Error.Message); 
     })); 
     } 
     else 
     { 
     Dispatcher.BeginInvoke((Action)(() => 
     { 
      this.tbStatusBar.Text = string.Format("{0} has completed", website); 
     })); 
     } 
    }; 

    client.DownloadStringAsync(new Uri(website)); 

    return true; 
    } 
} 
+0

FWIW, 저는 Peter가 생성자에 코드를 넣지 않는 것에 동의합니다. –