10.2.1. Анализ кода на предмет выявления потенциальных ошибок
Я уже упоминал, что анализ многопоточного кода на предмет выявления ошибок, связанных с параллелизмом, надо проводить тщательно, прочёсывая код мелким гребнем. Если возможно, попросите заняться этим кого-нибудь другого. Поскольку этот человек не писал код, то ему придётся думать, как он работает, и это поможет обнаружить скрытые ошибки. Важно, чтобы у рецензента было достаточно времени — нужно не проглядеть код мельком, за пару минут, а тщательно и усидчиво проанализировать. Большинство ошибок, связанных с параллелизмом, поверхностный читатель не увидит — обычно для их проявления нужно редкое сочетание временных соотношений.
Коллега, если вам удастся упросить его проанализировать ваш код, будет смотреть на него свежим взглядом и под другим углом зрения, чем вы сами. Поэтому он может заметить вещи, ускользнувшие от вашего внимания. Если коллег нет, попросите приятеля, можете даже выложить свой код в Интернет (не оскорбляя чувств юристов компании). Но даже если не найдется никого, кто проанализирует ваш код, или если рецензент ничего не обнаружит, все равно не отчаивайтесь — на этом свет клином не сошелся. Для начала имеет смысл на время отложить код — поработать над другой частью программы, книжку почитать, погулять. Во время перерыва вы будете подсознательно обдумывать задачу, заняв сознание чем-то другим. А когда вернетесь к коду, он будет казаться не таким знакомым, и, возможно, вам самому удастся взглянуть на него другими глазами.
Вместо того чтобы обращаться за помощью, можете проанализировать свой код самостоятельно. Например, полезно попытаться во всех деталях объяснить кому-нибудь, как он работает. Это даже необязательно должен быть человек — вполне подойдёт плюшевый медвежонок или надувной цыплёнок. Лично мне очень помогает написание подробных заметок. По ходу объяснения думайте над каждой строкой, рассказывайте, что может произойти, к каким данным происходят обращения и т.д. Задавайте себе вопросы о программе и объясняйте свои ответы. Мне кажется, что это очень действенная методика — задавая себе вопросы и тщательно продумывая ответы, зачастую удается выявить проблемы. Причем задавать вопросы полезно при анализе любого кода, а не только своего собственного.
Над какими вопросами следует задуматься при анализе многопоточного кода
Я уже говорил, что рецензенту (неважно, является он автором программы или нет) полезно задавать конкретные вопросы по поводу анализируемой программы. Они позволяют сосредоточиться на деталях кода и выявить потенциальные проблемы. Лично я люблю задавать вопросы, перечисленные ниже, хотя это, конечно, далеко не исчерпывающий список. Вам, возможно, помогут лучше сконцентрироваться совсем другие вопросы.
• Какие данные нужно защищать от одновременного доступа?
• Как вы обеспечиваете защиту этих данных?
• В каком участке программы могут в этот момент находиться другие потоки?
• Какие мьютексы удерживает данный поток?
• Какие мьютексы могут удерживать другие потоки?
• Существуют ли ограничения на порядок выполнения операций в этом и каком-либо другом потоке? Как гарантируется соблюдение этих ограничений?
• Верно ли, что данные, загруженные этим потоком, все еще действительны? Не могло ли случиться, что их изменили другие потоки?
• Если предположить, что другой поток может изменить данные, то к чему это приведёт и как гарантировать, что этого никогда не случится?
Последний вопрос — мой любимый, потому что заставляет думать о взаимосвязях между потоками. Допустив, что в некоторой строке имеется ошибка, вы дальше перевоплощаетесь в сыщика, которому нужно раскрыть преступление. Чтобы убедить себя в отсутствии ошибки, требуется рассмотреть все граничные случаи, приняв во внимание любой возможный порядок операций. Это особенно полезно, если данные в разные моменты времени защищаются разными мьютексами, как, например, обстояло дело в потокобезопасной очереди из главы 6, где мы завели разные мьютексы для головы и хвоста очереди. Чтобы гарантировать безопасность доступа в момент, когда захвачен один мьютекс, нужна уверенность в том, что поток, удерживающий другой мьютекс, не будет пытаться получить доступ к тому же элементу. Очевидно, что общедоступные данные, а также данные, на которые программа может получить ссылку или указатель, нужно анализировать особенно пристрастно.
Предпоследний вопрос из списка также важен, потому что касается очень распространенной ошибки: если вы освобождаете, а затем снова захватываете мьютекс, то должны предполагать, что другие потоки могли изменить разделяемые данные. На первый взгляд, очевидно, но если операции с мьютексами не видны — например, потому что скрыты внутри какого-то объекта, — то вы неосознанно допускаете именно эту ошибку В главе 6 мы видели, как это может привести к гонке и ошибкам, когда функции в потокобезопасной структуре данных слишком детализированы. Если для стека, не безопасного относительно потоков, наличие отдельных операций top() и pop() оправдано, то для стека, к которому могут одновременно обращаться несколько потоков, это уже не так, потому что между этими двумя вызовами внутренний мьютекс не захвачен, и, значит, какой-то другой поток может модифицировать стек. В главе 6 мы видели, что для решения этой проблемы нужно объединить обе операции в одну — выполняемую под защитой одной и той же блокировки мьютекса. Тем самым опасность гонки устраняется.
Итак, вы проанализировали код (или это сделал кто-то другой). Вы уверены, что в нем нет ошибок. Но критерием истины, как известно, является практика — как можно протестировать код, подтвердив или опровергнув вашу веру в отсутствие ошибок?