Часть 1. Глава 1. Жалкий неудачник

Сергей Васильевич Виноградов
Предисловие
Мемуары написаны профессиональным программистом и могут быть интересны начинающим и будущим техническим специалистам. Эта книга не по программированию, однако я больше размышляю о программных системах, чем вспоминаю свою жизнь.
Читать книгу можно с любого места, пропуская то, что неинтересно.

Часть 1. Глава 1. Жалкий неудачник

Мне 33 года, но я начал писать мемуары. Можно сказать, что это - глупейшее занятие, ведь я слишком молод и мне нечего рассказать о своей жизни.
Мало того, я жалкий неудачник и давно не работаю. Мама кормит меня на свою пенсию.

Однако, я чувствую, что скоро умру, а сил на большую и хорошую книгу по программированию у меня нет. Поэтому я напишу мемуары, дабы люди, прочитавшие это, могли учиться на чужих ошибках.
К сожалению, я совершенно не знаю, как описать мои ошибки и иногда даже не знаю, в чём их причины, поэтому учиться на моих ошибках будет тяжело.
К сожалению же, чтобы учиться на чужих ошибках, нужно очень хорошо понимать, что было сделано и видеть причинно-следственные связи, которые я вряд ли смогу показать. Я просто не помню достаточно примеров из своего жизненного опыта и не всегда вижу скрытые связи между ними.

Вот сейчас, в последнем предложении выше, я написал формулировку, которая не выражает всей мысли и вообще сформулирована неверно. Но краткость - сестра таланта, и я сижу и пытаюсь понять, как же написать в одном предложении ту мысль, которую хотел написать.
А хотел написать я следующее:
Помнить примеры из своего жизненного опыта надо и надо хорошо
Я не помню эти примеры достаточно хорошо
У меня нет достаточного количества этих примеров, так как многие события я забыл вообще
Я не всегда вижу скрытые причинно-следственные связи, управлявшие этими событиями. Не вижу и в силу недостаточной интеллектуальной мощи, и в силу того, что просто недостаточно осведомлён о том, что происходило за моей спиной.

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

-----------------------------------------------

Кстати, сразу поговорим о том, как сделать так, чтобы почувствовать эту разницу более просто, так как это часто надо при анализе текстов. Для этого можно использовать один приём.
############
А именно, записывать отдельно каждое утверждение, которое делается в некотором тексте о каком-либо факте. А потом сравнить получившиеся списки фактов от двух разных текстов.
############

Так как я пишу в электронном виде, я не ограничен объёмом текста, поэтому я сразу же разберу обе формулировки для того, чтобы показать пример использования данного приёма. Если вам станет скучно, читайте ниже с разделителя "----". Или вообще не читайте.

И так, предложение "Я просто не помню достаточно примеров из своего жизненного опыта и не всегда вижу скрытые связи между ними".
1. Я существую на момент написания и способен помнить [это скрытое утверждение, которое надо обнаружить и записать]
2. "Я не помню достаточного количества примеров" либо "Я не помню примеры в достаточной степени". Формулировка предложения здесь не ясна и корява. [Кстати, часто это признак манипуляции читателем: читателю даётся неясная, неточная фраза, на которой тяжело строить логические утверждения. Поэтому логику текста можно незаметно нарушить для усиления манипуляции.]
3. Речь идёт о примерах "из своего жизненного опыта". Значит "свой жизненный опыт" имеется.
4. Количество примеров недостаточно для целей, предследуемых автором.
5. Автор иногда видит скрытые связи "между ними". В данном случае формулировка подразумевает "между примерами", но явно речь идёт не о примерах, а о другой сущности. Например, о событиях и их причинно-следственных связях. [Тоже признак манипуляции, но в данном случае это не манипуляция, а просто неудачная формулировка. На что указывает дальнейшее уточнение.]
6. Автор иногда не видит скрытые связи "между ними".
7. Для целей автора ("учить на чужих ошибках") нужно помнить достаточно примеров [это скрытое утверждение, в тексте явно не указано, но следует из связи между предложенями]
8. Для целей автора нужно всегда понимать причинно-следственные связи между событиями


Аналогичную процедуру сделаем с другой формулировкой.
1. Надо помнить примеры из своего жизненного опыта
2. Надо помнить эти примеры хорошо
3. Автор способен способен помнить примеры
4. Автор помнит эти примеры недостаточно хорошо
5. У автора нет дотаточного количества примеров
6. Количества примеров может быть недостаточно для целей автора
7. У автора нет достаточного количества примеров в связи с тем, что он их не помнит
8. Автор не всегда видит причинно-следственные связи, управлявшие событиями
9. Это значит, что причинно-следственные связи можно видеть
10. И они могут управлять событиями
11. Автор не видит потому, что у него недостаточно интеллектуальной мощи
12. Вообще такое бывает, что может быть недостаточно интеллектуальной мощи
13. Автор недостаточно осведомлён об этих связях
14. Недостаточно осведомлён в связи с тем, что это происходило за его спиной.
15. Возможно, автор хотел сказать, что от него пытались скрыть эти причинно-следственные связи: на это указывает его формулировка "за моей спиной" [не всегда от меня это скрывали. Опять неудачная формулировка, которая может ввести читателя в заблуждение]


Сравним списки.
Сначала пройдём по списку один и посмотрим, представлены все ли пункты этого списка во втором списке.
Пункт "я существую" не представлен в той же формулировке. Однако "способен помнить" представлен в пункте 3. Хотя формулировки утверждений чуть расходятся.
Пункт 2 представлен в пунктах 4 и 5 (второго списка). Но без предположений.
Пункт 3 не представлен. Это ошибка. При составлении второго списка я не подумал об этом факте. Что указывает на то, что составление такого списка дело не совсем тривиальное и зависит как от везения, так и от формулировок, с которых он составляется.
Пункт 4 не представлен. Хотя есть пункт 5 и 7, нет сторой формулировки о том, что у автора нет достаточного количества примеров именно "для целей автора".
Пункт 5. Не представлен. Точнее, он представлен пунктом 8, но это неверное представление. Вот это "Автор не всегда видит" - это именно два утверждения. Иногда видит, а иногда - нет.
Пункт 6. Представлен пунктом 8.
Пункт 7. Представлен в прямо противоположной формулировке пунктом 6. Хотя тоже спорно.
Пункт 8. Не представлен.

Как видим то, что мысль во второй формулировке более понятна, совершенно не означает, что выписать все подразумеваемые во второй формулировке факты проще. Во втором списке я забыл много чего.

Аналогично, можем сравнить и второй список с первым.
Пункт 1. Не представлен.
Пункт 2. Не представлен.
Пункт 3. Представлен, хотя формулировка чуть другая.
Пункт 4. Фактически, не представлен (только как предположение в пункте 2).
Пункт 5. Представлен как предположение в пункте 2.
Пункт 6. Не представлен.
Пункт 7. Не представлен. Так как это отдельный факт, но он совмещён с пунктом 2.
Пункт 8. Представлен в лучшем описании в пунктах 5 и 6
Пункт 9. Не представлен.
и так далее, мне лень дальше перечислять.

-----------------------------------------------

Что мы здесь видим? Не только факты из второй формулировки получаются другими, но и сами мысли идут чуть по другому направлению. Так сильно всё зависит от точности формулировки. Причём более точная формулировка не всегда влечёт за собой простоту написания по ней тех же фактов, что и из менее точной формулировки.

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


Ошибка.
Пытаясь записать нечто в одном предложении я смешал сразу много терминов и сразу запутался в них. Предложение тяжело сформулировать так, чтобы дать в нём много информации и я был поглощён не собственно вопросом о том, какую мысль хочу донести, а вопросом о том, как сформулировать предложение.

Вывод.
1. Иногда проще быть многословным.
2. Разные представления одной и той же информации воспринимаются по-разному. Поэтому иногда даже просто другая формулировка может помочь лучше взглянуть на мысль.
3. Одна выполняемая задача (сформулировать предложение верно) мешает более высокоуровневой задаче осознания всей мысли, которую я хочу сказать. То есть человек довольно ограничен в своих мыслительных способностях, поэтому иногда следует сначала сформулировать более пространно то, что хочешь сказать, а потом более строго. Или наоборот. В общем, выбор формулировки мысли иногда стоит делать уже после того, как мысль записана в промежуточной формулировке.


Зачем я так подробно всё это описыванию?
Это именно то, о чём я пишу мемуары. Об ошибках. Я не буду рассказывать о себе, о том, как я ходил в детский сад, пошёл в школу и так далее. А может и буду. Но смысл не в воспоминаниях. Я пишу об ошибках и методах их преодоления и выявления.

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

При этом необходимо не только записать все утверждения, но ещё и проверить их обоснованность.
У этого метода есть один недостаток: на него нужно очень много времени и внимания. Как видно выше, с первого раза можно даже пропустить некоторые утверждения. А проверять надо все. То есть приходится проходить по тексту повторно и задумываться над тем, какие утверждения ещё ты пропустил или неверно сформулировал.

############
Многократные проходы - это тоже метод. Методом является и привычка исходить из того, что мы обязательно сделали ошибку в своей работе на предыдущем проходе. А раз сделали ошибку, значит должны проверить текст и найти её. Или проверить нашу создаваемую или ремонтируемую техническую систему: как её проект, так и её саму на какой-то стадии до начала следующей стадии создания или перед пуском.
############

Человек не способен осознавать большой объём информации одновременно, не способен оперировать большим количеством объектов (или мы не знаем, как это делать).
Кроме этого, при мыслительной деятельности мы совершаем ошибки и это - закономерность. Как бы хорошо мы не работали, любая мало-мальски сложная логическая задача вызывает появление ошибок.
Отсюда и идёт многократность (итеративность). Потому что для выявления ошибок надо проверить то, что уже написано, а чтобы учесть больше факторов необходимо сконцентрироваться в каждом проходе на чём-то одном, так как делать качественно несколько сложных разных работ человек, обычно, не в состоянии.

В проходах не обязательно искать ошибки, можно и добавлять что-либо (инкрементальная разработка), видоизменять и так далее.
При поиске ошибок есть два метода разделения на проходы. Это специализация проходов (от общего к частному) и разукрупнение (спуск от целого к частям) [я сам только что придумал эти названия].

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

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

1. Так, для начала мы можем спросить себя, все ли объекты, которые используются разными потоками, синхронизированны.
Для этого нам придётся сначала выписать объекты, которые используются. Либо проверку можно делать в несколько проходов, разукрупняя программу, проверяя только один объект одновременно.
Так, видя один новый объект с которым работает программа, мы смотрим, происходит из него только чтение, не требующее синхронизации, или какие-либо требующие синхронизации процедуры. При этом мы осуществляем проход сразу по всему коду с целью определения того, нуждается ли объект в синхронизации (если точно не помним, что нуждается). Когда поняли, что нуждается, осуществляем следующий проход и смотрим, нет ли в программе несинхронизированного доступа к этому объекту.
Необходимо не забывать, что синхронизация должна осуществляться одним и тем же способом синхронизации. То есть синхронизация через Interlocked (в Intel x86 и x64 - префикс lock перед машинной командой, который осуществляет аппаратную операцию синхронизации, например, блокировку системной шины) не является взаимозаменяемой с синхронизациями через, скажем, ключевые слова lock в C# (через критические секции и мьютексы). Да и синхронизации разными примитивами синхронизации не являются взаимозаменяемыми. то есть нужно контролировать, что используется один и тот же объект и способ синхронизации во всех случаях синхронизации.

Сразу же исправляем ошибки. Если ошибки исправлены, значит код изменился. Внося изменения мы делали то, что вносит новые ошибки. Поэтому всю процедуру так или иначе придётся повторять заново ещё раз (можно начать новый проход сразу же или сначала пройти по другим пунктам в этом проходе).

После того, как мы сделали этот проход, есть один нюанс. Мы делали проход по коду, который проверяли, но реально объекты могут быть использованы и в других участках кода. Поэтому нам надо повторить либо этот проход сразу либо всю процедуру проверки (все проходы) уже с поиском доступа к этим объектам из других участков кода, а может быть, даже проверять код исходя из предположения, что такие участки появятся потом. Задуматься, будет ли возможно правильно синхронизировать доступ из других участков кода? Допустим, мы синхронизировали объект с помощью какого-то локального примитива синхронизации, которые будет недоступен в другом участке кода - тогда синхронизацию из того участка будет выполнить невозможно.
Таким образом, мы специализировали проходы ещё и по тому, являются они внешними или внутренними, потенциальными или реальным ошибками.


2. Затем, мы можем спросить себя нет ли тут взаимоблокировок. То есть найти в коде участки, которые работают сразу под двумя блокировками (или более). Опять же, повторить процедуру для внешних участков кода (ещё один проход) и подумать над тем, что будет, если появится какой-то другой участок кода (будет ли программист, который пишет этот участок, вообще знать, в каком порядке нужно получать блокировки?).

3. Не слишком ли долго мы находимся в ожидании синхронизации, эффективна ли многопоточность?

4. Все ли глобальные (для потоков) переменные инициализированны до того, как начнут работу все потоки.

5. Что будет, если поток окончится с ошибкой или перетратит ресурсы?

6. Что будет, если поток не завершится?

7. Может ли быть такое и что будет, если поток не начнёт своего исполнения, а мы будем ждать, пока он завершится?

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

9. Можно ли иметь доступ к этому объекту из потоков, их которых к нему происходит доступ (некоторые объекты, иногда, должны обрабатываться из того же потока, который их создал). Все ли потоки могут получить нужную информацию и сделать нужные операции (иногда бывает так, что, скажем, авторизация привязана только к одному потоку)?

10. Везьде ли потоки верно возвращают свой результат? Должен ли быть результат независим от порядка выполнения команд и достигается ли эта независимось? Хватит ли памяти для хранения результата и исходных данных?

11. Есть ли возможность отменить многопоточную операцию (если это необходимо) или проконтролировать её прогресс?

12. Не будет ли такого, что события или даже все события будут пропущены (бесконечное ожидание уже свершившегося события), в том числе, в случае неожиданно быстрого (аварийного) завершения потоков)?

13. Смогут ли потоки, вызывающие многопоточную операцию ожидать её синхронно и синхронно с другими операциями?

14. Хватит ли памяти (других ресурсов) работающим одновременно потокам? Есть ли проверки времени выполнения на выполнение предположений относительно этого (если такие предположения делаются)?

15. Нет ли внешних объектов, доступ к которым происходит неявно и несинхронно (получение или обновление данных из файлов, однопоточные библиотеки и алгоритмы с заранее выделенными участками памяти)?

16. Нет ли неявных операций, которые будут длительно выполняться внутри блокировки (вставка в длинные списки на основе массивов, выделение памяти, сборка мусора).


-----------------------------------------------

Для каждого программного продукта вопросы могут удаляться или добавляться в зависимости от его специфики и статистики ошибок. Для последнего нужно на каждую ошибку многопоточности спрашивать, какой вопрос надо было задать, чтобы ошибка была выявлена при анализе?


Ещё один пример многопроходности можно привести с точки зрения методики работы корректора. Это метод разукрупнения (как он называется у корректоров я не знаю). Сначала читается некоторый участок текста и ошибки исправляются в нём, затем читается предложение. После чего читаются конкретные слова и проверяются согласования слов. То есть чтение идёт сначала крупными блоками, потом поиск идёт в мелких единицах. Я не корректор, поэтому, возможно, изложил не совсем верно.


В завершение главы я хочу вернуться к примеру с формулировкой предложения.
Я описал, что человек не может окинуть взглядом, иногда, даже одну собственную мысль. Мысли, как таковой, нет. Она формируется последовательно, сначала первая часть, потом вторая, третья и так далее (а предыдущие части забываются).
Часто так и делается. Человек не запоминает информацию, он запоминает задачу, которую нужно решить, чтобы получить информацию.
Он не выражает мысль, он создаёт её в процессе выражения.
Поэтому важно, каким именно способом, в каком порядке он её выражает (создаёт).


Допустим, я пытаюсь вспомнить теорему Куратовского-Понтрягина.

Реально я вспоминаю примерно следующее:
Если граф не планарен, то в нём есть графы K5 и K3,3.
И вот тут я допустил ошибку. Так как может быть и только один граф, а я уже сформулировал так, что есть оба графа. И вообще, теорема является утверждением о планарных графах, а не наоборот (я не стал писать весь путь, как я обнаружил эту ошибку).
Перевспомню.
Если граф планарен, то в нём нет графов K5 и K3,3.
Потом я вспоминаю, что это неточная формулировка теоремы. В формулировке теоремы выступали гомеоморфные графы.
Поэтому я могу вспомнить, что речь идёт о том, что граф планарен, если в нём нет графов, гомеоморфных K5 и K3,3. Причём я могу ещё даже не очень помнить, что это за графы за такие, так как человек может запомнить сами термины без смысла (а может и смысл, а термины забыть).
Осталось вспомнить, что K5 - это полносвязный граф с пятью вершинами (объединение пентаграммы и звезды), а K3,3 - это три поросёнка и три колодца (полный двудольный граф с тремя вершинами в каждой доле).
Ну и то, что гомеоморфные графы, по сути, это графы, которые можно друг к другу приравнять, осуществив одну дополнительную операцию: возможно, придётся включить некоторые дополнительные вершины в графы посреди их рёбер (то есть приравнять не сами графы, а их подразбиения).

Всё. Теорему вспомнил.
(есть ещё одна формулировка: "граф планарен тогда и только тогда, когда он не содержит подразбиение графа K5 или подразбиения графа K3,3".)

Тут можно увидеть то, что я вспоминаю последовательно, уточняя формулировку (я не совсем точно записал, как вспоминал, т.к. реально ошибку о планарности/не планарности я обнаружил позже).
При этом сначала я пользуюсь более простыми формулировками. Вместо "содержит графы, гомеоморфные K5 и K3,3" я просто вспоминаю, что "нет графов K5 и K3,3".
То есть я вспоминаю нестрогую формулировку. Её легко и просто запомнить.
Затем вспоминаю, что есть случаи, когда эта нестрогая формулировка неверна. Смысл в том, что подграфы могут быть гомеоморфны, а не обязательно равны (изоморфны).

Затем я начинаю вспоминать уже значения терминов, причём где-то образно (три поросёнка), а где-то скорее формально (граф из пяти вершин, каждая из которых связана со всеми другими). Три поросёнка лично мне запомнить проще.
Таким образом, мы можем увидеть, что запоминание происходит с использованием двух приёмов:
1. Более сложные термины заменяются более простыми, легко запоминаемыми. А к такому упрощению запоминается то, что это именно упрощение.
2. Я сначала вспоминаю формулировку с терминами, а потом уже вспоминаю значение терминов.

То есть, если мы хотим проще оперировать какими-то формулировками, нам нужно:
1. Ввести новые термины, которые будут упрощать запоминание и понимание.
2. Более сложные термины можно иногда представить какими-то образами, упрощениями. Но с запоминанием того, в чём именно упрощение.

Почему это работает?
Каждый термин - это установление связи между какими-то, как минимум, двумя другими терминами.
Поэтому, если мы используем формулировку без термина, то вместо одного термина у нас появляется как минимум два других термина и связь между ними. То есть количество объектов, которыми должен манипулировать человек, работая с формулировкой, увеличилось.
Однако, человек может оперировать лишь ограниченным количеством объектов, а запомнить может допольно много. Поэтому формулировка с меньшим количеством объектов часто запоминается проще, чем с большим колчеством объектов.
Даже если термин является лишь сокращением другого термина, может оказаться, что это тоже облегчает работу. Так как человеку не нужно отвлекаться на то, чтобы распознать формулировку длинного термина. Поэтому больше ресурсов мозга остаётся на целевую деятельность.

В то же время, формулировка может быть дополнена различного рода примерами, которые человек также относительно легко запоминает, и которые позволяют ему лучше овладеть формулировкой.
Например, иллюстрацией к теореме Куратовского может быть подразбиение графа три поросёнка - три колодца.

Есть три домика, в которых живут поросята, (вершины графа) и есть три колодца (вершины графа). Между каждым домиком и каждым колодцем есть тропинка (ребро графа), других тропинок нет. То есть имеем двудольный граф с шестью вершинами и шестью рёбрами.
Однако, мы можем дополнить этот граф блокпостами. Например, на всех тропинках, ведущих к третьему колодцу есть блокпосты (дополнительные вершины графа). Мы вставили дополнительные вершины, разделив часть рёбер графа пополам. Вставка вершин, делящих рёбра - это создание нового подразбиения графа.
Теперь в графе уже не 6, а 9-ть вершин, но он по-прежнему непланарен. И он гомеоморфен графу K3,3.

Теперь мы имеем простой пример непланарного графа, содержащего (точнее, даже не содержащего, а изоморфного, то есть равного с точностью до подстановки) граф, гомеоморфный графу K3,3. Это просто граф, где на некоторые рёбра вставлены дополнительные вершины.

Этот пример дополнительно иллюстрирует то, зачем в определении теоремы используется гомеоморфизм. Действительно, если граф непланарен, то его подразбиение тоже не планарно.


Как сокращения термины выполняют задачу не только упрощения чтения, но и упрощения написания. Когда-то я выводил формулы, где были довольно длинние подинтегральные выражения. Я мучался, каждый раз переписывая их всех. Получалось очень длинно. Мой научный руководитель просто заменил часть из этих выражений кратким обозначением. Так, что все длинные, но ненужные подинтегральные выражения просто записывались одной буквой. Это сильно сократило объём вывода формул.


Конец первой главы