Асинхронная функция С# преждевременно возвращается

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

Вот код

    public Form1()
    {
        InitializeComponent();

        Task t = new Task(Repopulate);
        t.Start();
        // some other work here
        t.Wait(); //completes prematurely

    }

    async void Repopulate()
    {           
        var query = ParseObject.GetQuery("Offer");
        IEnumerable<ParseObject> results = await query.FindAsync();

        if (TitleList == null)
            TitleList = new List<string>();
        foreach (ParseObject po in results)
            TitleList.Add(po.Get<string>("title"));
    }

TitleList = null в Form1(), потому что Repopulate() еще не завершен. Поэтому я использовал Wait(). Однако ожидание возвращается до того, как функция будет завершена.

Что я здесь делаю неправильно?


person Grimson    schedule 24.03.2016    source источник


Ответы (1)


Вам нужно изменить тип возвращаемого значения вашего метода Repopulate, чтобы он возвращал задачу, представляющую асинхронную операцию.

Кроме того, вам не следует выполнять асинхронные операции из конструктора формы, поскольку вызов Task.Wait приведет к блокировке вашего потока пользовательского интерфейса (и ваше приложение не будет отвечать на запросы). Вместо этого подпишитесь на его метод Form.Load и выполняйте там асинхронные операции, используя ключевое слово await, чтобы обработчик событий оставался асинхронным. Если вы не хотите, чтобы пользователь взаимодействовал с формой до завершения асинхронной операции, отключите форму в конструкторе и снова включите ее в конце обработчика Load.

private async void Form1_Load(object sender, EventArgs e)
{
    Task t = Repopulate();
    // If you want to run its synchronous part on the thread pool:
    // Task t = Task.Run(() => Repopulate());

    // some other work here
    await t;
}

async Task Repopulate()
{           
    var query = ParseObject.GetQuery("Offer");
    IEnumerable<ParseObject> results = await query.FindAsync();

    if (TitleList == null)
        TitleList = new List<string>();
    foreach (ParseObject po in results)
        TitleList.Add(po.Get<string>("title"));
}

Обновление: для будущих читателей я перерабатываю свои комментарии в ответ:

Task.Wait вызывает блокировку вызывающего потока до завершения задачи. Конструкторы форм и обработчики событий по своей природе работают в потоке пользовательского интерфейса, поэтому вызов Wait внутри них приведет к блокировке потока пользовательского интерфейса. Ключевое слово await, с другой стороны, приведет к тому, что текущий метод передаст управление обратно вызывающей стороне — в случае обработчиков событий это позволит потоку пользовательского интерфейса продолжить обработку событий. Ожидающий метод (обработчик событий) затем возобновит выполнение в потоке пользовательского интерфейса после завершения задачи.

Task.Wait всегда будет блокировать вызывающий поток, независимо от того, вызывается ли он из конструктора или обработчика событий, поэтому его следует избегать, особенно при работе в потоке пользовательского интерфейса. С этой целью в C# 5 были введены ключевые слова async и await; однако они поддерживаются только в методах, а не в конструкторах. Это ограничение лежит в основе основной причины, по которой вам необходимо перенести код инициализации из конструктора формы в обработчик асинхронного события.

Что касается причины преждевременного возврата Task.Wait(): в исходном коде задача t представляет собой выполнение Task, экземпляр которого вы создали в конструкторе формы. Эта задача выполняется Repopulate; однако указанный метод вернется, как только обнаружит первый оператор await, и выполнит остальную часть своей логики в режиме «запустил и забыл». В этом заключается опасность использования async void — вы не узнаете, когда асинхронный метод завершит выполнение. (По этой причине async void следует использовать только для обработчиков событий.) Другими словами, t.Wait() возвращает значение, как только Repopulate достигает своего первого await.

Изменив сигнатуру Repopulate на async Task, вы получите еще одну задачу, представляющую завершение ее асинхронного выполнения, включая асинхронный вызов query.FindAsync() и последующую за ним обработку. Когда Task.Run передается асинхронная операция (Func<Task>) в качестве аргумента, возвращаемая задача будет ожидать (развернуть) внутреннюю задачу. Вот почему следует использовать Task.Run вместо Task.Start или Task.Factory.StartNew.

person Douglas    schedule 24.03.2016
comment
это все еще использует блокирующий вызов Task.Wait() - person sara; 25.03.2016
comment
@kai: Извиняюсь за это; теперь исправлено. - person Douglas; 25.03.2016
comment
Это отлично работает, спасибо. Можете ли вы сказать мне, почему 1) Task.Wait() делает поток пользовательского интерфейса не отвечающим? (в отличие от ожидания) 2) Task.Wait() возвращался преждевременно? (в исходном коде) - person Grimson; 25.03.2016
comment
@Nick_McCoy: Task.Wait вызывает блокировку вызывающего потока до завершения задачи. Конструкторы форм и обработчики событий по своей природе работают в потоке пользовательского интерфейса, поэтому вызов Wait внутри них приведет к его блокировке. С другой стороны, ключевое слово await возвращает управление, позволяя потоку пользовательского интерфейса продолжать обработку событий; а затем возобновить выполнение после завершения задачи. - person Douglas; 25.03.2016
comment
@Douglas Я использовал Task.Wait() в обработчике событий (вместо ожидания), и мой пользовательский интерфейс по-прежнему не отвечал. - person Grimson; 25.03.2016
comment
Да, Task.Wait всегда будет блокировать вызывающий поток, независимо от того, вызывается ли он из конструктора или обработчика событий. Вам нужно было переключиться на обработчик событий намеренно, потому что конструкторы не поддерживают ключевое слово await. - person Douglas; 25.03.2016
comment
О, понял.. И почему Task.Wait() преждевременно вернулся в моем исходном коде? - person Grimson; 25.03.2016
comment
Что касается вашего другого вопроса: ваша задача t представляет собой выполнение Task, экземпляр которого вы создали в конструкторе формы. Эта задача выполняется Repopulate; однако указанный метод вернется, как только встретит первое await, и выполнит остальную часть своей логики в режиме «запустил и забыл». В этом заключается опасность использования async void — вы не будете знать, когда асинхронный метод завершит выполнение. (По этой причине async void следует использовать только для обработчиков событий.) Другими словами, t.Wait() возвращает значение, как только Repopulate достигает своего первого оператора await. - person Douglas; 25.03.2016
comment
Имеет смысл .. Большое спасибо - person Grimson; 25.03.2016