Quantcast
Jump to content


Recommended Posts

Posted

tizen-banner-tizennet.png

Asynchronous coding is essential for writing responsive and efficient programs. However, writing good asynchronous code is often not easy. Some bad asynchronous code even leads to hard-to-diagnose problems like deadlocks. In this article, I'll explain a few basic concepts of asynchronous coding in C# and some common practices in Tizen applications.

How is asynchronous coding different from multi-threading?

Asynchronous coding allows a program to run multiple tasks at once without blocking a specific thread. However, this does not necessarily mean that another thread is created. For example, if a program started a Task on the main thread to read some large data from the storage, the program can still do something else on that thread until the data are fully read.

You may think async code seems a little like multithreaded code. After all, many methods could be executing at the same time in both. In reality, async programming can be used together with singular or multithreaded applications. This means you can have a single-threaded async program, where one thread can run concurrent tasks. Conversely, you can also have a multi-threaded async application, where multiple threads can each run multiple concurrent tasks.

Leslie Richardson - How Do I Think About Async Code?!

I'll show you what really happens with this simple code:

static void Main()
{
    AsyncContext.Run(Start);
}

static async Task Start()
{
    var task = DoSomethingAsync();

    Thread.Sleep(1000);

    await task;

    Thread.Sleep(1000);
}

static async Task DoSomethingAsync()
{
    await Task.Delay(1000);
}

