Хабрахабр

[Из песочницы] Lock с приоритетами в .NET

Каждый программист, использующий более одного потока в своей программе, сталкивался с примитивами синхронизации. В контексте .NET их очень и очень много, перечислять не буду, за меня это уже сделал MSDN.

Но в этой статье я хочу рассказать про обычный lock в десктопном приложении и о том как же появился новый (по крайней мере для меня) примитив, который можно назвать PriorityLock.
Мне приходилось пользоваться многими из этих примитивов, и они прекрасно помогали справиться с задачами.

Проблема

При разработке высоконагруженного многопоточного приложения где то появляется менеджер, который обрабатывает бесчисленное множество потоков. Так было и у меня. И вот работал этот менеджер, обрабатывал тонны запросов от многих сотен потоков. И все у него было хорошо, а внутри трудился обычный lock.

У меня еще 950 таких как ты. И вот однажды пользователь (например я) нажимает кнопку в интерфейсе приложения, поток летит в менеджер (не UI поток конечно) и ожидает увидеть супер приветливый ресепшен, а вместо этого его встречает тетя Клава из самой дремучей регистратуры самой дремучей поликлиники со словами «Мне плевать кто тебя направил. Мне всё равно как вы там разберетесь». Иди и втавай к ним. NET. Примерно так работает lock в . И вроде все хорошо, все выполнится корректно, но пользователь явно не планировал ждать несколько секунд ответа на своё действие.

На этом душещипательная история заканчивается и начинается решение технической проблемы.

Решение

Изучив стандартные примитивы, я не нашел подходящего варианта. Поэтому решил написать свой lock, который бы имел стандартный и высокий приоритет входа. Кстати после написания я изучил и nuget, там тоже ничего подобного не нашел, хотя возможно плохо искал.

В спойлере я привел первый вариант моего PriorityLock (только синхронный код, но он и есть самый важный), и пояснения к нему. Для написания такого примитива (или уже не примитива) мне потребовались SemaphoreSlim, SpinWait и Interlocked операции.

Скрытый текст

В плане синхронизации нету никаких открытий, пока кто-то в локе, другие не могут зайти. Если пришел high priority, его пускают вперед всех ожидающих low priority.

Именно он является тем самым объектом синхронизации. Класс LockMgr, с ним предлагается работать в вашем коде. Создает объекты Locker и HighLocker, содержит в себе семафоры, SpinWait'ы, счетчики желающих попасть в критическую секцию, текущий поток и счетчик рекурсии.

public class LockMgr
public Locker Lock(bool high = false) { return new Locker(this, high); }
}

Класс Locker реализует интерфейс IDisposable. Для реализации рекурсии при завладении локом запоминаем Id потока, после проверяем его. Далее в зависимости от приоритета, в случае высокого приоритета сразу говорим что мы пришли (увеличиваем счетчик HighCount), получаем семафор High, и ждём (если нужно) освобождения лока от низкого приорита, после мы готовы получить лок. В случае низкого приорита получает семафор Low, далее ждем завершения всех high приоритетных потоков, и, забирая на время семафор High увеличиваем LowCount.

Стоит оговориться, что смысл HighCount и LowCount разный, HighCount отображает количество приоритетных потоков, которые пришли к локу, когда LowCount всего лишь означает что поток (один единственный) с низким приоритетом зашел в лок.

public class Locker : IDisposable
{ private readonly bool _isHigh; private LockMgr _mgr; public Locker(LockMgr mgr, bool isHigh = false) { _isHigh = isHigh; _mgr = mgr; if (mgr.CurThread == Thread.CurrentThread) { mgr.RecursionCount++; return; } if (_isHigh) { Interlocked.Increment(ref mgr.HighCount); mgr.High.Wait(); while (Interlocked.CompareExchange(ref mgr.LowCount, 0, 0) != 0) mgr.HighSpin.SpinOnce(); } else { mgr.Low.Wait(); while (Interlocked.CompareExchange(ref mgr.HighCount, 0, 0) != 0) mgr.LowSpin.SpinOnce(); try { mgr.High.Wait(); Interlocked.Increment(ref mgr.LowCount); } finally { mgr.High.Release(); } } mgr.CurThread = Thread.CurrentThread; } public void Dispose() { if (_mgr.RecursionCount > 0) { _mgr.RecursionCount--; _mgr = null; return; } _mgr.RecursionCount = 0; _mgr.CurThread = null; if (_isHigh) { _mgr.High.Release(); Interlocked.Decrement(ref _mgr.HighCount); } else { _mgr.Low.Release(); Interlocked.Decrement(ref _mgr.LowCount); } _mgr = null; }
} public class HighLocker : Locker
{ public HighLocker(LockMgr mgr) : base(mgr, true) { }
}

Использование объекта класса LockMgr получилось очень лаконичным. В примере явно показана возможность переиспользования _lockMgr внутри критической секции, при этом приоритет уже не важен.

private PriorityLock.LockMgr _lockMgr = new PriorityLock.LockMgr(); public void LowPriority()
{ using (_lockMgr.Lock()) { using (_lockMgr.HighLock()) { // your code } }
} public void HighPriority()
{ using (_lockMgr.HighLock()) { using (_lockMgr.Lock()) { // your code } }
}

Таким образом я решил свою задачу. Обработка пользовательских действий стала выполнятся с высоким приоритетом, никто не пострадал, все выиграли.

Асинхронность

Так как объекты класса SemaphoreSlim поддерживают асинхронное ожидание, я так же добавил себе эту возможность. Код отличается минимально и в конце статьи я приведу ссылку на исходный код.

Более того, свойство Task. Здесь важно отметить то, что Task не привязан к потоку никаким образом, поэтому нельзя аналогичным образом реализовать асинхронное переиспользование лока. На этом мои варианты закончились. CurrentId по описанию MSDN не гарантирует ничего.

AsyncLock, в описании которого была указана поддержка переиспользования асинхронного лока. В поисках решения я наткнулся на проект NeoSmart. Но к сожалению сам лок не является локом. Технически переиспользование работает. Будте осторожны, если используете этот пакет, знайте, он работает НЕ правильно!

Заключение

В итоге получился класс поддерживающий синхронные операции с переиспользованием, и асинхронные операции без переиспользования. Асинхронные и синхронные операции можно использовать рядом, но нельзя использовать вместе! Все из-за отсутствия поддержки переиспользования асинхронным вариантом.

Библиотеку я выложил на github и в nuget. Надеюсь я не одинок в таких проблемах и моё решение кому то пригодится.

На асинхронной части этого теста проверялся NeoSmart. В репозитории есть тесты, показывающие работоспособность PriorityLock. AsyncLock, и тест он не прошел.

Ссылка на nuget
Ссылка на github

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть