In this lesson, you will learn how to maintain data integrity when multiple clients invoke SignalR Hub methods that modify shared application state simultaneously.
The Concurrency Problem
SignalR Hubs are transient, meaning a new instance of your Hub class is created for every single method invocation. While this ensures isolation for properties defined within the Hub, it creates a significant pitfall: if your Hub methods modify shared, singleton-scoped application state (such as a global dictionary tracking active game rooms or aggregate counters), multiple threads will attempt to update that shared state at the same time. This leads to classic race conditions where data can be corrupted or lost.
Thread-Safe Patterns
Because Hub methods are asynchronous by design, you cannot rely on traditional lock statements, as lock blocks cannot contain await keywords. Instead, you must use thread-safe collection types or asynchronous primitives to manage concurrent access.
If you are maintaining a registry of data, use System.Collections.Concurrent namespaces. For more complex logic, use SemaphoreSlim.
Consider this example where we update a shared counter safely:
private static int _globalCounter = 0;
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public async Task IncrementGlobalCount()
{
await _semaphore.WaitAsync();
try
{
// Thread-safe update section
_globalCounter++;
}
finally
{
_semaphore.Release();
}
}
Why This Matters
If you were to simply perform _globalCounter++ without the semaphore, two overlapping requests could read the same initial value, increment it independently, and write back the same result, effectively "dropping" one of the increments. By using SemaphoreSlim.WaitAsync(), you ensure that even though the Hub instances are transient, the access to the underlying shared state is serialized and predictable across the entire application domain. Always prefer these asynchronous primitives over blocking locks to keep your SignalR pipeline performant and responsive.
Key Takeaway: Since Hub instances are transient and methods are asynchronous, protect shared state using Concurrent collections or SemaphoreSlim to prevent race conditions without blocking the underlying thread pool.