Конкурентность и многопоточность в C#: продвинутый уровень
В современной разработке на C# конкурентность и многопоточность — это не просто расширение возможностей, а фундаментальные инструменты для создания быстрых, отзывчивых и масштабируемых приложений. На продвинутом уровне мы выходим за рамки простого запуска Task.Run и сталкиваемся с тонкостями планировщиков, синхронизацией контекстов и паттернами для работы с общими данными. В этой статье мы разберём ключевые продвинутые концепции конкурентности в C# (.NET), которые помогут вам писать эффективный и безопасный код.
1. Task и ValueTask: углублённое использование
Класс Task — основа асинхронного программирования в C#. Однако на продвинутом уровне важно понимать разницу между Task и ValueTask, а также управлять жизненным циклом задач.
Когда использовать ValueTask?
ValueTask — это структура, которая позволяет избежать выделения памяти под объект Task, когда результат уже доступен синхронно. Это критично для высокопроизводительных сценариев, например, в кэширующих методах.
public class CacheService
{
private readonly Dictionary<string, string> _cache = new();
public async ValueTask<string> GetDataAsync(string key)
{
if (_cache.TryGetValue(key, out string value))
{
// Результат уже есть, не создаём Task
return value;
}
// Асинхронное получение данных
string data = await FetchFromDatabaseAsync(key);
_cache[key] = data;
return data;
}
private async Task<string> FetchFromDatabaseAsync(string key)
{
await Task.Delay(100); // Имитация задержки
return $"Data for {key}";
}
}
Продвинутая обработка ошибок и отмена
Используйте CancellationToken для корректной отмены длительных операций. Комбинируйте несколько токенов через CancellationTokenSource.CreateLinkedTokenSource.
public async Task ProcessWithTimeoutAsync(CancellationToken externalToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeoutCts.Token);
try
{
await LongRunningOperationAsync(linkedCts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Операция отменена или превышен таймаут.");
}
}
2. SynchronizationContext и планировщики задач
Понимание SynchronizationContext необходимо для работы с UI-потоками (WPF, WinForms) и ASP.NET Core. На продвинутом уровне вы можете создавать собственные контексты синхронизации.
Настройка контекста для пула потоков
По умолчанию await захватывает текущий SynchronizationContext. Чтобы избежать этого (и предотвратить deadlock), используйте ConfigureAwait(false).
public async Task ProcessDataAsync()
{
// Этот код выполняется в исходном контексте (например, UI)
await Task.Run(() => HeavyComputation()).ConfigureAwait(false);
// После ConfigureAwait(false) — в контексте пула потоков
// Нет доступа к UI-элементам!
Console.WriteLine("Выполнено в потоке пула");
}
Создание пользовательского планировщика
Вы можете реализовать свой TaskScheduler, например, для ограничения количества одновременно выполняемых задач (аналог SemaphoreSlim на уровне планировщика).
public class LimitedConcurrencyTaskScheduler : TaskScheduler
{
private readonly SemaphoreSlim _semaphore;
private int _runningTasks;
public LimitedConcurrencyTaskScheduler(int maxConcurrency)
{
_semaphore = new SemaphoreSlim(maxConcurrency);
}
protected override IEnumerable<Task> GetScheduledTasks() => Enumerable.Empty<Task>();
protected override void QueueTask(Task task)
{
ThreadPool.QueueUserWorkItem(_ =>
{
_semaphore.Wait();
Interlocked.Increment(ref _runningTasks);
try
{
TryExecuteTask(task);
}
finally
{
Interlocked.Decrement(ref _runningTasks);
_semaphore.Release();
}
});
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false;
}
3. Продвинутые паттерны синхронизации
Использование lock — это базовый уровень. Для высоконагруженных систем нужны более тонкие механизмы.
ReaderWriterLockSlim
Позволяет нескольким потокам одновременно читать данные, но блокирует запись. Идеально для кэша с частым чтением.
public class ThreadSafeCache<TKey, TValue>
{
private readonly ReaderWriterLockSlim _lock = new();
private readonly Dictionary<TKey, TValue> _cache = new();
public TValue Read(TKey key)
{
_lock.EnterReadLock();
try
{
return _cache.TryGetValue(key, out var value) ? value : default;
}
finally
{
_lock.ExitReadLock();
}
}
public void Write(TKey key, TValue value)
{
_lock.EnterWriteLock();
try
{
_cache[key] = value;
}
finally
{
_lock.ExitWriteLock();
}
}
}
SpinLock и Interlocked
Для очень коротких критических секций (буквально на несколько инструкций) используйте SpinLock или Interlocked — это быстрее, чем блокировка через Monitor.
private int _counter;
private SpinLock _spinLock = new(false); // false — без отладки
public void Increment()
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
_counter++;
}
finally
{
if (lockTaken) _spinLock.Exit();
}
}
// Альтернатива с Interlocked:
public void IncrementAtomic() => Interlocked.Increment(ref _counter);
Channel<T> для producer-consumer
Современный и потокобезопасный способ передачи данных между потоками, встроенный в .NET.
public async Task ProducerConsumerExample()
{
var channel = Channel.CreateBounded<string>(new BoundedChannelOptions(10)
{
FullMode = BoundedChannelFullMode.Wait
});
// Producer
var producer = Task.Run(async () =>
{
for (int i = 0; i < 100; i++)
{
await channel.Writer.WriteAsync($"Item {i}");
await Task.Delay