Red Architecture — красная кнопка помощи для сложных и запутанных систем — часть 3 (многопоточность нам в помощь)
Заключительную часть описания Red Architecture посвятим многопоточности. Ради справедливости стоит сказать, что начальный вариант класса v нельзя считать оптимальным, так как в нём ничего нет для решения одной из главных проблем к которой неминуемо приходят разработчики real world приложений. Для полного понимания текущей статьи необходимо познакомиться с концепцией Red Architecture здесь.
Забегая вперёд скажу, что нам удастся решить все проблемы многопоточности не выходя за пределы класса v. Причём изменений будет гораздо меньше чем могло показаться, и в итоге код класса v с полностью решёнными проблемами многопоточности будет состоять из немногим более 50 строк! Причём эти 50 с небольшим строк будут более оптимальны, чем вариант класса v, описанный в первой части. При этом конкретный код, решающий проблемы синхронизации потоков займёт всего лишь 20 строк! По ходу текста мы будем разбирать отдельные строки из листинга законченных классов v и Tests, которые приведены в конце данной статьи.
Где можно применить Red Architecture?Хочу подчеркнуть, что приведённые здесь примеры, как и вся концепция Red Architecture, предлагается для использования на всех возможных языках и платформах. С#/Xamarin и платформа .NET выбраны для демонстрации Red Architecture исходя из моих личных предпочтений, не более того.
Два варианта класса vУ нас будет два варианта класса v. Второй вариант, идентичный по функционалу и способу использования первому, будет устроен несколько сложнее. Зато его можно будет использовать не только в “стандартном” C# .NET окружении, но и в PCL окружении Xamarin, а значит — для мобильной разработки сразу под три платформы: iOS, Android, Windows 10 Mobile. Дело в том, что в PCL окружении фреймворка Xamarin не доступны потокобезопасные (thread safe) коллекции, поэтому вариант класса v для Xamarin/PCL будет содержать больше кода для синхронизации потоков. Именно его мы и рассмотрим в данной статье, поскольку упрощённый вариант класса v (тоже есть в конце данной статьи) представляет меньшую ценность с точки зрения понимания проблем многопоточности и способов их решения.
Чуть-чуть оптимизацииПрежде всего мы избавимся от базового класса и сделаем класс v самодостаточным. Нам не нужен механизм нотификации базового класса, который мы использовали до текущего момента. Унаследованный механизм не позволяет решить проблемы многопоточности оптимальным способом. Поэтому мы теперь “сами” будем рассылать события функциям-обработчикам:
В методе Add() в цикле foreach мы копируем элементы из HashSet'a в List и итерация происходит уже по листу, а не хешсету. Нам необходимо это сделать, поскольку значение, возвращаемое выражением handlersMap[key] является глобальной переменной, доступной из публичных мутирующих состояние класса методов, таких как m() и h(), следовательно, возможна ситуация, когда HashMap возвращаемый выражением handlersMap[key] будет модифицирован другим потоком во время итерации по нему в методе Add(), а это вызовет эксепшн рантайма, поскольку пока не закончена итерация по коллекции внутри foreach, её (коллекции) модификация запрещена. Именно поэтому мы «подставляем» для итерации не глобальную переменную, а List в который скопированы элементы глобального HashSet'a.
Но этой защиты недостаточно. В выражении
к значению (хешсету) handlersMap[key] неявно применяется операция копирования. Это определённо вызовет проблемы если в период между началом и окончанием операции копирования какой-нибудь другой поток попытается добавить или удалить элемент в копируемом хешсете. Поэтому мы ставим лок (Monitor.Enter(handlersMap[key])) на данный хешсет непосредственно перед началом foreach
и “релизим” (Monitor.Exit(handlersMap[key])) лок сразу после входа в цикл foreach
По правилам объекта Monitor количество вызовов Enter() должно соответствовать количеству вызовов Exit(), поэтому у нас есть проверка if (Monitor.IsEntered(handlersMap[key])) которая гарантирует, что если лок был установлен, то мы из него выйдем только один раз, в начале первой итерации цикла foreach. Сразу после строки Monitor.Exit(handlersMap[key]) хешсет handlersMap[key] станет снова доступен для использования другими потоками. Таким образом мы ограничиваем блокировку хешсета минимально возможным временем, можно сказать, что в данном случае хешсет будет заблокирован буквально на мгновение.
Сразу после цикла foreach мы видим повторение кода освобождения лока.
Этот код необходим на тот случай, если в foreach не произошло ни одной итерации, что возможно, когда для какого-то из ключей в соответствующем ему хешсете не будет ни одного обработчика.
Следующий код требует развёрнутого пояснения:
Дело в том, что в концепции Red Architecture единственным объектом созданным за пределами класса v и требующим синхронизации потоков являются функции-обработчики. Если бы мы не могли управлять кодом, который вызывает наши функции обработчики, нам бы пришлось в каждом обработчике “городить” что-нибудь вроде
Обратите внимание на строки lock() unlock() между которыми находится полезный код метода. Если внутри обработчика модифицируются данные, являющиеся по отношению к нему внешними, то lock() и unlock() было бы необходимо добавить. Потому что одновременно входящие в эту функцию потоки будут менять значения внешних переменных в хаотичном порядке.
Но вместо этого мы добавили всего одну строку на всю программу — lock(handlr), причём сделали это внутри класса v не трогая ничего за его пределами! Теперь мы можем писать сколько угодно функций обработчиков не задумываясь об их потокобезопасности, поскольку реализация класса v нам гарантирует, что только один поток может войти в данный конкретный обработчик, другие потоки будут “стоять” на lock(handlr) и ждать окончания выполнения работы в данном обработчике предыдущим вошедшим в него потоком.
foreach, for(;;) и многопоточностьВ листинге Tests (в конце статьи) есть метод foreachTest(string[] a) проверяющий работу цикла for(;;) во время одновременного входа в этот метод и, следовательно, в цикл for(;;) двух потоков. Ниже приведена возможная часть вывода этого метода: