Разница между использованием Task.Yield () в пользовательском интерфейсе и консольном приложении - PullRequest
0 голосов
/ 20 декабря 2018

Я пытаюсь асинхронно отобразить форму прогресса, в которой говорится, что приложение работает во время работы самого приложения.

Как следует этот вопрос , у меня есть следующее:

Основная форма:

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    async Task<int> LoadDataAsync()
    {
        await Task.Delay(2000);
        return 42;
    }

    private async void Run_Click(object sender, EventArgs e)
    {
        var runningForm = new RunningForm();

        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.Close();
        await progressFormTask;

        MessageBox.Show(data.ToString());
    }
}

ПрогрессФорма

public partial class RunningForm : Form
{
    private readonly SynchronizationContext synchronizationContext;

    public RunningForm()
    {
        InitializeComponent();
        synchronizationContext = SynchronizationContext.Current;
    }

    public async void ShowRunning()
    {
        this.RunningLabel.Text = "Running";
        int dots = 0;

        await Task.Run(() =>
        {
            while (true)
            {
                UpadateUi($"Running{new string('.', dots)}");

                Thread.Sleep(300);

                dots = (dots == 3) ? 0 : dots + 1;
            }
        });
    }

    public void UpadateUi(string text)
    {
        synchronizationContext.Post(
            new SendOrPostCallback(o =>
            {
                this.RunningLabel.Text = text;
            }),
            text);
    }

    public void CloseThread()
    {
        synchronizationContext.Post(
            new SendOrPostCallback(o =>
            {
                this.Close();
            }),
            null);
    }
}

internal static class DialogExt
{
    public static async Task<DialogResult> ShowDialogAsync(this Form form)
    {
        await Task.Yield();
        if (form.IsDisposed)
        {
            return DialogResult.OK;
        }
        return form.ShowDialog();
    }
}

Выше работает нормально, но это не работает, когда я звоню извне другого из.Это мое консольное приложение:

class Program
{
    static void Main(string[] args)
    {
        new Test().Run();
        Console.ReadLine();
    }
}

class Test
{
    private RunningForm runningForm;

    public async void Run()
    {
        var runningForm = new RunningForm();

        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.CloseThread();

        await progressFormTask;

        MessageBox.Show(data.ToString());
    }

    async Task<int> LoadDataAsync()
    {
        await Task.Delay(2000);
        return 42;
    }
}

Наблюдая за тем, что происходит с отладчиком, процесс переходит к await Task.Yield() и никогда не переходит к return form.ShowDialog(), поэтому вы никогда не увидите RunningForm.Затем процесс переходит на LoadDataAsync() и навсегда зависает на await Task.Delay(2000).

Почему это происходит?Имеет ли это какое-то отношение к тому, как приоритеты Task (то есть: Task.Yield())?

1 Ответ

0 голосов
/ 21 декабря 2018

Наблюдая за тем, что происходит с отладчиком, процесс ожидает Task.Yield () и никогда не переходит к возвращению form.ShowDialog (), и, таким образом, вы никогда не увидите RunningForm.Затем процесс переходит к LoadDataAsync () и навсегда зависает в ожидании Task.Delay (2000).

Почему это происходит?

Что происходит здесь, когда вы делаете var runningForm = new RunningForm() в потоке консоли без какого-либо контекста синхронизации (System.Threading.SynchronizationContext.Current имеет значение null), он неявно создает экземпляр WindowsFormsSynchronizationContext и устанавливает его в текущий поток, подробнее об этом здесь .

Затем, когда вы нажимаете await Task.Yield(), метод ShowDialogAsync возвращается вызывающей стороне, и продолжение await публикуется в этом новом контексте синхронизации.Однако у продолжения никогда не будет возможности быть вызванным, потому что текущий поток не запускает цикл сообщений и отправленные сообщения не перекачиваются.Нет тупика, но код после await Task.Yield() никогда не выполняется, поэтому диалог даже не отображается.То же самое относится и к await Task.Delay(2000).

Мне больше интересно узнать, почему он работает для WinForms, а не для консольных приложений.

Вам нужен поток пользовательского интерфейсас циклом сообщений в вашем консольном приложении.Попробуйте рефакторинг консольного приложения следующим образом:

public void Run()
{
    var runningForm = new RunningForm();
    runningForm.Loaded += async delegate 
    {
        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.Close();

        await progressFormTask;

        MessageBox.Show(data.ToString());
    };
    System.Windows.Forms.Application.Run(runningForm);
}

Здесь задача Application.Run - запустить модальный цикл сообщений (и установить WindowsFormsSynchronizationContext в текущем потоке), а затем показать форму.Асинхронный обработчик runningForm.Loaded вызывается в этом контексте синхронизации, поэтому логика внутри него должна работать точно так же, как и ожидалось.

Это, однако, делает Test.Run синхронным методом , т.е.возвращается только тогда, когда форма закрыта и цикл сообщений завершен.Если это не то, что вам нужно, вам нужно создать отдельный поток для запуска вашего цикла сообщений, что-то вроде того, что я делаю с MessageLoopApartment здесь .

Тем не менее, в типичных приложениях WinForms или WPF почти никогда не требуется дополнительный поток пользовательского интерфейса.

...