Понимание async/wait и Task.Run()

Я думал, что я понял, async/await и Task.Run() довольно хорошо, пока я не пришел на этот вопрос:

Я программирую приложение Xamarin.Android, используя RecyclerView с ViewAdapter. В моем методе OnBindViewHolder я попытался выполнить асинхронную загрузку некоторых изображений

public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
    // Some logic here

    Task.Run(() => LoadImage(postInfo, holder, imageView).ConfigureAwait(false)); 
}

Затем в моей функции LoadImage я сделал что-то вроде:

private async Task LoadImage(PostInfo postInfo, RecyclerView.ViewHolder holder, ImageView imageView)
{                
    var image = await loadImageAsync((Guid)postInfo.User.AvatarID, EImageSize.Small).ConfigureAwait(false);
    var byteArray = await image.ReadAsByteArrayAsync().ConfigureAwait(false);

    if(byteArray.Length == 0)
    {
        return;
    }

    var bitmap = await GetBitmapAsync(byteArray).ConfigureAwait(false);

    imageView.SetImageBitmap(bitmap);
    postInfo.User.AvatarImage = bitmap;
}

Эти фрагменты кода работали. Но почему?

То, что я узнал, после ожидания configure установлено в значение false, код не запускается в SynchronizationContext (который является потоком пользовательского интерфейса).

Если я сделаю метод OnBindViewHolder асинхронным и OnBindViewHolder использовать вместо Task.Run, код выйдет из строя

imageView.SetImageBitmap(bitmap);

Говорить, что это не в потоке пользовательского интерфейса, что имеет для меня совершенно смысл.

Так почему же async/await аварии коды в то время как Task.Run() не делает?

Обновление: ответ

Поскольку Task.Run не ожидалось, исключенное исключение не было показано. Если я ожидаю Task.Run, я ожидал ошибку. Дальнейшие объяснения можно найти в ответах ниже.

Ответ 1

Это так же просто, как вы не ожидаете Task.Run, поэтому исключение попадает и не возвращается на сайт вызова Task.Run.

Добавьте "ждут" перед Task.Run, и вы получите исключение.

Это не приведет к сбою вашего приложения:

private void button1_Click(object sender, EventArgs e)
{
    Task.Run(() => { throw new Exception("Hello");});
}

Это приведет к сбою вашего приложения:

private async void button1_Click(object sender, EventArgs e)
{
   await Task.Run(() => { throw new Exception("Hello");});
}

Ответ 2

Task.Run() и поток пользовательского интерфейса должны использоваться для другой цели:

  • Task.Run() следует использовать для связанных с CPU методов.
  • UI-Thread должен использоваться для связанных с UI методов.

Перемещая свой код в Task.Run(), вы избегаете блокировки потока пользовательского интерфейса. Это может решить вашу проблему, но это не самая лучшая практика, потому что это плохо для вашей работы. Task.Run() блокирует поток в пуле потоков.

Вместо этого вам следует называть ваш связанный с UI метод в потоке пользовательского интерфейса. В Xamarin вы можете запускать материал в потоке пользовательского интерфейса с помощью Device.BeginInvokeOnMainThread():

// async is only needed if you need to run asynchronous code on the UI thread
Device.BeginInvokeOnMainThread(async () =>
{
    await LoadImage(postInfo, holder, imageView).ConfigureAwait(false)
});

Причина, по которой он работает, даже если вы явно не называете его в потоке пользовательского интерфейса, вероятно, потому, что Xamarin каким-то образом обнаруживает, что это то, что должно запускаться в потоке пользовательского интерфейса, и переносит эту работу на поток пользовательского интерфейса.

Вот несколько полезных статей Стивена Клири, которые помогли мне написать этот ответ и помогут вам понять асинхронный код:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Ответ 3

Вероятно, доступ к UI по-прежнему вызывает UIKitThreadAccessException. Вы не наблюдаете это, потому что не используете ключевое слово await или Task.Wait() для маркера, Task.Run() возвращает Task.Run(). См. Catch исключение, вызванное обсуждением метода async в StackOverflow, документация MSDN по этой теме немного устарела.

Вы можете присоединить продолжение к маркеру, который Task.Run() возвращает и проверяет исключения, брошенные внутри пройденного действия:

Task marker = Task.Run(() => ...);
marker.ContinueWith(m =>
{
    if (!m.IsFaulted)
        return;

    // Marker Faulted status indicates unhandled exceptions. Observe them.
    AggregateException e = m.Exception;
});

В общем, доступ к пользовательскому интерфейсу из неинтерфейса может сделать приложение неустойчивым или сбой, но это не гарантируется.

Для получения дополнительной информации проверьте, как обрабатывать Task.Run Exception, Android - Проблема с обсуждениями асинхронных задач в StackOverflow, Значение статьи TaskStatus Стивена Тууба и Работа с статьей потока пользовательского интерфейса в Microsoft Docs.

Ответ 4

Task.Run LoadImage для выполнения асинхронного процесса в пуле потоков с помощью ConfigureAwait(false). Задача, LoadImage возвращает LoadImage, НЕ ожидается, хотя я считаю, что это важная часть здесь.

Таким образом, результаты Task.Run том, что он немедленно возвращает Task<Task>, но внешняя задача не имеет ConfigureAwait(false), поэтому вся вещь решается на основном потоке.

Если вы измените свой код на

Task.Run(async () => await LoadImage(postInfo, holder, imageView).ConfigureAwait(false)); 

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