Skip to content

TaskSynchronizer is a library built to synchronize multiple parallel invocations of an async method.

License

Notifications You must be signed in to change notification settings

JoasE/TaskSynchronizer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TaskSynchronizer

What is TaskSynchronizer?

TaskSynchronizer is a library built to synchronize multiple parallel invocations of an async method.

What does that mean?

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!

Why would you want that?

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);
	}
}

Usage

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.

Installation

https://www.nuget.org/packages/TaskSynchronizer/

About

TaskSynchronizer is a library built to synchronize multiple parallel invocations of an async method.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages