Что вы называете «потокобезопасным»?
Предостережение: я не эксперт по многопоточному программированию. На самом деле, я бы даже не стал утверждать, что я в нём компетентен. За всю мою карьеру, необходимость написать код, который запускает второй рабочий поток, возникала, вероятно, менее полудюжины раз. Так что воспринимайте всё, что я пишу на эту тему, с некоторым скептицизмом.
Вопрос, который мне часто задают: «потокобезопасен ли этот код? ». Для ответа на этот вопрос, нам явно нужно знать, что означает «потокобезопасен».
Но, есть кое-что, что я хочу прояснить перед тем, как мы погрузимся в это. Вопрос, который мне задают значительно реже – «Эрик, почему Мишель Пфайфер всегда так хорошо выглядит на фотографиях? » За помощью в ответе на этот животрепещущий вопрос я обратился к Википедии:
«Фотогеничный субъект – это субъект, который обычно оказывается физически привлекательным или красивым на фотографиях.»
Почему Мишель Пфайфер всегда так хорошо выглядит на фотографиях? Потому, что она фотогенична. Очевидно.
Ну, хорошо, что мы разгадали эту тайну, но, похоже, я несколько отклонился от предмета обсуждения. Википедия столь же полезна в определении потокобезопасности:
«Код потоково-безопасный, если он функционирует корректно при использовании из нескольких потоков одновременно.»
Как и с фотогеничностью, это очевидно, провоцирует вопросы. Когда мы спрашиваем «является ли этот код потокобезопасным? », то на самом деле мы хотим узнать «является ли этот код корректным, будучи вызван определённым образом?» Так как же мы определим, корректен ли код? Мы, собственно, ничего здесь не объяснили.
Википедия продолжает:
«В частности, он должен обеспечивать корректный доступ нескольких потоков к разделяемым данным»
Это выглядит честным; этот сценарий почти всегда и есть то, что люди имеют в виду, говоря о потокобезопасности. Но затем:
«и обеспечивать доступ к фрагменту разделяемых данных только одному потоку в каждый момент времени. »†
Теперь мы говорим о техниках обеспечения потокобезопасности, а не об определении того, что означает потокобезопасность. Блокировка данных, так, чтобы к ним мог обращаться только один поток за раз – это только одна из возможных техник обеспечения потокобезопасности; это само по себе не определение потокобезопасности.*
Мой аргумент не в том, что определение неверно; как неформальное определение потокобезопасности это не слишком ужасно. Скорее, мой аргумент в том, что определение показывает что сама концепция абсолютно туманна и, в сущности, означает не более, чем «ведёт себя корректно в некоторых ситуациях». Так что, когда меня спрашивают «потокобезопасен ли этот код?», мне всегда приходится отступать и спрашивать «о каких конкретно многопоточных сценариях вы беспокоитесь?» и «какое конкретно поведение объекта корректно в каждом из этих сценариев?»
Проблемы коммуникации возникают тогда, когда люди с различными ответами на эти вопросы пытаются общаться о потокобезопасности. Например, представьте, что я сказал вам, что у меня есть «потокобезопасная изменяемая очередь», которую вы можете использовать в своей программе. Вы затем радостно пишете следующий код, который выполняется в одном потоке, пока другой поток занят добавлением и удалением элементов из изменяемой очереди:
if (!queue.IsEmpty) Console.WriteLine(queue.Peek());
Затем ваш код падает, когда Peek бросает QueueEmptyException. Что происходит? Я же сказал, что штуковина потокобезопасна, но ваш код всё равно падает в многопоточном сценарии.
Когда я говорил «очередь потокобезопасна», я имел виду, что очередь поддерживает своё внутреннее состояние целостным, независимо от того, каков порядок отдельных операций, выполняемых в других потоках. Но я не имел в виду, что вы можете использовать мою очередь в произвольном сценарии, который требует поддержания логической целостности между несколькими последовательными операциями. Короче, моё мнение насчёт «корректного поведения» и ваше мнение на ту же тему разошлись, потому, что подразумеваемые нами сценарии были совершенно различны. Меня беспокоило только отсутствие сбоев, но вам важно иметь возможность логически рассуждать об информации, возвращаемой из каждого вызова метода.
В этом примере мы с вами, вероятно, говорим о различных видах потокобезопасности. Потокобезопасность изменяемых структур данных обычно сводится к гарантии, что операции над разделяемыми данными всегда работают с самым свежим состоянием разделяемых данных по мере изменения, даже если это означает, что некоторая комбинация операций оказывается логически несогласованной, как в нашем примере выше. Потокобезопасность неизменяемых структур данных сводится к гарантии того, что использование данных во всех операциях логически согласовано, ценой того факта, что вы смотрите на неизменяемый отпечаток данных, который может оказаться устаревшим.
Проблема здесь в том, что выбор, получать ли доступ к первому элементу, основан на «несвежих» данных. Проектирование полностью потокобезопасной изменяемой структуры данных в мире, где ничему не позволено быть устаревшим может быть весьма сложным. Подумайте, что бы вам пришлось сделать, чтобы операция «Peek» выше стала реально потокобезопасной. Вам бы потребовался новый метод:
if (!queue.Peek(out first)) Console.WriteLine(first);
«Потокобезопасно» ли это? Выглядит определённо лучше. Но что, если после Peek, другой поток опустошает очередь? Теперь вы не падаете, но вы значительно изменили поведение предыдущей программы. В предыдущей программе, если после проверки в другом потоке выполнилась операция, изменившая статус первого элемента, то вы либо падали, либо печатали свежий первый элемент очереди. Теперь вы печатаете устаревший первый элемент. Корректно ли это? Нет, если вы хотите всегда оперировать свежими данными!
Но, минуточку – на самом деле, в предыдущей версии кода тоже была эта проблема. Что, если из очереди извлекли элемент в другом потоке после того, как завершился вызов Peek, но до того, как выполнился вызов Console.WriteLine? Опять же, вы бы вывели устаревшие данные.
Что, если вы хотите гарантировать, что всегда печатаете свежие данные? Вот, что нужно вам на самом деле, чтобы сделать это потокобезопасным:
queue.DoSomethingToHead(first=>{Console.WriteLine(first);});
Теперь автор очереди и пользователь очереди договорились о том, какие сценарии нужны, так что это и вправду потокобезопасно. Правильно?
За исключением... в том делегате может быть что-то суперсложное. Что, если код делегата случайно вызывает событие, которое заставляет выполниться код в другом потоке, который в свою очередь запускает некую операцию с очередью, которая затем блокируется таким способом, что мы получили взаимоблокировку? Является ли взаимоблокировка «корректным поведением»? И если нет, то является ли этот метод истинно «безопасным»?
Буэ.
Теперь, я уверен, вы поняли, к чему я клоню. Как я указывал ранее, нет смысла говорить, что здание или кусок кода «безопасны» без некоторого описания того, от каких угроз применяемый механизм безопасности защищает, а от каких – нет. Аналогично, нет смысла говорить, что код потокобезопасен без некоторого описания того, какие виды нежелательного поведения предотвращаются применяемыми механизмами потокобезопасности, а какие – нет. «Потокобезопасность» - это ни больше, ни меньше, как контракт кода, как и любой другой контракт кода. Вы соглашаетесь общаться с объектом определённым образом, и он соглашается возвращать вам корректные результаты, если вы так делаете; выяснение того, каков именно этот образ, и что считать корректным результатом, представляет собой потенциально сложную задачу.
************
(†) В русскоязычной Википедии этот фрагмент определения отсутствует, есть в оригинале. – прим. перев.
************
(*) Да, я в курсе, что если я думаю, что что-то в Википедии неверно, то я могу это изменить. Есть две причины, по которым мне не стоит этого делать. Во-первых, как я уже заявил, я не эксперт в этой области; я оставляю экспертам разобраться между собой, что здесь будет правильно сказать. И во-вторых, смысл моего утверждения не в том, что страничка в Википедии неверна, а скорее в том, что она иллюстрирует неопределённость термина по его природе. – Эрик.
Comments
Anonymous
December 12, 2009
скажите, пожалуйста, будут ли ещё переводы блога Эрика? очень ждемAnonymous
October 19, 2010
хороша стаття, але часта зміна italic <-> normal виводить.Anonymous
July 11, 2014
Здравствуйте Гайдар, Огромное спасибо за перевод данной статьи, очень познавательно!