Don't worry about AsyncContext.Run. It's used to make this console program have a UI app-like nature. This program is single-threaded, but runs two concurrent tasks at once: running an async Task (DoSomethingAsync) for a second and doing some other computation (Thread.Sleep) for a second. After the Task finishes, it again does some computation (Thread.Sleep) for a second. As shown in the below trace, the program only takes two seconds to finish and everything happens in a single thread. Notice that the two Thread.Sleep calls have different callstacks although they have the same caller (Start) in the code. This is because the compiler internally deconstructs and transforms the async method into an async state machine at compile time. (You don't have to learn the details right now.)

Asynchronous execution

When should I use async code?

There are three major types of asynchronous operations. You have to identify which one of these fits your need.

1. I/O-bound work

When you perform an I/O operation (such as sending a network request or reading a large file from disk), you don't want the whole application UI to freeze until the operation finishes. The await keyword in the following example allows the caller thread to do other work (such as handling UI events) while the network operation is in progress. There is no need to create a thread.

button.Clicked += async (s, e) =>
{
    var result = await httpClient.GetStringAsync(requestUri);
    label.Text = result;
};

Note: Although you don't typically need threading for I/O-bound work, you may sometimes have to use synchronous APIs which do not natively support asynchronous operations (for example, DataContractSerializer.WriteObject). In that case, a sync API can be wrapped into an async API using a background thread (or preferably, re-implement the async API). For how to use a background thread, read the next section.

2. CPU-bound work

A background thread can be used if you don't want a specific thread to be occupied by a heavy computational job for a long time. This type of concurrency is also called parallelism or multi-threading. You can generally use Task.Run to offload work to a background thread in most cases.

Consider you have a large JSON text that requires a noticeable amount of time to be deserialized. The following example parses a JSON string into an instance of Book class on a thread pool thread. Without Task.Run, you experience an uncomfortable delay when pressing the button because the operation blocks the UI thread.

button.Clicked += async (s, e) =>
{
    var book = await Task.Run(() =>
    {
        return JsonSerializer.Deserialize<Book>(jsonString);
    });
    label.Text = book.Title;
};

3. UI transitions

This is the most common scenario where a developer encounters an async Task for the first time when developing a UI application. In Xamarin.Forms, most page transitions (such as NavigationPage.PushAsync) and animations have a return type of Task, which means that the operations are done asynchronously. Similarly to the I/O-bound scenario, you can simply use the async and await keywords to wait for the Task completion.

button.Clicked += async (s, e) =>
{
    await Navigation.PushAsync(page);
};

Notice that the await expression has no return value. You might think that you can just call Navigation.PushAsync without the async and await keywords (it's syntactically correct). However, not properly waiting for a Task returned by an asynchronous method is not safe. I'll explain why in the next chapter.

Important principles

Badly written asynchronous code is an evil. Keep the following principles in mind when you write any asynchronous code.

1. Avoid async void

As described in many other articles, you should always avoid using async void methods in your code. The main reason is their unique exception handling semantics. Any exception thrown by the async void methods cannot be caught by their callers and always crashes the process.

There are only three exceptions when you can use async void. Otherwise, all async methods should return Tasks which can be awaited by their callers.

  • App lifecycle methods (OnCreate, OnStart, etc.)
  • Event handlers
  • Commands (ICommand implementations)

Caution: Just changing the signature of the method from async void to async Task (and not waiting for the returned Task) makes the problem even worse. Any exception thrown by the unawaited Task is silently ignored (actually, it's captured within the Task's Exception property). The following code doesn't raise an exception, but is not safe.

public async Task DoSomethingAsync()
{
    await Task.Delay(1000);
    throw new Exception();
}

button.Clicked += (s, e) =>
{
    _ = DoSomethingAsync(); // Discard the result
};

The above pattern is also referred to as fire-and-forget. Use this pattern only if you don't really care about the Task's result. Consider using an extension method to enable structured error logging.

2. Avoid .Result and .Wait()

It is sometimes tempting to use Task.Result or Task.Wait to synchronously wait for Task completion without having to use async/await. Never use them because they can lead to immediate deadlocks when used in UI applications. Blocking a thread for a background task (sync-over-async) is always a bad idea.

The best solution is to use async and await. The problem usually arises when a developer wants to change only a small part of their application and 'hide' asynchronous operations from the rest of the code. However, switching from sync to async often requires significant changes in your application. For example, you may have to implement a new INotifyPropertyChanged-based type to visualize the progress and the result of the currently running asynchronous operation.

If the callee is a pure library method which knows nothing about the app UI, you can make use of .ConfigureAwait(false) to enable synchronous calls to the method. Adding .ConfigureAwait(false) to every occurrence of await in the callee method prevents deadlocks. However, this is a dangerous practice and not generally recommended.

Scenarios

I have investigated some common patterns of using async code in Tizen applications. Some of them are listed below.

1. UI transitions

Any UI transitions including animations and page navigations (.PushAsync, .PopAsync, .PopToRootAsync) should be awaited in Xamarin.Forms applications even though there is no extra work to do after the await expression.

❌ DON'T

private void OnDismissButtonClicked(object s, EventArgs e)
{
    Navigation.PopAsync();
}

✅ DO

private async void OnDismissButtonClicked(object s, EventArgs e)
{
    await Navigation.PopAsync();
}

2. Async Commands

The ICommand interface is often used to define a data binding between a XAML file and a ViewModel in the MVVM architecture. Similar to an event handler, a Command can be constructed using an async void Action delegate. Make sure all exceptions are captured in the scope of the Command so as not to crash your application.

public ICommand CheckForecastCommand = new Command(CheckForecast);

private async void CheckForecast()
{
    ...
}

Another approach is to implement a custom AsyncCommand class to visualize the Command's execution status using data binding. For more details, read the post Async Programming : Patterns for Asynchronous MVVM Applications: Commands.

3. Async constructors

The await keyword cannot be used in constructors because they are synchronous. I've seen many developers using async void methods for asynchronous construction without considering the exact consequences. As stated above however, this kind of code should be avoided:

❌ DON'T

private readonly SQLiteAsyncConnection _database;

public RecordDatabase()
{
    _database = new SQLiteAsyncConnection(PATH);

    InitializeAsync();
}

private async void InitializeAsync()
{
    await _database.CreateTableAsync<Record>();
}

Instead, the factory pattern can be used to enable async construction. The caller should await the static method CreateAsync to instantiate this type.

✅ DO

private readonly SQLiteAsyncConnection _database;

public RecordDatabase()
{
    _database = new SQLiteAsyncConnection(PATH);
}

private async Task<RecordDatabase> InitializeAsync()
{
    await _database.CreateTableAsync<Record>();
    return this;
}

public static Task<RecordDatabase> CreateAsync()
{
    var instance = new RecordDatabase();
    return instance.InitializeAsync();
}

There are also other approaches.

  • The AsyncLazy pattern is useful when the creation of an expensive resource can be delayed until it's actually needed.
  • If the type is instantiated using data binding, you can implement the INotifyPropertyChanged interface to update the UI according to the status of the asynchronous initialization.

For more details, see the post Async OOP 2: Constructors by Stephen Cleary.

4. Wrapping event-based APIs

Many TizenFX APIs follow the Event-based Asynchronous Pattern (EAP). You may want to wrap some of these APIs into Task-based asynchronous calls using TaskCompletionSource to make your code more readable and easier to understand. A common example of this is asking users for privacy-related privileges using the PrivacyPrivilegeManager API.

public async Task<bool> CheckPrivilege()
{
    switch (PrivacyPrivilegeManager.CheckPermission(HEALTHINFO_PRIVILEGE))
    {
        case CheckResult.Allow:
            return true;
        case CheckResult.Deny:
            return false;
        case CheckResult.Ask:
            if (!PrivacyPrivilegeManager.GetResponseContext(HEALTHINFO_PRIVILEGE).TryGetTarget(out var context))
                return false;

            var tcs = new TaskCompletionSource<bool>();
            context.ResponseFetched += (s, e) =>
            {
                if (e.cause == CallCause.Answer)
                    tcs.SetResult(e.result == RequestResult.AllowForever);
                else
                    tcs.SetResult(false);
            };
            PrivacyPrivilegeManager.RequestPermission(HEALTHINFO_PRIVILEGE);

            return await tcs.Task;
        default:
            return false;
    }
}

The ResponseFetched event is raised when there is a user response for PrivacyPrivilegeManager.RequestPermission. The wrapper Task is awaited until the result is set by the EventHandler associated with the event. You can also consider registering a CancellationToken to set a timeout for the Task.

Advanced tips

1. Use .ConfigureAwait(false) for library code

It is recommended to use .ConfigureAwait(false) for every await call in your (non-UI) library code. It prevents deadlocks when the code is accidentally called from a synchronous context in the user code. Tizen has its own SynchronizationContext-derived type (TizenSynchronizationContext) just as other platforms (such as WinForms and WPF) do. For more details, read the following articles.

2. Run UI code on the UI thread

When you manipulate the app UI in your code, make sure to do it on the UI thread. Otherwise, the code will not act as you expect. Your code runs on the UI thread unless you explicitly use a thread (Task.Run) or a non-default context (.ConfigureAwait(false)).

For example, in the following code, the current SynchronizationContext is captured by the await keyword, and the code after await also runs on the same context. If you change Task.Delay(100) to Task.Delay(100).ConfigureAwait(false), the context is null and changing the button text has no effect.

private async void OnButtonClicked(object sender, EventArgs e)
{
    await Task.Delay(100);
    button.Text = "Clicked";
}

The following code is incorrect because it tries to change the UI from a background (thread pool) thread. There is no SynchronizationContext for a thread pool thread.

private void OnButtonClicked(object sender, EventArgs e)
{
    Task.Run(() =>
    {
        button.Text = "Clicked";
    });
}

In a Xamarin.Forms application with the MVVM architecture, it is generally possible to update ViewModels from non-UI threads. However, the better practice is to use Device.BeginInvokeOnMainThread to avoid any confusion.

3. TizenFX thread-safety

TizenFX APIs are not generally meant to be thread-safe. If you call APIs which are not marked to be thread-safe simultaneously from different threads, they may lead to incorrect results or even deadlocks. For now, I recommend calling TizenFX APIs only from the main (UI) thread.

Conclusion

Although I've tried to provide as many details as possible, there are also other patterns you may face in real-world applications. If the above information is not sufficient to fit your need, you can find other materials on the web, including the pages I linked below. If you don't feel you fully understand all the concepts, simply note that you should try to complete your code first, and then polish it as you can. Even though your code doesn't meet the async standards, it should generally work for most cases.

If you have any questions or feedback, please let me know at [email protected].

View the full blog at its source



  • Replies 0
  • Created
  • Last Reply

Top Posters In This Topic

Popular Days

Top Posters In This Topic

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...





×
×
  • Create New...