Home > Software design >  How to get result from a Task.Run<T>() without blocking threads
How to get result from a Task.Run<T>() without blocking threads

Time:01-26

I've read many explanations but none of them made sense to me.

I'm doing this in Xamarin.Forms:

var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
var cts = new CancellationTokenSource();
var location = Task.Run<Location>(async () => await Geolocation.GetLocationAsync(request, cts.Token));
var userLat = location.Latitude; // doesn't work
var userLon = location.Longitude; // doesn't work

Inside of my constructor so its within a synchronous method.

It tells me:

'Task<Location>' does not contain a definition for 'Longitude'...

I know I need to get the returned value in location but I don't know how since I've read that using Task.Result() was bad.

How to get the result from the Task.Run and be able to use it without impact on performance ?

CodePudding user response:

I think it could be this:

public async Task SomeMethodAsync()
{
    var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
    var cts = new CancellationTokenSource();
    var location = await Geolocation.GetLocationAsync(request, cts.Token);
    var userLat = location.Latitude;
    var userLon = location.Longitude;
}

Not that methods which contain await calls should be marked as async. If you need to return something from this method then the return type will be Task<TResult>.

Constructors cannot be marked with async keyword. And it's better not to call any async methods from constructor, because it cannot be done the right way with await. As an alternative please consider Factory Method pattern. Check the following article of Stephen Cleary for details: Async OOP 2: Constructors.

CodePudding user response:

The normal way to retrieve results from a Task<T> is to use await. However, since this is a constructor, there are some additional wrinkles.

Think of it this way: asynchronous code make take some time to complete, and you can never be sure how long. This is in conflict with the UI requirements; when Xamarin creates your UI, it needs something to show the user right now.

So, your UI class constructor must complete synchronously. The normal way to handle this is to (immediately and synchronously) create the UI in some kind of "loading..." state and start the asynchronous operation. Then, when the operation completes, update the UI with that data.

I discuss this in more detail in my article on async data binding.

CodePudding user response:

You have to await the task:

var location = await Task.Run<Location>(async () => await Geolocation.GetLocationAsync(request, cts.Token));

Otherwise, location is just a Task which, obviously, doesn't contain any longitude and latitude properties.

CodePudding user response:

You mentioned in a comment to another answer that you can't do it async because this code is in a constructor. In that case, it is recommended to move the asynchronous code into a separate method:

public class MyClass
{
   public async Task Init()
   {
      var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
      var cts = new CancellationTokenSource();
      var location = await Geolocation.GetLocationAsync(request, cts.Token);
      var userLat = location.Latitude; 
      var userLon = location.Longitude;
  }
}

You can use it the following way:

var myObject = new MyClass();
await myObject.Init();

The other methods of this class could throw an InvalidOperationException if Init() wasn't called yet. You can set a private boolean variable wasInitialized to true at the end of the Init() method. As an alternative, you can make your constructor private an create a static method that creates your object. By this, you can assure that your object is always initialized correctly:

public class MyClass
    {
       private MyClass() { }
       public async Task<MyClass> CreateNewMyClass()
       {
          var result = new MyClass();
          var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
          var cts = new CancellationTokenSource();
          var location = await Geolocation.GetLocationAsync(request, cts.Token);
          result.userLat = location.Latitude; 
          result.userLon = location.Longitude; 
          return result;
       }
    }

CodePudding user response:

Do the asynchronous work before constructing the object and pass in the data to the constructor. I suggest a factory.

class MyFactory : IMyFactory
{
    public async Task<MyClass> GetMyClass()
    {
        var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
        var cts = new CancellationTokenSource();
        var location = Task.Run<Location>(async () => await Geolocation.GetLocationAsync(request, cts.Token));
        return new MyClass(location);   
    }
}

class MyClass
{
    public MyClass(Location location)
    {
        var userLat = location.Latitude; 
        var userLon = location.Longitude; 
        //etc....
    }
}

To create an instance, instead of

var x = new MyClass();

you'd call

var factory = new MyFactory();
var x = await factory.GetMyClass();

The nice thing about this approach is that you can mock the factory (e.g. for unit tests) in a way that does not depend on the external service.

  • Related