[Перевод] 7 самых неприятных проблем в программировании
Известно, что на старых картах, на неизведанных территориях, часто помещали зловещее предупреждение: «Здесь живут драконы». Вероятно, смысл этого предупреждения состоял в том, что не стоит входить в это пространство мира, не будучи готовыми сражаться с внушающим ужас противником. Всё что угодно может случиться на этих загадочных просторах, и нередко такое «что угодно» может закончиться очень плохо.
Программисты, возможно, являются несколько более цивилизованными, чем средневековые рыцари — однако это вовсе не означает, что современный технический мир не имеет своих технических драконов, поджидающих нас в непредвиденных местах: сложные проблемы, которые всплывают именно тогда, когда подходит срок сдачи работы или в моменты особенно высоких нагрузок и ответственной работы; конкуренты, которые прочитали руководство и знают, что недостаточно хорошо реализовано; злые «бесы», которые знают, как использовать неустранённые до конца баги и дефекты программ, причём узнают это часто сразу же после того, как программа сдана в использование.
Находятся люди, которые спокойно спят ночью, согреваемые своей наивной уверенностью, что компьютеры совершенно предсказуемы и без устали выдают потоком правильные ответы. Как же мало им ведомо! Несмотря на серьёзнейшие усилия разработчиков чипов, создателей языков и миллионов программистов, есть ещё в программировании опасные чащи, которые могут погубить даже самых продвинутых из программистов и разработчиков.
Вот семь из устрашающих уголков мира программирования, на которых легко можно написать: «Здесь живут драконы».
Многопоточность
Выглядит хорошей идеей. Разбить вашу программу на независимые секции и дать возможность операционной системе обрабатывать их как отдельные небольшие программы. Если процессор имеет четыре, шесть, восемь или больше ядер, то почему бы не написать программу так, чтобы иметь четыре, шесть, восемь или больше потоков, использующих все ядра независимо?
Идея работает — когда все части являются, действительно, полностью раздельными и никак не связаны друг с другом. Но если они должны получать доступ к одним и тем же переменным или записывать биты в одни и те же файлы — игра окончена. Какой-то из потоков выйдет на эти данные первым, и невозможно предсказать какой.
Поэтому мы создаём мониторы-синхронизаторы, семафоры и другие средства для упорядочения мультипоточной путаницы. Когда они работают, то дело идёт. Они просто вводят другой уровень сложности и превращают действие по сохранению данных в какой-то переменной в операцию, которая требует некоторого дополнительного интеллектуального усилия.
Когда и если они не работают, то воцаряется хаос. Получаемые данные теряют смысл. Столбцы не стыкуются. Деньги исчезают со счетов со свистом. И это всё — просто биты в памяти. И большая удача, если удастся определить какой-либо из них. В большинстве случаев разработчики заканчивают тем, что блокируют значительные участки структуры данных так, чтобы только один поток мог взаимодействовать с ними. Это позволяет остановить хаос, но только за счёт ликвидации большей части положительных сторон наличия несколько потоков, работающих на тех же данных. Вы просто могли бы переписать такую программу как «однопоточную».
Замыкания
Где-то по ходу дела кто-то решил, что было бы полезно передавать функции, как если бы они были данными. Это работало хорошо в простых случаях, но программисты начали осознавать, что возникают проблемы, когда функции стали выходить снаружи на самих себя и получать доступ к другим данным, часто называемыми «свободными переменными». Какая версия является правильной в этом случае? Данные на момент инициализации вызова функции? Или же на момент её фактической работы? Это особенно важно для JavaScript, где данные моменты могут быть сильно разнесены.
Решение — «замыкание» — является одной из самых больших причин головной боли для программистов JavaScript (а теперь и Java и Swift). Новички и даже многие опытные программисты не могут понять, что замыкается и где могли бы быть границы этого так называемого замыкания.
Название не помогает — это не похоже на предупреждение о постоянном закрытии доступа вроде предложения для посетителей паба сделать последний заказ (перед его закрытием). В любом случае доступ открыт, но только через «червоточину» в континууме данные-время — странном механизме сдвига во времени, который, должно быть, навеян научно-фантастическими сериалами. Но названия «комплексный механизм доступа к стеку» или «система манипулирования управлением данными» представляются слишком длинными, поэтому было выбрано «замыкание». И не будем начинать разговор о том, кто должен поплатиться за несвободные переменные.
Слишком большие данные
Когда ОЗУ начинает переполняться, всё падает. Не имеет значения, выполняете вы навороченный статистический анализ потребительских данных или работаете с привычной старой электронной таблицей. Когда машина выходит за пределы ОЗУ, то она обращается к т.н. виртуальной памяти, которая действует на чрезвычайно медленном (по сравнению с ОЗУ) жёстком диске или твердотельном накопителе. Это, конечно, лучше, чем полная остановка или завершение задания, но работа замедляется в любом случае.
Проблема в том, что быстродействие жёстких дисков в 20–30 раз меньше, чем у ОЗУ, а накопители, массово продаваемые на рынке, работают ещё медленнее. Если какой-то другой процесс также пытается в это время писать на диск или считывать с него, то ситуация становится просто драматической, поскольку эти накопители могут выполнять только одну задачу в каждый момент времени.
Активация виртуальной памяти обостряет другие, скрытые проблемы вашего программного обеспечения. Если имеются какие-то потоковые дефекты, то они начинают проявляться намного сильнее, поскольку потоки, попадающие в виртуальную память на жёстком диске, обрабатываются намного медленнее, чем другие потоки. Другие потоки «подвисают» лишь на то время, пока поток-неудачник находится в этой памяти. Если программа сделана хорошо, то результатом является заметное замедление. Если в программе есть дефекты, то они быстро приводят к катастрофе. И это лишь один маленький пример.
Управление подобной ситуацией является серьёзным вызовом для программистов, оперирующих большими наборами данных. Любой, кто хоть немного расслабляется при проектировании структур больших наборов данных, неизбежно заканчивает программой, работающей неприемлемо медленно. Она сможет нормально пройти несколько тестов, но реальные нагрузки отправят её штопором в отказ.
NP-полная задача
Каждый, имеющий университетское образование по информатике, знает о таинственных проблемах, скрытых за некоторым сокращением, которое редко разъясняют: «полиномиальная для недетерминированной машины Тьюринга задача поиска и принятия решения» или, иначе, NP-полная задача. Подробное их изучение занимает целый семестр курса информатики, но и в этом случае многие студенты выходят с туманным представлением. В основном о том, что никто не может решить эти проблемы из-за их исключительной трудности.
Проблемы NP-полной задачи часто являются довольно сложными — если пытаться решить их, используя просто грубый, силовой подход. Например, длительность решения «проблемы коммивояжёра» растёт экспоненциально по мере увеличения количества точек маршрута. Решение «задачи о рюкзаке» нахождением подмножества чисел, которые подходят ближе всего к некоторому значению N, требует перебора всех возможных подмножеств, число которых чрезвычайно велико. Все с опасением стараются уйти от этих проблем, потому что они являются примером одного из самых страшных компьютерных «монстров»: немасштабируемые алгоритмы.
Нюанс в том, что некоторые проблемы NP-полных задач легко решаются при некотором приближённом решении. Соответствующие алгоритмы не обещают точного решения, но дают результат, довольно близкий к точному. Они, возможно, не дадут идеального маршрута для коммивояжёра, но их решение будет отличаться от правильного лишь на несколько процентов.
Существование таких довольно хороших решений делает «драконов» только более загадочными. Никто заранее не может быть уверен, действительно ли трудны эти проблемы или же они могут быть сравнительно легко решены — если вас устроит ответ, который будет просто хорошим.
Безопасность
«Есть познанное знание — то, о чём мы знаем, что знаем», — сказал однажды на пресс-конференции Дональд Рамсфельд, министр обороны второй администрации президента США Джорджа Буша. «Есть также познанное незнание — то, о чём мы знаем, что не знаем. Но есть ещё и непознанное незнание — это то, о чём мы не знаем, что не знаем.»
Рамсфельд говорил о войне в Ираке, но то же самое справедливо и для компьютерной безопасности. Самой большой проблемой являются дыры, о которые мы даже не знаем, что они возможны. Все понимают, что необходимо сделать свой пароль трудным для разгадки; это — познанное знание. Но кто-нибудь когда-нибудь хотя бы говорил, что ваше сетевое оборудование имеет свой собственный программный уровень, спрятанный глубоко внутри? Возможность не заниматься взломом вашей ОС, а вместо этого нацелиться на данный «секретный» уровень является непознанным незнанием.
Возможность подобного проникновения можно теперь не рассматривать как «непознанную», но ведь могут существовать и другие? Мы не имеем ни малейшего понятия, возможно ли заделать дыры, если мы даже не знаем, существуют ли они. Можно тщательно защитить пароли, но существуют способы взлома, которые мы не можем даже представить себе. Работа с компьютерной безопасностью доставляет, несомненно, море удовольствия. А когда речь идёт о программировании, то мышление, ориентированное на безопасность, становится всё более важным. Нельзя перекладывать на профессионалов, занимающихся безопасностью, расчистку созданной вами путаницы.
Шифрование
Шифрование кажется мощным и неприступным, когда сотрудники правоохранительных органов стоят перед конгрессом и просят дать им официальные входы в программы, чтобы преодолеть его. Проблема состоит в том, что основная часть шифрования построена на туманном облаке неопределённости. Какие математические доказательства у нас есть для неясных допущений, вроде того, что трудно разложить на множители действительно большие числа или провести дискретное логарифмирование?
Эти проблемы, действительно, трудные? Никто публично не описал какие-то алгоритмы их решения, но это не означает, что так решения не существует. Если бы вы нашли способ подслушивать каждый разговор и проникать в любой банк, вы бы сразу сообщили об этом всему миру, что помочь заделать дыры? Или же вы промолчали бы?
Реально сложной задачей является использование шифрования в собственном коде. Даже если базовые алгоритмы, как мы полагаем, безопасные, должно быть сделано много работы по обращению с паролями, ключами и соединениями. Если будет сделана хотя бы одна ошибка и какой-то пароль останется незащищённым, то всё будет напрасно.
Управление идентификацией
Многие любят рисунок, опубликованный в газете «The New Yorker» в 1993 году, с надписью: «В интернете никто не знает, что ты собака». Он даже имеет свою собственную страницу в Википедии с четырьмя детально проработанными разделами. (В интернете мало кто знает старую шутку об анализе юмора и препарировании лягушек.)
Хорошая новость заключается в том, что анонимность может быть раскрепощающей и полезной. Плохая же в том, что мы не имеем ни малейшего понятия, как сделать что-либо, кроме анонимного общения. Некоторые программисты говорят о «двухфакторной идентификации и аутентификации», но более продвинутые устремляются к «N-факторной».
Кроме пароля и, возможно, текстового сообщения на сотовый телефон у нас совсем немного того, на что можно полажиться. Считыватели отпечатков пальцев выглядят впечатляюще, но многие, кажется, готовы рассказать и показать, как такая идентификация может быть взломана (см. здесь, здесь и здесь для начинающих).
Немногое из этого имеет значение для мира пустословия в Snapchat или Reddit, но множество взломанных страниц Facebook немного обескураживает. Нет какого-то лёгкого способа заниматься в сети серьёзными вопросами, такими как собственность, деньги, здравоохранение и в значительной степени всё остальное в жизни, кроме «светского» разговора на общие темы.
Фанаты биткоинов любят высказываться, насколько надёжным может быть блокчейн, но так или иначе биткоины продолжают воровать (см. здесь и здесь). Тут у нас нет никакого реального метода для идентификации.
Надёжность измерения
Конечно, когда речь идёт о программировании, то встаёт вопрос — есть ли хотя бы способ, с помощью которого мы можем измерить сложность проблемы? Никто, на самом деле, не знает этого. Мы знаем, что некоторые проблемы решить легко, но совсем другое дело определить, насколько трудно это сделать. NP-полнота является лишь одной частью сложной попытки кодифицировать сложность алгоритмов и анализа данных. Теория полезна, но она не может предложить никаких гарантий. Заманчиво сказать, что трудно даже знать, трудна ли проблема, но это, конечно, шутка.
Комментарии (2)
27 ноября 2016 в 20:34 (комментарий был изменён)
0↑
↓
В Swift для замыканий ввели типы аргументов — weak/unowned, внутри при доступе к вызываемому контексту требуется написать self, тем самым среда как бы говорит — «ага! self написал — утечку осознал!» (здесь я имею ввиду что если в замыкании используется self без типа аргумента weak/unowned — привет утечка)27 ноября 2016 в 20:45
+4↑
↓
Не нашёл в списке инвалидации кэша и придумывания названий для вещей. Разочарован.