DiabloRusso
DiabloRusso

Reputation: 19

How to synchronize multithreading notification for UI update

I have a async method that is called when I need to update UI controls content, like this:

public async Task UpdateUI(int i)
{
    Debug.WriteLine("Enter {0}", i);
    DoSomethingSync(1000);

    Debug.WriteLine("Await {0}", i);
    await GetDataFromServerAsync(5000);

    //Update UI Controls
    Debug.WriteLine("Update {0}", i);
    DoSomethingSync(2000);

    Debug.WriteLine("Exit {0}", i);
}

public void DoSomethingSync(int delay)
{
    Thread.Sleep(delay);
}

public Task GetDataFromServerAsync(int delay)
{
    return TaskEx.Delay(delay);
}

When the data is changed on the server, I get a notification that come in different threads and I have to call a UpdateUI method on the UI thread, like this:

public async void OnServerDataChanged(int i)
{
    Debug.WriteLine("WaitOne {0}", i);
    _semaphore.WaitOne();
    try
    {
        await Task.Factory.StartNew(async () =>
        {
            await UpdateUI(i);
        }, CancellationToken.None, TaskCreationOptions.None, _scheduler);
    }
    finally 
    {
        _semaphore.Release();
        Debug.WriteLine("Release {0}", i);
    }
}

I simulate multithreading notification:

public void SimulateMultiThreadingNotification()
{
    TaskEx.Run(() =>
    {
        for (var i = 0; i < 3; i++)
        {
            TaskEx.Run(() =>
            {
                var id = Thread.CurrentThread.ManagedThreadId;
                OnServerDataChanged(id);
            });
        }
    });
}

Run:

_semaphore = new Semaphore(1, 1);
_scheduler = TaskScheduler.FromCurrentSynchronizationContext();
SimulateMultiThreadingNotification();

Output:

Enter 11
Await 11
Release 11
Enter 12
Await 12
Release 12
Enter 6
Await 6
Release 6
Update 11
Exit 11
Update 12
Exit 12
Update 6
Exit 6

How to synchronize so that the method executed sequentially like this:

Enter 11
Await 11
Update 11
Exit 11
Release 11
Enter 12
Await 12
Update 12
Exit 12
Release 12
Enter 6
Await 6
Update 6
Exit 6
Release 6

Advance thanx!

Edit: I found solution:

public void OnServerDataChanged(int i)
{
    _semaphore.WaitOne();
    try
    {
        var tcs = new TaskCompletionSource<bool>();
        Task.Factory.StartNew(async () =>
        {
            await UpdateUI(i);
            tcs.SetResult(true);
        }, CancellationToken.None, TaskCreationOptions.None, _scheduler);
        tcs.Task.Wait();
    }
    finally 
    {
        _semaphore.Release();
        Debug.WriteLine("Release {0}", i);
    }
}

Edit 2: Solution by Yuval Itzchakov:

public void OnServerDataChanged(int i)
{
    _semaphore.WaitOne();
    try
    {
        Task.Factory.StartNew(async () =>
        {
            await UpdateUI(i);
        }, CancellationToken.None, TaskCreationOptions.None, _scheduler).Unwrap().Wait();
    }
    finally 
    {
        _semaphore.Release();
        Debug.WriteLine("Release {0}", i);
    }
}

Upvotes: 1

Views: 149

Answers (1)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149588

Your problem is with the fact Task.Factory.StartNew doesn't "handle" async Task returning lambdas properly, since they generate a Task<Task>, where you actually await on the outter Task, not the inner one.

You can use SemaphoreSlim instead of a Semaphore, it has a WaitAsync which you can asynchronously wait on.

Calling Unwrap() will solve your problem:

private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
public async Task ServerDataChangedAsync(int i)
{
    Debug.WriteLine("WaitAsync {0}", i);
    await _semaphore.WaitAsync();
    try
    {
        await Task.Factory.StartNew(async () =>
        {
            await UpdateUI(i);
        }, CancellationToken.None, TaskCreationOptions.None, _scheduler).Unwrap();
    }
    finally 
    {
        _semaphore.Release();
        Debug.WriteLine("Release {0}", i);
    }
}

Also, don't do async void, that's for event handlers. Do async Task instead.

Upvotes: 1

Related Questions