Je pensais avoir bien compris async
/await
et Task.Run()
jusqu'à ce que je tombe sur ce problème:
Je programme une application Xamarin.Android en utilisant un RecyclerView
avec un ViewAdapter
. Dans ma OnBindViewHolder méthode, j'ai essayé de charger asynchrone quelques images
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
// Some logic here
Task.Run(() => LoadImage(postInfo, holder, imageView).ConfigureAwait(false));
}
Ensuite, dans ma fonction LoadImage j'ai fait quelque chose comme:
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;
}
Ces morceaux de code ont fonctionné . Mais pourquoi?
Ce que j'ai appris, une fois la configuration de l'attente définie sur false, le code ne s'exécute pas dans le SynchronizationContext
(qui est le thread d'interface utilisateur).
Si je fais asynchroniser la méthode OnBindViewHolder
et que j'attends au lieu de Task.Run, le code se bloque sur
imageView.SetImageBitmap(bitmap);
Dire que ce n'est pas dans le fil de l'interface utilisateur, ce qui est tout à fait logique pour moi.
Alors pourquoi le code async
/await
plante-t-il alors que Task.Run () ne le fait pas?
Mise à jour: réponse
Étant donné que le Task.Run n'était pas attendu, l'exception levée n'a pas été affichée. Si j'attends le Task.Run, il y a eu l'erreur que j'attendais. De plus amples explications se trouvent dans les réponses ci-dessous.
C'est aussi simple que vous n'attendez pas le Task.Run, donc l'exception est mangée et n'est pas renvoyée sur le site d'appel de Task.Run.
Ajoutez "attendre" devant le Task.Run, et vous obtiendrez l'exception.
Cela ne fera pas planter votre application:
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() => { throw new Exception("Hello");});
}
Cependant, cela bloquera votre application:
private async void button1_Click(object sender, EventArgs e)
{
await Task.Run(() => { throw new Exception("Hello");});
}
Task.Run()
et le thread d'interface utilisateur doivent être utilisés dans un but différent:
Task.Run()
doit être utilisé pour les méthodes liées au processeur .En déplaçant votre code dans Task.Run()
, vous évitez que le thread d'interface utilisateur ne soit bloqué. Cela peut résoudre votre problème, mais ce n'est pas la meilleure pratique car c'est mauvais pour vos performances. Task.Run()
bloque un thread dans le pool de threads.
Ce que vous devez faire à la place est d'appeler votre méthode liée à l'interface utilisateur sur le thread d'interface utilisateur. Dans Xamarin, vous pouvez exécuter des éléments sur le thread d'interface utilisateur en utilisant 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)
});
La raison pour laquelle cela fonctionne même si vous ne l'appelez pas explicitement sur le thread d'interface utilisateur est probablement parce que Xamarin détecte en quelque sorte que c'est quelque chose qui devrait s'exécuter sur le thread d'interface utilisateur et déplace ce travail vers le thread d'interface utilisateur.
Voici quelques articles utiles de Stephen Cleary qui m'ont aidé à écrire cette réponse et qui vous aideront à mieux comprendre le code asynchrone:
https://blog.stephencleary.com/2013/11/taskrun-Etiquette-examples-dont-use.htmlhttps://blog.stephencleary.com/2013/11/ taskrun-Etiquette-examples-using.html
L'accès à l'interface utilisateur génère probablement toujours UIKitThreadAccessException
. Vous ne le respectez pas car vous n'utilisez pas le mot clé await
ou Task.Wait()
sur un marqueur que Task.Run()
renvoie. Voir Attraper une exception levée par une méthode async discussion sur StackOverflow, la documentation MSDN sur le sujet est un peu datée.
Vous pouvez attacher une continuation au marqueur que Task.Run()
retourne et inspecter les exceptions levées dans une action passée:
Task marker = Task.Run(() => ...);
marker.ContinueWith(m =>
{
if (!m.IsFaulted)
return;
// Marker Faulted status indicates unhandled exceptions. Observe them.
AggregateException e = m.Exception;
});
En général, l'accès à l'interface utilisateur à partir d'un thread non UI peut rendre une application instable ou la bloquer, mais elle n'est pas garantie.
Pour plus d'informations, consultez Comment gérer l'exception Task.Run , Android - Problème avec les tâches asynchrones discussions sur StackOverflow, La signification de TaskStatus article de Stephen Toub and Working with the UI Thread article on Microsoft Docs.