7

リソースを消費するライブラリを作成していますが、何らかの理由で API が異なるスレッドでイベントが発生するように設計されていますが、API の呼び出しはメイン スレッドで行う必要があります。

私が消費しようとしている API が次のように定義されているとしましょう (イベント定義は省略します)。

public sealed class DodgyService
{
    public void MethodThatHasToBeCalledOnTheMainThread() { ... }
}

この API を使用するために、新しいタスクを作成する Service (そう、非常に元の名前) という名前のライブラリにサービスを追加しました ( SynchronizationContext.

これが私の実装です:

public class Service
{
  private readonly TaskFactory _taskFactory;
  private readonly TaskScheduler _mainThreadScheduler;

  public Service(TaskFactory taskFactory, TaskScheduler mainThreadScheduler)
  {
      _taskFactory = taskFactory;
      _mainThreadScheduler = mainThreadScheduler;
  }

  // Assume this method can be called from any thread.
  // In this sample is called by the main thread but most of the time
  // the caller will be running on a background thread.
  public Task ExecuteAsync(string taskName)
  {
      return _taskFactory.StartNew(
          () => ReallyLongCallThatForWhateverStupidReasonHasToBeCalledOnMainThread(taskName),
          new CancellationToken(false), TaskCreationOptions.None, _mainThreadScheduler)
          .ContinueWith(task => Trace.TraceInformation("ExecuteAsync has completed on \"{0}\"...", taskName));
  }

  private void ReallyLongCallThatForWhateverStupidReasonHasToBeCalledOnMainThread(string taskName)
  {
      Trace.TraceInformation("Starting \"{0}\" really long call...", taskName);
      new DodgyService().MethodThatHasToBeCalledOnTheMainThread();
      Trace.TraceInformation("Finished \"{0}\" really long call...", taskName);
  }

}

ここで、(メイン スレッドで) サービスの呼び出しを実行し、メイン スレッドで待機しようとすると、メイン スレッドで実行するようにスケジュールされたタスクをメイン スレッドが待機するため、アプリケーションはデッドロックに入ります。

プロセス全体をブロックせずにこれらの呼び出しをメイン スレッドにマーシャリングするにはどうすればよいですか?

ある時点で、新しいタスクを作成する前にメインスレッドの検出を実行することを考えましたが、これをハックしたくありません。

興味のある人のために、コードと問題を示す WPF アプリの要点をここで入手しました。

ちなみに、ライブラリは.net Framework 4.0で作成する必要があります

編集!ここに提供されているように、スコット・チェンバレンから提供されたアドバイスに従って問題を解決しました

4

3 に答える 3

8

メインスレッドがタスクを待っているため

それは保証されたデッドロックです。タスクは、アイドル状態になり、ディスパッチャー ループを実行する (つまり、メッセージ ループをポンピングする) まで、メイン スレッドで実行できません。特定のスレッドでコードを実行する魔法を実装するのは、そのディスパッチャ ループです。ただし、メインスレッドはアイドル状態ではなく、「タスクを待機中」です。したがって、メイン スレッドがアイドル状態にならないため、タスクを完了できません。タスクが完了しないため、メイン スレッドはアイドル状態になりません。デッドロックシティ。

メインスレッドが待機しないように、コードを書き直す必要があります。待機呼び出しの後に表示されるコードは、ReallyLongCall() のように、メイン スレッドで実行される別のタスクに移動します。

タスクを使用してもマイレージがまったく得られないように見えることに注意してください。スニペットは、重要なコードがワーカースレッドで実行されていないことを示唆しています。したがって、直接呼び出すこともでき、問題も解決します。

于 2013-11-11T22:53:06.587 に答える
2

サンプルプログラムから:

  private void HandleClosed(object sender, EventArgs e)
  {
      var list = new[]
      {
          _service.ExecuteAsync("first task"),
          _service.ExecuteAsync("second task"),
          _service.ExecuteAsync("third task")
      };

      //uncommenting this line blocks all three previous activities as expected
      //as it drives the current main thread to wait for other tasks waiting to be executed by the main thread.

      //Task.WaitAll(list);
  }

Task.WaitAllブロッキング呼び出しである場合、メイン スレッドでブロッキング呼び出しを実行できないか、デッドロックが発生します。できること (Visual Studio 2012 以降を使用している場合) は、.Net 4.0Microsoft.Bcl.Asyncをサポートする NuGet パッケージを使用することです。async/await

パッケージを追加した後、コードを次のように変更します

private async void HandleClosed(object sender, EventArgs e)
{
    var list = new[]
  {
      _service.ExecuteAsync("first task"),
      _service.ExecuteAsync("second task"),
      _service.ExecuteAsync("third task")
  };

    //uncommenting this line blocks all three previous activities as expected
    //as it drives the current main thread to wait for other tasks waiting to be executed by the main thread.

    await TaskEx.WhenAll(list);
}

そして、あなたのプログラムはもはやデッドロックしません(その後もコードを実行しませんawait TaskEx.WhenAll(list);が、これは、このコードがシャットダウンプロセス中に実行awaitされているためであり、クリックイベントのように他の場所に配置された場合、シャットダウンの処理を続行できるためです。より正常な動作が見られます)。


別のオプションは、2 番目の「メイン スレッド」を用意し、それに作業をディスパッチすることです。多くの場合、何かを "メイン" スレッドで実行する必要がある場合、実際には " オブジェクトが最初に" スレッドで作成されたことを示す STA Windows メッセージで実行する必要があると言っています。その方法の例を次に示します(ここから取得)

private void runBrowserThread(Uri url) {
    var th = new Thread(() => {
        var br = new WebBrowser();
        br.DocumentCompleted += browser_DocumentCompleted;
        br.Navigate(url);
        Application.Run();
    });
    th.SetApartmentState(ApartmentState.STA);
    th.Start();
}

void browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) {
    var br = sender as WebBrowser;
    if (br.Url == e.Url) {
        Console.WriteLine("Natigated to {0}", e.Url);
        Application.ExitThread();   // Stops the thread
    }
}
于 2013-11-11T22:54:54.477 に答える
0

@HansPassantは正しいです。ディスパッチャ スレッドがタスクを待機するのをブロックすることで、タスクが実行されないようにします。おそらく行うことができる最も簡単な変更は、次のものに置き換えることTask.WaitAll(list)です。

_taskFactory.ContinueWhenAll(
    list,
    tasks => { /* resume here */ });

...そして、への呼び出しに続くコードをWaitAll()継続に移動します。タスクの結果を確認し、発生した可能性のある例外に適切に対応することを忘れないでください。

しかし、あなたのコード例では明らかにされていないタスクを使用することによる具体的な利点がない限り、ハンスのアドバイスに耳を傾け、同期呼び出しを優先して単純にタスクを放棄します。

于 2013-11-12T01:26:02.387 に答える