Reputation: 18593
I've written a loop in Xamarin for Android where I load 5 images asynchronously using System.Net.Http.HttpClient
. Initiating the 5 requests executes instantly, but I'm not getting any continuation before all 5 responses are completed - about 4 seconds later. Why are not the responses coming individually, asynchronously?
I don't know much about how threads are handled in Xamarin so I might be doing something wrong. Should I not call it from the UI thread? Is there any way I can specify a Scheduler or threading policy for HttpClient?
The code for the load function:
// This method is called 5 times in a foreach loop, initated from the UI thread, not awaited
private async void LoadAsync(String uri)
{
Bitmap bitmap = await imgSvc.loadAndDecodeBitmap(uri);
image.SetImageBitmap(bitmap);
}
public async Task<Bitmap> loadAndDecodeBitmap(String uri) {
var client = new HttpClient();
byte[] data = await client.GetByteArrayAsync(uri);
Bitmap img = BitmapFactory.DecodeByteArray(data, 0, data.Length);
return img;
}
Edit: Minimal, reproducable example in this repo
Try switching ImagesToLoad
in that example between 5 and 1 and see how the load time for the first image changes dramatically (about 2 sec difference on my computer).
Load log 10 images
Load log if I only load 1
Upvotes: 0
Views: 489
Reputation: 18593
HttpClient
scales badly in when run in parallell. It seems to be using only a few (1?) threads behind the scene, which are interdependent and can block each other, no matter if we wrap it in Task.Run
. I came to this conclusion after swapping Client.GetByteArrayAsync()
with Task.Delay(1000)
. They behave vastly different.
Running 1 or 10 Task.Delay(1000)
in parallell both give me the first result in just above ~1200ms. See commit ff2a8e9
in the repo - swap LoadAndDecodeBitmapFake
with LoadAndDecodeBitmapFake
.
Running 1 Client.GetByteArrayAsync()
give me the first result after ~1300ms while running 10 of them gives me the first result after ~5000ms. Se commit 561691f
Turns out the optimal solution is to run them in serial - awaiting every single execution instead of lumping them and running await Task.WhenAll()
on all 10. See commit eaa0f18
. Then I get the first result after ~1400ms and the last after ~2000ms!
Sounds like a design flaw with HttpClient
or something broken in the rewrite/wrapping to monodroid.
Upvotes: 0
Reputation: 247413
You should consider loading them all at the same time using Task.WhenAll
. You are also creating multiple instance of HttpClient
which can have negative effects on performance.
First updating call to avoid using async void
.
ArticleTeaserView.cs
public async Task SetModel(ArticleTeaser model) {
title.SetText(model.promotionContent.title.value, TextView.BufferType.Normal);
description.SetText(model.promotionContent.description.value, TextView.BufferType.Normal);
try {
var uri = Android.Net.Uri.Parse(model.promotionContent.imageAsset.urls[0].url);
Log.Debug(TAG, "Image " + (++ctr) + " load starting...");
await LoadAsync(model.promotionContent.imageAsset.GetUrlWithMinHeight(240), image);
Log.Debug(TAG, "Image " + ctr + " load completed");
} catch (Exception ex) {
Log.Debug(TAG, ex.Message);
}
}
static ImageSvc imgSvc = new ImageSvc(); //should consider injecting service
private async Task<Image> LoadAsync(String uri, ImageView image) {
Bitmap bitmap = await imgSvc.loadAndDecodeBitmap(uri);
image.SetImageBitmap(bitmap);
}
//Use only one shared instance of `HttpClient` for the life of the application
private static HttpClient client = new HttpClient();
public async Task<Bitmap> loadAndDecodeBitmap(String uri) {
byte[] data = await client.GetByteArrayAsync(uri);
Bitmap img = BitmapFactory.DecodeByteArray(data, 0, data.Length);
return img;
}
Finally assuming you have a collection of urls. You create all the tasks and invoke all of them at the same time.
MainActivity.cs
private event EventHandler LoadData = delegate { };
protected override void OnCreate(Bundle bundle) {
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView (Resource.Layout.Main);
InitViews();
LoadData += onDataLoading; // subscribe to event
LoadData(this, EventArgs.Empty); // raise event
}
//async void allowed on event handlers (actual event handler)
//*OnCreate* is not an event handler. Just a based method.
private async void onDataLoading(object sender, EventArgs e) {
LoadData -= onDataLoading;
await LoadDataAsync();
}
private void InitViews() {
//...
}
private async Task LoadDataAsync() {
var svc = new NewsService();
var promContent = svc.syncLoadStrong();
await BindView(promContent);
}
private async Task BindView(ArticleList list) {
Log.Debug(TAG, "Binding MainActivity");
ViewGroup scroller = FindViewById<ViewGroup>(Resource.Id.scroller);
if (list != null) {
var tasks = new List<Task>();
foreach (ArticleTeaser teaser in list) {
var atv = new ArticleTeaserView(this, null);
tasks.Add(atv.SetModel(teaser));
scroller.AddView(atv);
}
await Task.WhenAll(tasks);
}
}
Make sure that you are not mixing blocking calls like .Result
, .Wait()
with async/await calls and also avoid using async void
unless it is for an event handler.
Upvotes: 1
Reputation: 1704
My recommendation would be to use a single instance of an HttpClient object. As stated here, you can reuse it to handle multipe instances.
The problem you are facing is because each time an HttpClient object is created, a connection is opened and this operation takes some time. Likewise, disposing each of the instances requires the connection to be closed and this may have an impact over other requests.
Upvotes: 0