TaskSynchronizer is a library built to synchronize multiple parallel invocations of an async method.
Review the following code:
public static async Task DoWork()
{
await Task.Delay(100); // Some heavy work.
Console.WriteLine("Work done!");
}
public static async Task WorkRequested()
{
await DoWork();
}
static void Main(string[] args)
{
var tasks = new List<Task>();
for (var i = 0; i < 2; i++)
{
tasks.Add(WorkRequested());
}
Task.WaitAll(tasks.ToArray());
}
The output of this program would be:
Work done!
Work done!
Now let's say that even though DoWork
is invoked twice here, you wanted it to only run once.
You can do this using the TaskSynchronizer
.
public static TaskSynchronizer Synchronizer = new TaskSynchronizer();
public static async Task DoWork()
{
await Task.Delay(100); // Some heavy work.
Console.WriteLine("Work done!");
}
public static async Task WorkRequested()
{
using (Synchronizer.Acquire(DoWork, out var task)) // Synchronize the call to work.
{
await task;
}
}
static void Main(string[] args)
{
var tasks = new List<Task>();
for (var i = 0; i < 2; i++)
{
tasks.Add(WorkRequested());
}
Task.WaitAll(tasks.ToArray());
}
The ouput of this program would be:
Work done!
Imagine you are developing an ASP.NET application which talks to a 3rd party WebAPI using a JWT token. Your controller might look something like this:
public class ValuesController : ControllerBase
{
public static JwtSecurityToken Token;
public static readonly HttpClient HttpClient = new HttpClient();
public Secrets Secrets { get; } = new Secrets()
{
Username = Environment.GetEnvironmentVariable("Username"),
Password = Environment.GetEnvironmentVariable("Password")
};
public static async Task GetTokenAsync(Secrets secrets)
{
var response = await HttpClient.PostAsync("https://my.api.com/token", new StringContent(JsonConvert.SerializeObject(secrets)));
var result = await response.Content.ReadAsStringAsync();
var handler = new JwtSecurityTokenHandler();
Token = handler.ReadToken(result) as JwtSecurityToken;
}
[HttpGet]
public async Task<ActionResult<string>> Get()
{
if (Token == null || Token.ValidTo <= DateTime.UtcNow)
{
await GetTokenAsync(Secrets);
}
var result = await DoRequestAsync();
return Ok(result);
}
}
But this code is NOT thread safe! What if multiple requests to the Get
action come in at the same time when the Token
is expired? The GetTokenAsync
would run multiple times!
You can prevent this behaviour simply by using the TaskSynchronizer
:
public class ValuesController : ControllerBase
{
public static TaskSynchronizer Synchronizer = new TaskSynchronizer();
public static JwtSecurityToken Token;
public static readonly HttpClient HttpClient = new HttpClient();
public Secrets Secrets { get; } = new Secrets()
{
Username = Environment.GetEnvironmentVariable("Username"),
Password = Environment.GetEnvironmentVariable("Password")
};
public static async Task GetTokenAsync(Secrets secrets)
{
var response = await HttpClient.PostAsync("https://my.api.com/token", new StringContent(JsonConvert.SerializeObject(secrets)));
var result = await response.Content.ReadAsStringAsync();
var handler = new JwtSecurityTokenHandler();
Token = handler.ReadToken(result) as JwtSecurityToken;
}
[HttpGet]
public async Task<ActionResult<string>> Get()
{
if (Token == null || Token.ValidTo <= DateTime.UtcNow)
{
using (Synchronizer.Acquire(() => GetTokenAsync(Secrets), out var task))
{
await task;
}
}
var result = await DoRequestAsync();
return Ok(result);
}
}
A TaskSynchronizer
instance will synchronize any aquire call until the returned synchronization object is disposed.
This means you should usually use one TaskSynchronizer
per method.
If you used one TaskSynchronizer
for multiple methods and they were all invoked at the same time, only one of the methods would run.