воскресенье, 6 октября 2013 г.

О Lock'ах, GetHashCode и внутреннем устройстве объекта

Недавно, в Concurrent Programming on Windows прочитал такую вещь: когда у объекта со стандартной реализацией GetHashCode(не переопределенной) вызывается одноименный метод, CLR кэширует полученное значение.
И захотелось мне посмотреть как это там внутри устроено. Вот так и родилась эта статья.


Давайте вспомним немного внутренне устройство объекта. Лучшее описание из тех, что я видел находится здесь. Если коротко, то там говорится следующее: у любого объекта(кроме структур) есть два вспомогательных поля(у массивов есть третье - количество элементов) размером 4 байта на х86 и 8 байт на х64 каждое. Одно из полей это указатель на тип. Оттуда, например, runtime берет информацию о методах которые вы вызываете на объекте. Второе используется CLR для хранения разной информации. Например, вы вызвали GetHashCode для объекта(еще раз подчеркну, этот метод не должен быть переопределен) он будет кэширован в этом поле. Также, как говорит Duffy: COM interoperability information is also held here for certain objects. С COM как-то не приходилось работать, так что не совсем понимаю что это. Тем не менее, некоторая контекстная информация о COM тоже хранится здесь(при необходимости). Также, в заголовке объекта может храниться такая вещь как "thin lock" - это ID потока который владеет монитором и счетчик рекурсии.

Не трудно заметить, что для имеющихся свободных 4(8) байтов, информации, которую надо хранить, многовато. Плюс, в случае с мониторами, если некоторому потоку надо будет перейти в ждущее состояние, то надо будет где-то хранить информацию об объекте на котором будет выполняться ожидание.

Поэтому, как только CLR понимает, что заголовка объекта не хватает для хранения всей необходимой информации, он создает sync block для этого объекта, который хранится в специальной таблице, назовем ее SyncBlocks. В заголовке объекта записывается соответствующий индекс в этой таблице, а все данные из заголовка переносятся туда.

Давайте посмотрим как это все выглядит.
Создадим консольное приложение, захватим lock главным потоком:


Мы видим, что таблица SyncBlocks пуста. А команда DumpHead c флагом thinlock выводит нам информацию о "thin lock" - это информация о потоке который владеет монитором записанная в заголовке объекта.Значит места в заголовке объекта пока хватает для хранения информации.

Давайте в окне Watch вызовем метод GetHashCode объекта obj, вот что мы увидим теперь:


Теперь команда DumpHeap говорит, что у нас нет объектов у которых в заголовке хранились бы "thin lock". А команда SyncBlk, напротив, говорит, что вот сейчас у нас появился один элемент, назовем его "fat lock".

Аналогично, если у нас уже хранится "thin lock"  в заголовке объекта, пусть мы даже не вызывали GetHashCode, но в это время другой поток попытался захватить lock и естественно не смог, то ему надо перейти в состояние ожидания, для чего нужно будет создать объект ядра и сохранить где-то ссылку на него. В этом случае нам тоже не хватит места заголовке и будет создан элемент в таблице SyncBlocks.


На рисунке видно что "thin lock" у нас нету. Есть элемент в таблице SyncBlocks который говорит нам что наш объект захвачен потоком с ID 1, это основной поток приложения, а вот вручную созданный поток, ID 3, находится в ожидающем состоянии. Для этого был создан объект ядра и поэтому места в заголовке стало недостаточно. Как результат - перенос данных из заголовка объекта в таблицу.
После того как метод TestMethod отработает в обоих потоках мы увидим следующую картину:


Наш объект в таблице SyncBlock был удален, вообще, было заявлено что объекты ядра на которых ожидают потоки пытающиеся захватить монитор будут кэшироваться, видно тут какой-то эвристический алгоритм решил, что кэшировать ничего не надо.

Я не нашел как отобразить служебные поля в заголовке объекта, похоже в SOS нет такого функционала. И не совсем понятно как найти информацию об объекте ядра на котором ожидает поток, попытавшийся захватить монитор. Но в целом, после прочтения данной статьи у читателя должно сложиться хоть какое-то представление о том, для чего CLR нужны служебные поля объекта.

Испытывалось на Windows 7 64, .Net 4.5, VS2012.
Используемая литература: 1.Duffy Concurrent Programming on Windows, 2.MSDN



Комментариев нет:

Отправить комментарий