13

ReadAllLinesAsync非同期機能を使用して呼び出されるメソッドを実装しようとしています。次のコードを作成しました。

private static async Task<IEnumerable<string>> FileReadAllLinesAsync(string path)
{
    using (var reader = new StreamReader(path))
    {
        while ((await reader.ReadLineAsync()) != null)
        {

        }
    }
    return null;
}

private static void Main()
{
    Button buttonLoad = new Button { Text = "Load File" };
    buttonLoad.Click += async delegate
    {
        await FileReadAllLinesAsync("test.txt"); //100mb file!
        MessageBox.Show("Complete!");
    };

    Form mainForm = new Form();
    mainForm.Controls.Add(buttonLoad);
    Application.Run(mainForm);
}

リストされたコードが非同期で実行されることを期待していますが、実際にはそうです! ただし、Visual Studio Debuggerを使用せずにコードを実行した場合のみです。

Visual Studio Debugger をアタッチしてコードを実行すると、コードが同期的に実行され、メイン スレッドがブロックされて UI がハングします。

3 台のマシンで問題の再現を試み、成功しました。各テストは、Visual Studio 2012 を使用して 64 ビット マシン (Windows 8 または Windows 7) で実施されました。

この問題が発生する理由と解決方法を知りたいです (デバッガーなしで実行すると開発が妨げられる可能性があるため)。

4

4 に答える 4

8

私はあなたと同じ問題をある程度見ていますが、それはある程度だけです。私にとって、UI はデバッガーで非常にぎくしゃくしていますが、デバッガーではない場合もあります。(ちなみに、私のファイルは 10 文字の多数の行で構成されています。データの形状によって、ここで動作変わります。) 多くの場合、デバッガーでは、最初は良い結果が得られますが、その後長い間問題が発生し、その後回復することもあります。

ディスクが速すぎて回線が短すぎることが問題なのかもしれません。ばかげているように聞こえるので、説明させてください...

await式を使用すると、必要な場合にのみ「継続を添付」パスを通過します。結果が既に存在する場合、コードは単に値を抽出し、同じスレッドで続行します。

つまり、ReadLineAsync常にタスクが返され、それが返されるまでに完了している場合、効果的に同期動作が見られます。ReadLineAsyncすでにバッファリングされているデータを見て、その中の行を同期的に見つけて開始しようとする可能性は十分にあります。オペレーティング システムは、アプリケーションが使用できるように、ディスクからさらに多くのデータを読み取る可能性があります。これは、UI スレッドが通常のメッセージを送信する機会を得られないため、UI がフリーズすることを意味します。

ネットワーク経由で同じコードを実行すると問題が「修正」されると予想していましたが、そうではないようでした。(ジャーキネスの表示方法が正確に変わりますのでご注意ください。) ただし、以下を使用します。

await Task.Delay(1);

UIのフリーズを解除します。(Task.Yieldそうではありませんが、これもまた私を大いに混乱させます。これは、継続と他のUIイベントの間の優先順位の問題であると思われます。)

これがデバッガーでしか表示されない理由については、まだ混乱しています。おそらく、デバッガで割り込みが処理される方法と関係があり、タイミングが微妙に変化します。

これらは推測にすぎませんが、少なくともある程度は知識のあるものです。

編集:わかりました、少なくとも部分的にそれと関係があることを示す方法を考え出しました. メソッドを次のように変更します。

private static async Task<IEnumerable<string>>
    FileReadAllLinesAsync(string path, Label label)
{
    int completeCount = 0;
    int incompleteCount = 0;
    using (var reader = new StreamReader(path))
    {
        while (true)
        {
            var task = reader.ReadLineAsync();
            if (task.IsCompleted)
            {
                completeCount++;
            }
            else
            {
                incompleteCount++;
            }
            if (await task == null)
            {
                break;
            }
            label.Text = string.Format("{0} / {1}",
                                       completeCount,
                                       incompleteCount);
        }
    }
    return null;
}

... 適切なラベルを作成して UI に追加します。私のマシンでは、デバッグと非デバッグの両方で、 「不完全」よりもはるかに多くの「完全」ヒットが見られます。奇妙なことに、完全と不完全の比率は、デバッガーの下でもそうでない場合でも一貫して 84:1 です。したがって、UIが更新されるのは、約 85 行に 1 行を読み取った後でのみです。マシンで同じことを試してください。

別のテストとして、イベントでインクリメントするカウンターを追加しましlabel.Paintた。デバッガーでは、同じ行数に対して、デバッガーでは実行されなかった回数の 1/10 しか実行されませんでした。

于 2013-07-30T20:57:15.550 に答える
2

Visual Studio は、実際には非同期コールバックを同期的に実行していません。ただし、コードは、UI スレッドで実行する必要のないメッセージで UI スレッドを「フラッディング」するような構造になっています。具体的には、 がループFileReadAllLinesAsyncの本体で実行を再開する場合、同じメソッドの行でキャプチャされた で再開します。これが意味することは、ファイル内のすべての行に対して、その while ループの本体の 1 つのコピーを実行するようにメッセージが UI スレッドに戻されるということです。whileSynchronizationContextawait

この問題は、ConfigureAwait(false)慎重に使用することで解決できます。

  1. ではFileReadAllLinesAsync、while ループの本体は、それが実行されるスレッドに依存しないため、代わりに次を使用できます。

    while ((await reader.ReadLineAsync().ConfigureAwait(false)) != null)
    
  2. で、行を UI スレッドで実行する必要Mainあるとします (おそらく、UI スレッドにもステートメントがあります)。を使用していないため、 を変更しなくてもこの動作を取得できます (そしてそうなるでしょう!) 。MessageBox.ShowbuttonLoad.Enabled = trueMainConfigureAwait(false)

デバッガーで観察された遅延は、デバッガーがアタッチされている間、マネージ/アンマネージ コードでの .NET のパフォーマンスが低下したことが原因であると思われるため、デバッガーがアタッチされていると、これらの数百万のメッセージのそれぞれを UI スレッドにディスパッチするのが最大 100 倍遅くなります。 . 機能を無効にしてディスパッチを高速化しようとするのではなく、上記の項目 1 で問題の大部分がすぐに解決されると思います。

于 2013-08-01T12:29:02.960 に答える
1

Microsoft ダウンロード センターのタスクベースの非同期パターンから:

パフォーマンス上の理由から、タスクが待機するまでにタスクが既に完了している場合、制御は渡されず、代わりに関数の実行が続行されます。

場合によっては、操作を完了するのに必要な作業量が、操作を非同期で起動するのに必要な作業量よりも少ないことがあります (たとえば、既にメモリにバッファリングされているデータによって読み取りが満たされるストリームからの読み取りなど)。このような場合、操作は同期的に完了し、既に完了した Task を返すことがあります。

したがって、私の最後の答えは正しくありませんでした (パフォーマンス上の理由から、短時間の非同期操作は同期的です)。

于 2013-07-30T10:34:09.957 に答える