A lightweight C# library providing Go-inspired concurrency patterns for .NET applications.
Install Concur via the NuGet Package Manager console:
Install-Package ConcurOr using the .NET CLI:
dotnet add package ConcurConcur is built around three fundamental concepts from Go:
- Goroutine: A lightweight, concurrent function. In Concur, you create one using the Go() method. It can be a synchronous action or an async task that runs in the background.
- Channel: A typed, thread-safe queue that allows goroutines to communicate. One goroutine writes data to the channel, and another reads from it, ensuring safe data transfer without explicit locks.
- WaitGroup: A synchronization aid that allows your code to block and wait until a collection of goroutines has finished executing.
To get started, add the following using directive. This static import gives you direct access to the Go() method.
using static ConcurRoutine;The Go() method immediately executes a function on a background thread without blocking the caller.
This is ideal for "fire and forget" operations.
// Run a synchronous action concurrently.
Go(() => {
Console.WriteLine("This is running in the background.");
});
// Run an async lambda concurrently.
// The calling thread is not blocked by the Task.Delay.
Go(async () => {
Console.WriteLine("Starting async work...");
await Task.Delay(1000);
Console.WriteLine("Async work completed after 1 second.");
});
// The main thread continues its execution immediately.
Console.WriteLine("This message appears first!");Channels are the best way for goroutines to communicate. The producer sends data, and the consumer receives it.
For maximum clarity, you can create and manage a channel yourself.
// 1. Create a channel that will transport integers.
var channel = new DefaultChannel<int>();
// 2. Start a "producer" goroutine to write data to the channel.
// The 'writer' argument is a reference to the channel.
Go(async writer =>
{
for (var i = 1; i <= 5; i++)
{
Console.WriteLine($"[Producer] Sending: {i}");
await writer.WriteAsync(i);
await Task.Delay(100); // Simulate work
}
// 3. Close the channel when done. This signals the consumer to stop.
await writer.CompleteAsync();
}, channel);
// 4. Consume data from the channel on the main thread.
// The loop will automatically end when the channel is completed.
await foreach (var number in channel)
{
Console.WriteLine($"[Consumer] Received: {number}");
}
Console.WriteLine("Channel consumed. Program finished.");Concur provides a convenient shorthand that creates and returns a channel for you.
// Go<T> creates and returns a readable channel.
// The lambda you provide is the producer.
var numbers = Go<int>(async writer =>
{
for (var i = 0; i < 5; i++)
{
await writer.WriteAsync(i);
await Task.Delay(100);
}
await writer.CompleteAsync(); // The producer must complete the channel.
});
// Consume the values just like before.
await foreach (var number in numbers)
{
Console.WriteLine($"Received: {number}");
}By default, channels are unbounded. You can specify a capacity to create a bounded channel. A producer writing to a full bounded channel will block asynchronously until a consumer makes space.
// Create a channel that can only hold 2 items at a time.
var numbers = Go<int>(async writer =>
{
for (var i = 0; i < 10; i++)
{
Console.WriteLine($"[Producer] Writing {i}...");
// This will 'block' if the channel has 2 unread items.
await writer.WriteAsync(i);
Console.WriteLine($"[Producer] Wrote {i}.");
}
await writer.CompleteAsync();
}, capacity: 2); // Set the buffer size
// Consumer
await foreach (var number in numbers)
{
Console.WriteLine($"[Consumer] Read {number}.");
await Task.Delay(500); // Simulate slow consumer
}A WaitGroup is used to wait for a group of goroutines to finish. You pass the WaitGroup instance to each goroutine you want to track.
Console.WriteLine("Starting multiple background jobs...");
var wg = new WaitGroup();
var results = new bool[3];
// Each call to Go(wg, ...) increments the WaitGroup counter.
// The counter is decremented automatically when the goroutine finishes.
Go(wg, async () => {
await Task.Delay(100);
results[0] = true;
Console.WriteLine("Job 1 finished.");
});
Go(wg, async () => {
await Task.Delay(500);
results[1] = true;
Console.WriteLine("Job 2 finished.");
});
Go(wg, async () => {
await Task.Delay(250);
results[2] = true;
Console.WriteLine("Job 3 finished.");
});
// await wg.WaitAsync() blocks until the WaitGroup counter becomes zero.
await wg.WaitAsync();
Console.WriteLine("\nAll background jobs have completed!");
Console.WriteLine($"Results: {results[0]}, {results[1]}, {results[2]}");Concur provides a flexible, context-aware exception handling system that supports both global and per-operation exception handling.
using Microsoft.Extensions.Logging;
using Concur.Abstractions;
using static ConcurRoutine;
// Create a custom exception handler,
// by default Concur will use a `DefaultLoggingExceptionHandler`.
public class LoggingExceptionHandler : IExceptionHandler
{
private readonly ILogger logger;
public LoggingExceptionHandler(ILogger logger)
{
this.logger = logger;
}
public ValueTask HandleAsync(IExceptionContext context)
{
logger.LogError(context.Exception,
"Exception in routine {RoutineId} during {OperationName}: {Message}",
context.RoutineId,
context.OperationName ?? "Unknown",
context.Exception.Message);
return ValueTask.CompletedTask;
}
}
// Set the global default handler
var logger = loggerFactory.CreateLogger("Concur");
var options = new GoOptions
{
ExceptionHandler = new LoggingExceptionHandler(logger),
};
// All goroutines will now use the handler specified in options
Go(() => throw new InvalidOperationException("This will be logged"), options);Concur aims to offer a more expressive, Go-style concurrency API—not to outperform the Task Parallel Library (TPL).
Performance is nearly identical to using Task.Run and Channel<T> directly. The small overhead from WaitGroup is negligible in most cases.
Below is a benchmark comparing Concur (Go, DefaultChannel, WaitGroup) with idiomatic C# (Task.Run, System.Threading.Channels.Channel, Task.WhenAll):
Contributions are welcome! If you'd like to help improve Concur, please feel free to fork the repository, make changes, and submit a pull request.
This project is licensed under the MIT License. See the LICENSE file for details.