Mastering Cross:Platform Development with Xamarin
上QQ阅读APP看书,第一时间看更新

Asynchronous methods

The Task Parallel Library (TPL) constitutes the core part of parallel computing in the .NET framework and has inherently the same stature in Xamarin runtime(s).

Asynchronous method execution, together with the async and await keywords (introduced with C# 5.0), can make the apps more responsive and efficient and decrease the complexity of implementing multithreading and synchronization. Without having the need to implement a parameterized thread, start and push are delegated to a background thread, with so called "awaitables." You can convert your methods to async promises easily with Task or Task<T> as the return type. In return, the runtime chooses the best time to execute the code and returns the result to your execution context.

For instance, the previous thread creation example with Tasks would be as simple as:

Task.Run(() => MyLongRunningProcess());

// Or
Task.Factory.StartNew(MyLongRunningProcess, TaskCreationOptions.LongRunning);

However, the Tasks framework is not only about creating threads or executing non-blocking methods, but also about coordinating and managing these asynchronous tasks in the easiest way possible. There are many static helper methods as well as methods implemented for Tasks that help developers to easily implement some of these coordination scenarios.

Continuation

The ContinueWith function on the Task class allows the developers to chain dependent Tasks together and execute them as one Task as a whole. The continuation delegate is executed once the result from the first task is posted back to the task scheduler. It is important to mention that the first task and the continuation methods are not necessarily executed on the same thread. The code is as follows:

Task.Run(() => MyLongRunningProcess())
    .ContinueWith(task => MySecondLongRunningProcess());

In case the second task was dependent on the result from the first task:

Task.Run(() => MyLongRunningProcess())
            .ContinueWith(task => MySecondLongRunningProcess(task.Result));

Cancellation

CancellationToken and CancellationTokenSource are used as the remote token to control the execution lifetime of an async method, thread, or a number of threads and the event source that the token reflects the events of.

In simple terms, CancellationTokenSource is responsible for throwing either time-based or manual cancel events and these events can be retrieved through the token in the context of the asynchronous method.

You can create a cancellation source using the default constructor and time-based cancellation can be added to the token:

m_CancellationSource = new CancellationTokenSource();

var token = m_CancellationSource.Token;

// You can cancel it after a certain amount of time, which would trigger an OperationCanceledException
// m_CancellationSource.CancelAfter(2000);

Once we are executing an async method, we can use the token from this source, or we can associate it with a TaskFactory to create a cooperating list of tasks:

Task.Run(() =>
{
    // Executing my long running method
    if (token.IsCancellationRequested)
    {
        token.ThrowIfCancellationRequested();
    }
}, token);

Or:

var taskFactory = new TaskFactory(m_CancellationSource.Token);
taskFactory.StartNew(() =>
    {
        // Executing my long running method
        if (Task.Factory.CancellationToken != CancellationToken.None && Task.Factory.CancellationToken.IsCancellationRequested)
        {
           Task.Factory.CancellationToken
               .ThrowIfCancellationRequested();
        }
    });

Finally, you can also cancel the thread or a group of threads using the Cancel or CancelAfter (with a time delay) methods of CancellationTokenSource.

Progress

Another asynchronous control feature that helps keep the user informed about the operations being invoked in the background is the progress callback implementation. Just like CancellationToken, we can supply the asynchronous tasks with an event handler for progress events that the asynchronous method can invoke to pass progress information back to the main thread.

For simple progress reporting, it is enough to expand asynchronous methods with an additional parameter that derives from the IProgress<T> interface.

For instance, if we were to implement a progress event handler in the GetFibonacciRangeAsync method, we could use the number of values to be calculated and the current ordinal being calculated to report an overall progress in percentages:

public async Task<List<int>> GetFibonacciRangeAsync(int firstOrdinal, int lastOrdinal, IProgress<int> progress = null)
{
    var results = new List<int>();

    for (var i = firstOrdinal; i < lastOrdinal; i++)
    {
        results.Add(await GetFibonacciNumberAsync(i));
        
        decimal currentPercentage = (decimal) lastOrdinal - i/(decimal) lastOrdinal - firstOrdinal;

        if (progress != null)
            progress.Report((int)(currentPercentage * 100);
    }

    return results;
}

In order to be able to use the progress value in our view model, we can make use of the Progress<T> class, which is the default implementation of IProgress<T>. The code is as follows:

Action<int> reportProgress = (value) =>
{
    InfoText = string.Format("{0}% Completed", value);
};

var progress = new Progress<int>(reportProgress);

m_SourceAsync.GetFibonacciRangeAsync(numberOrdinal1, numberOrdinal2, progress)
    .ContinueWith(task =>
    {
        Result = string.Join(",", task.Result.Select(val=>val));
        InfoText = "";
    });

Task batches

In task-based asynchronous pattern, there are other ways than continuation to execute tasks in a batch, even in parallel. The example from the previous section was awaiting each number calculation separately and executing the next call. However, the manner in which the inner methods were implemented made them independent from each other. Hence, it is not actually necessary to wait for them one by one to return the result. The code is as follows:

List<Task<int>> calculations = new List<Task<int>>();

Mvx.Trace("Starting Calculations");

for (var i = firstOrdinal; i < lastOrdinal; i++)
{
    var currentOrdinal = i;
    calculations.Add(Task.Factory.StartNew(() => 
        GetFibonacciNumberInternal(currentOrdinal).Value, TaskCreationOptions.LongRunning));
}

Mvx.Trace("Starting When All", DateTime.Now);
int[] results = await Task.WhenAll(calculations);
Mvx.Trace("Calculations Completed", DateTime.Now);

return results.OrderBy(value=>value).ToList();
Note

The Mvx static class and the Trace method are provided by the MvvmCross library. It will be further discussed in later chapters.

Now, each Fibonacci number in the sequence is calculated in parallel and when the sequence range is complete, an array of result values is returned. Finally, we sort the array and return the list of values.

We can extend this implementation by adding a progress notifier with an interlocked (thread-safe) counter:

calculations.Add(Task.Factory.StartNew(() =>
    GetFibonacciNumberInternal(currentOrdinal).Value, TaskCreationOptions.LongRunning)
    .ContinueWith(task =>
    {
        if (progress != null)
        {
            var currentTotal = Interlocked.Increment(ref currentCount);
            decimal currentPercentage = (decimal) currentTotal/(decimal) totalCount;
            progress.Report((int)(currentPercentage * 100));
        }
        return task.Result;
    })); 

The resulting log traces from the calculations above are as follows:

09-07 21:18:29.232 I/mono-stdout( 3094): mvx:Diagnostic: 40.80 Starting Calculations
09-07 21:18:29.352 I/mono-stdout( 3094): mvx:Diagnostic: 40.92 Starting When All
09-07 21:18:30.432 I/mono-stdout( 3094): mvx:Diagnostic: 42.00 Calculations Completed

The total time for the calculations was about 1.2 seconds.

Repeating the same calculations with an await on each method would give the following output (calculating ordinal 4 until 11):

09-07 21:26:58.716 I/mono-stdout( 3281): mvx:Diagnostic: 10.60 Starting Calculations
09-07 21:26:58.724 I/mono-stdout( 3281): mvx:Diagnostic: 10.61 Starting calculating ordinal 4
…
09-07 21:27:03.900 I/mono-stdout( 3281): mvx:Diagnostic: 15.78 Starting calculating ordinal 11
09-07 21:27:05.028 I/mono-stdout( 3281): mvx:Diagnostic: 16.91 Calculations Completed

The same calculations took around 6.3 seconds overall.

On top of WhenAll, developers are also equipped with the WhenAny, WaitAll, WaitAny methods on the Task class and ContinueWhenAll and ContinueWhenAny on the TaskFactory class.