To solve problems like this, we have used an AsyncLock
previous projects. The AsyncLock
will wait until the previous lock was released.
The AsyncLock
may seem a little bit complicated first, but i hope the provided usage examples will illustrate its behaviour.
public class AsyncLock
{
private TaskCompletionSource<object> _lastSection;
public AsyncLock()
{
_lastSection = new TaskCompletionSource<object>();
_lastSection.SetResult(null);
}
public class ReleaseLock : IDisposable
{
private readonly TaskCompletionSource<object> _tcs;
public ReleaseLock(TaskCompletionSource<object> tcs)
{
_tcs = tcs;
}
public void Dispose()
{
_tcs.SetResult(null);
}
}
/// <summary>
/// Enters and locks a critical section as soon as the currently executing task has left the section.
/// The critical section is locked until the returned <see cref="IDisposable"/> gets disposed.
/// </summary>
public Task<ReleaseLock> EnterAsync()
{
var newTcs = new TaskCompletionSource<object>();
var toAwait = Interlocked.Exchange(ref _lastSection, newTcs);
return toAwait.Task.ContinueWith((_) => new ReleaseLock(newTcs), TaskContinuationOptions.ExecuteSynchronously);
}
}
You then can use await AsyncLock.EnterAsync()
to wait until any previous lock was released. In the EnterAsync
we queue the next Task
after the current Task
using ContinueWith
. This means the await AsyncLock.EnterAsync()
will be executed after the previous has finished.
using (await _lock.EnterAsync())
{
// ...
}
Here is an usage example:
class Program
{
private static readonly AsyncLock _lock = new AsyncLock();
private static async Task Test(int i, Task toComplete)
{
using (await _lock.EnterAsync())
{
await toComplete;
Console.WriteLine(i);
}
}
public static void Main(string[] args)
{
var tcs1 = new TaskCompletionSource<object>();
var tcs2 = new TaskCompletionSource<object>();
Task.Run(async () =>
{
var t1 = Test(1, tcs1.Task); // start first task
var t2 = Test(2, tcs2.Task); // start second task
tcs2.SetResult(null); // finish second first
tcs1.SetResult(null); // fiish last task
await Task.WhenAll(t1, t2); // will print: 1 and then 2
}).Wait();
}
}
The Test
method takes will first enter the Async
lock, then await the task toComplete
and then write to the console.
We start two Test
tasks ("1", and "2") and complete the second toComplete
first. Without the AsyncLock
the previous example prints: "2", "1". With the AsyncLock
however the tasks are processed in the sequence they were started.
REMARKS: One last remark. This will achieve your processing order, but can be tricky sometimes. Using locks like this can easily lead to deadlocks which are hard to solve and harder to find in the first place. Use Locks very carefully.
EDIT: Here a usage example your your problem:
private readonly AsyncLock _lock = new AsyncLock();
public Textbox_TextChangedEvent()
{
GetStocks(texboxText); // every call is now "queued" after the previous one
}
public async Task GetStocks(string texboxText)
{
using(await _lock.EnterAsync())
{
IsBusy = true;
await Task.Run(() => { CreateCollection(texboxText); });
IsBusy = false;
}
}