Автор: Герман Иванов
Источник: компьютерная газета
Введение
Все мы уже давно привыкли к тому, что новые процессоры, предлагаемые нам производителями, неуклонно наращивают мегагерцы своей рабочей частоты, увеличивая таким образом свою вычислительную мощь. Но в наступившем 2006 году покупателю был преподнесен сюрприз. Упершись в невозможность дальнейшего роста частот, оба ведущих производителя микропроцессоров (и Intel, и AMD) отказались от привычного лозунга "Выше, дальше сильнее", переключившись на новый, звучащий как "Шире, толще, объемнее". Правильным направлением развития микропроцессорной техники теперь признано использование в персональных компьютерах процессоров, состоящих не из одного, а сразу из нескольких вычислительных ядер. Говоря другими словами, прыгать "в длину" у производителей процессоров пока не особенно-то получается, вот их коммерческие отделы и пытаются объяснить доверчивому покупателю, что, мол, прыжки в длину как вид спорта уже устарели, а новым, передовым, видом спорта являются "прыжки в ширину" ((с) matik). Давайте попробуем самостоятельно разобраться, насколько такие заявления соответствуют истине, и не окажется ли новый модный вид спорта «прыжки в ширину» хорошо известной нам всем «посадкой на шпагат», подаваемой под новым маркетинговым соусом.
Как оно вообще работает
Основным постулатом адептов мультиядерных систем является утверждение о том, что программы, запущенные на процессоре с несколькими вычислительными ядрами, работают при этом одновременно. Читатель, хотя бы однажды работавший на современном персональном компьютере, наверняка захочет им возразить, что такая параллельность выполнения легко достижима и без использования нескольких микропроцессоров. Например, когда вы набираете текст в Word, одновременно с вашей работой редактор успевает проверять на ошибки набираемый вами текст. Орфография проверяется специально оформленной частью программы, называемой на жаргоне программистов «потоком» («трейдом»). Операционная система, запустив приложение, проверяет наличие в его коде потоков и при их обнаружении запускает их все в работу параллельно основной программе, которая таким образом становится основным, родительским, потоком. Когда вы закрываете приложение, операционная система самостоятельно «прибивает» не только само приложение, но и запущенные им потоки. Дополнительный поток может и не проявляться внешне, выполняя какие-либо внутренние задачи в приложении. Так, сейчас, в момент написания этой статьи, в моем экземпляре Word запущено сразу 4 потока, причем о назначении трех из них я могу только догадываться. Так что даже если вы не видите «суслика», это вовсе не означает, что его нет. Потоки довольно широко используются в современном программном обеспечении. Помимо использования потоков, вы можете запустить одновременно с Word, например, закачку файлов из сети Интернет или просматривать телепередачи с помощью программы ТВ-тюнера. В данном случае мы имеем немного другой вид мультизадачности, так сказать, «мультизадачность в квадрате». Одновременно с основной задачей у нас трудятся еще несколько разных приложений, каждое из которых, возможно, вместе с собой запустило несколько дополнительных потоков. У вас как пользователя компьютера может сложиться впечатление, что все запущенные задачи работают одновременно, но это на самом деле совершенно не так.
Одновременность выполнения приложений в случае единственного процессора достигается с помощью следующей нехитрой методики. Операционная система поочередно «подключает» единственный микропроцессор к выполнению всех поставленных задач. Некоторую часть своего времени процессор решает задачи, поставленные перед ним потоками Word, затем операционная система мгновенно переключает его на решение задачи закачки файлов из сети Интернет, а следующим шагом процессор бросается на нужды программы ТВ-тюнера. Закончив обход всех запущенных приложений, операционная система возвращается к началу «кольца», переключая процессор снова на задачи Word, и обход приложений повторяется заново. Такое движение по кругу происходит много раз в секунду, поэтому вы как пользователь совершенно его не замечаете подобно тому, как не замечаете смены кадров при просмотре кинофильма. У вас складывается впечатление, что все задачи работают одновременно. В компьютерных системах, состоящих, скажем, из двух процессоров, физических или виртуальных, все работает примерно по той же схеме, но с одним важным исключением. Разные приложения (или разные потоки) запускаются на разных физических микропроцессорах. Операционная система на этот раз крутит уже не одно, а сразу два разных "кольца" приложений — таким образом, в любую отдельно взятую единицу времени два различных приложения (или два различных потока) у нас работают реально одновременно. На четырехпроцессорных системах «колец выполнения» будет уже четыре, и у нас одновременно будет выполняться сразу четыре приложения (потока), в восьмипроцессорной системе таких колец "реально одновременного" выполнения будет уже восемь. В свойствах процессов Windows (то самое окошко, вызываемое по Ctrl-Alt-Del) вы можете даже указать, на каком конкретном процессоре следует выполнять то или иное приложение. С помощью такого механизма вы можете вручную распределять нагрузку между несколькими процессорами в том случае, если вас не устраивает, как это делает операционная система.
Новое или хорошо забытое старое?
Компьютеры с несколькими микропроцессорами в одной системе выпускаются уже очень давно. Основное отличие "новинок" заключается в том, что раньше каждое вычислительное "ядро" таких многопроцессорных систем являлось полноценным отдельным микропроцессором, обладавшим собственным корпусом и способным работать в качестве единственного процессора в системе. В одно целое несколько разных микропроцессоров объединялись с помощью специализированных материнских плат, на которых было разведено не одно, а сразу несколько разъемов для установки процессора. Разумеется, помимо еще одного посадочного места для процессора, на плате имеется и специальная версия чипсета, умеющего работать с несколькими процессорами. Приобретая плату с разъемами для двух микропроцессоров, вы могли собрать себе на ней двухпроцессорную систему. Приобретая плату с четырьмя разъемами — четырехпроцессорную и т.д. С точки зрения своей конструкции микропроцессоры, рассчитанные на работу "в паре", немногим отличались от процессоров, рассчитанных на работу в одиночку.
Материнские платы, работающие с несколькими процессорами, стоят сравнительно дорого, что, в общем-то, вполне объяснимо. Как-никак для них требуются двойной комплект стабилизаторов питания процессора, более сложная разводка, два сокета для процессоров, в конце концов. Поддержка одновременной работы нескольких процессоров требует поддержки на уровне чипсета, который, опять-таки, нужно разработать. Тем не менее, подобные системы получили довольно широкое распространение в секторе серверов и высокопроизводительных рабочих станций, то есть узкоспециализированных компьютерных систем. Обычные пользователи домашних компьютеров относятся к ним с прохладцей. Стоят такие компьютерные системы больших денег, а вот выигрыш в производительности дают не такой уж большой. Вместо ожидаемого двух-, трех- или даже четырехкратного ускорения работы компьютера пользователь получает чаще всего не более 20-30 процентов прироста. Почему так происходит, мы с вами поговорим чуть позже, а в данный момент давайте посмотрим, что нам предлагается сейчас. А сейчас производители процессоров делают своеобразный "ход конем". Два физических микропроцессора упаковываются в один корпус, устанавливаемый в единственный разъем материнской платы.
Все контроллеры, необходимые для обеспечения совместной работы нескольких процессоров, находятся внутри него самого, поэтому никаких специальных версий чипсетов для подобных процессоров не требуется. Новые двухъядерные микропроцессоры устанавливаются в обычные "бытовые" недорогие материнские платы. Таким образом, главное новшество современной линейки мультиядерных процессоров заключается отнюдь не в новизне этой технологии, а скорее в ее стоимости для конечного потребителя. Даже сейчас, когда стоимость двухъядерного процессора держится примерно в районе цены двух равных ему по вычислительной мощности одноядерных процессоров, его покупка уже является выгодным делом. Мы оплачиваем лишь стоимость второго процессора и при этом экономим добрые $200, так как используем вместо дорогой серверной материнской платы обычную "дектопную". Со временем цена на подобные микропроцессоры наверняка должна упасть, а выгода от покупки дополнительно увеличиться.
Тут, правда, имеется несколько небольших подводных камней, которые могут испортить производителям процессоров всю идиллию. Два ядра на одном кристалле — это именно два ядра на одном кристалле. Это означает в два раза больший размер ядра и почти в два раза большее число транзисторов и, следовательно, удвоившуюся возможность получения брака при его изготовлении. Это обстоятельство может помешать производителю удешевить конечное изделие — слишком уж велики будут потери при его производстве. Впрочем, никто не запрещает производителю отключать бракованное ядро на этапе изготовления изделия. Таким образом, можно формировать из брака более дешевые одноядерные процессоры. В дальнейшем эти «половинки» можно продавать как процессоры начального уровня Celeron и Sempron. Уже сейчас в продаже попадаются микропроцессоры Athlon64 3000+, произведенные по этому алгоритму. Вторая, более серьезная, проблема — из-за наличия двух ядер на одном кристалле микропроцессора — связана с вдвое возросшим энергопотреблением такого изделия. Помимо дополнительной нагрузки на стабилизаторы материнской платы, это приводит и к повышенному нагреву двухъядерного процессора. И если с повышенной нагрузкой можно легко бороться путем улучшения технологического процесса изготовления ядер, то чрезмерный нагрев приведет к более забавному моменту. Как известно, излишний нагрев — главный враг оверклокера («разгонщика» процессоров), мешающий выжимать из процессора все соки. Как бы не вышло так, что те самые одноядерные «половинки» в силу того, что второе ядро у них отключено, станут разгоняться значительно лучше полноценных двухъядерных процессоров. Возросшая рабочая частота полезна всем приложениям, и поэтому запросто может выйти так, что одноядерные процессоры «начального уровня» в конце концов окажутся быстрее двухъядерных дорогих процессоров.
А будет ли от нескольких ядер в процессоре хоть какая-то польза?
Скорее всего, вопрос, выведенный мной в заголовок этой главы, покажется вам странным. Действительно, в теории грамотно созданная операционная система может распределять одновременно запущенные вами несколько программ по разным вычислительным ядрам, и, таким образом, каждое из приложений получит в свое распоряжение персональный микропроцессор, свободный от нагрузки остальных задач. Вполне вероятен сценарий, при котором операционная система монополизирует под свои собственные задачи одно из ядер, отдав другое ядро прикладным задачам. Благодаря такому подходу уйдет в небытие понятие "тормозной" операционной системы. Система может "медитировать" над своими внутренними потребностями столько, сколько ей самой хочется. Ее задачи никак не будут мешать прикладным приложениям, работающим на своем собственном вычислительном ядре. Компьютерные игры смогут поручить одному процессорному ядру рассчитывать физику игрового мира, второму обрабатывать звуковые эффекты. Третье ядро процессора сможет заниматься расчетом искусственного интеллекта компьютерных персонажей, а четвертое специализироваться на выводе графики на экран. Архиваторы, кодировщики MPEG и прочие программы, основательно завязанные на математических расчетах, смогут почти двукратно ускорить процесс своих вычислений, поручив разным ядрам кодировать разные части обрабатываемого файла. Казалось бы, нет никаких причин для пессимизма — в теории новая тенденция в процессоростроении выглядит очень заманчиво. Но, увы, практика вносит в нее свои не столь радужные коррективы, о которых мы сейчас с вами и поговорим.
Проблема, связанная с общими разделяемыми ресурсами
Во-первых, компьютер состоит не только из микропроцессора. В нем еще имеется оперативная память, жесткий диск, видео- и звуковые карты. Каждое из этих устройств одновременно может обслуживать запросы только одного из ядер процессора. Поэтому пока одно ядро, скажем, читает данные с жесткого диска, второе нервно курит бамбук в сторонке. Дождавшись своей очереди, оно захватывает дефицитный ресурс, и теперь уже первое ядро, обратившееся за очередной порцией информации, неторопливо приплясывает на месте, ожидая своей очереди. Дальнейшее увеличение количества ядер в процессорах лишь усугубит эту проблему. Теперь в очереди за доступом к разделяемым ресурсам будут стоять уже не два ядра, а три или четыре, и, таким образом, каждое отдельное ядро будет ждать большее количество времени. Поэтому не каждой программе мультиядерность системы может пойти на пользу — вполне возможна ситуация, при которой программа на таких системах, напротив, начнет работать несколько медленнее, хотя, честно говоря, это и маловероятно. Именно ситуацией с разделяемыми ресурсами чаще всего и объясняется тот факт, что вместо ожидаемого многократного ускорения выполнения кода программы, попав на многоядерный процессор, ускоряют свою работу максимум на треть. Дополнительно сдерживает рост быстродействия многоядерных систем пропускная способность оперативной памяти. Говоря простыми словами, память сейчас не настолько быстра, чтобы к ней без задержек могли обращаться сразу несколько процессоров.
Сложности в мультипоточном программировании
А самой главной препоной для продвижения мультиядерных процессоров в массы станут сами программисты. Да-да! Вы не ослышались: именно те самые ребята, которые пишут нам всем операционные системы и прикладные программы. Мультипоточное программирование — это настоящий ад для программистов. Подавляющее их большинство привыкло к тому, что создаваемый ими код неразрывен. То есть, если я написал инструкцию, складывающую между собой числа «А» и «B», а затем записывающую их сумму обратно в «A», то я уверен в том, что переменная «A» за время, пока происходит сложение, ничуть не изменилась. При мультипоточном программировании это совершенно не факт! За то время, пока один из потоков складывает «A» и «B», другой поток моего же собственного приложения может запросто записать туда какое-либо нужное ему самому значение. Если на одноядерных процессорах такое маловероятно (хотя и возможно), и процессор не сможет прерваться на середине выполняемой инструкции, то в случае потоков, работающих на двух разных процессорах, такое вполне в норме вещей, и, занося в переменную «A» результат сложения, первый поток уничтожит данные, сохраненные вторым потоком. У нас получается нечто вроде фазового нарушения согласованности одновременной работы двух разных фрагментов программ. Отловить такие ошибки в ходе отладки практически невозможно. Опытные программисты, скорее всего, поморщились, прочитав эти мои слова. Мол, вот Герман чайник — не знает о командах, специально предназначенных для "разруливания" подобных ситуаций. Перед началом работы с числом "A" выставляешь специальный флажок, означающий: "Переменную не трогать!" (lock), и остальные потоки не смогут ее изменить — всего-то и делов! Делаешь свои дела, а потом сбрасываешь флажок блокирования переменной. А знаете ли вы, что именно при этом происходит? Остальные потоки начнут топтаться перед этой переменной, исполняя тот самый танец на месте, о котором я рассказывал в примере про разные ядра процессоров и жесткий диск. Таким образом, широко используя подобные приемы в своем коде, вы на корню убиваете все достоинства использования нескольких процессорных ядер, заставляя их работать в некотором «виртуальном» одноядерном режиме. Так зачем вообще городить весь сыр-бор с потоками, когда намного проще воспользоваться обычным программным таймером? Особенно если учесть, что пользователь вашей программы, скорее всего, и не заметит никакой разницы. Дабы окончательно откреститься от обвинений в незнании предмета, рекомендую ознакомиться с документаций MSDN, выпускаемой фирмой Microsoft для программистов на ее платформе. Практически любой класс в библиотеке NET содержит следующую пометку: “Потокобезопасность. Любые члены этого типа с модификаторами public static (Shared в Visual Basic) могут безопасно работать в многопоточных операциях. Потокобезопасность членов экземпляров не гарантируется”. Заметьте: это мнение тех «ребят», что пишут нам операционные системы!
Но и это еще не все! При мультипоточном программировании следует очень внимательно следить за тем, какой именно поток и как именно использует ту или иную переменную. Вполне можно невольно запрограммировать такую ситуацию, при которой первый поток ждет доступа к переменной "A", чтобы впоследствии изменить переменную "B", а в это самое время второй поток ждет, пока изменится переменная "B", для того чтобы освободить удерживаемую им переменную "A". Говоря простыми словами, оба потока будут вечно ждать друг друга, и это ожидание никогда не прервется, так как оба они просто стоят на месте. Эта ситуация настолько распространена, что получила собственное название — DEADLOCK — и стала притчей во языцех среди программистов, пишущих мультипоточный код. В качестве финального анекдота расскажу вам о задаче, которую мне пришлось решать во время одного из конкурсов на сайте gotdotnet.ru. Там давалось задание написать широко известную игрушку Pacman. Вы наверняка встречали одну из ее реализаций. Ну, типа управляемый вами шарик бегает по лабиринту между стенками. За шариком бегают три управляемых компьютером "танчика". На игровом поле разложена капуста. Ее собирают и танки, и шарик. Если при этом компьютерный танк совпал с вашим шариком — вас "съели". Если вы «съели» капусту, вам добавляются очки, а на поле появляется новая капуста. В качестве условия задачи ставилось то, что и "шарик", и "танки", и даже само игровое поле должно было быть реализовано в отдельных программных потоках. Призрак танка, "полудогнавшего" наполовину убежавший шарик, мне снился не одну ночь. Выглядело это так: шарик начинал уходить с клетки, и тут его поток прерывался потоком обработки танка, начинавшего, в свою очередь, на эту же самую клетку заходить. Так как шарик уже успевал отрапортовать о факте покидания клетки игрового поля, танк его не видел. Но чисто графически шарик отрисовать свое перемещение не успевал. В результате довольно забавно смотрелись танк и шарик, мирно двигающиеся бок о бок в одной клетке, из-за того, что никто из них не успевал осведомиться о наличии соседа. Иногда (именно "иногда"!) кто-то один из них успевал-таки почуять врага рядом с собой. Если это был танк, то все было хорошо — игра, как и положено, завершалась. Но другое дело, если почуявшим оказывался шарик! Дело принимало забавный оборот: в силу законов объектноориентированного программирования шарик ничего не знал о правилах игры, в которую он играет — сама игра — это отдельный класс (и поток) — игровое поле. Шарик подобно герою известного кинофильма "просто бежал", поэтому, будучи пойманным, он делал только то, на что запрограммирован, а именно исчезал с игрового поля, и танки оставались на нем в гордом одиночестве.
Сложности программирования потоков в той или иной степени присущи любому мультипоточному приложению, поэтому опытные программисты шарахаются от него как черт от ладана. Если я как программист и сяду за создание мультипоточного приложения, клиенту оно обойдется куда дороже обычного однопоточного. А так как работать мультипоточный вариант в самом лучшем случае будет всего лишь на треть быстрее однопоточного (из-за разделяемых ресурсов или общих переменных), то клиенту тратить на него деньги особого смысла нет. Вот он их и не тратит, и этим объясняется фактический провал широко разрекламированной в свое время технологии "гипертрейдинг" процессоров Intel. По моему личному мнению, такая же судьба ожидает и мультиядерные процессоры. Безусловно, они найдут свою нишу на серверах в Интернет или компьютерах, на которых крутятся базы данных, но обычному домашнему пользователю особого выигрыша не принесут. Никто просто не станет за бесплатно оптимизировать под множество ядер огромный объем уже вращающегося среди пользователей программного обеспечения. Заметьте: я вам рассказал историю создания мультипоточного приложения, работавшего на единственном процессоре. Если же программа будет запущена на нескольких физических ядрах, проблемы вырастут многократно. Совсем недавно была поймана ошибка, проявлявшаяся во множестве популярных игр. Все они использовали специальный таймер, имеющийся в микропроцессоре для расчета времени. В двухъядерных процессорах таких таймеров в системе оказалось не один, а два, и никто не гарантирует, что их значение будет совпадать. В результате, нажав на газ в игрушке Need for speed в самом начале длинной улицы, вы через секунду врезались в стенку в ее конце. Происходило так из-за того, что разные потоки игры использовали таймер разных ядер процессора. К слову, против этой ошибки уже патч вроде есть.
"Вам нужно всего лишь перекомпилировать программу" ((с) Intel)
Этот слоган фирмы Intel был придуман еще во времена попыток широкого введения технологии "гипертрейдинг" в процессоры Pentium IV. Нынче он успешно перекочевал и в рекламные материалы, посвященные новым многоядерным процессорам. Суть высказываемой идеи сводится к тому, что программисту ничего не нужно исправлять в своей программе — «умный» компилятор фирмы Intel все сделает за него сам. Программисту нужно только приобрести у Intel этот недешевый программный продукт, нажать две кнопки, и его программа волшебным образом станет мультипоточной. Данный слоган эксплуатируют веру обывателя в компьютер как некое разумное существо. Причем существо, разбирающееся в программе лучше самого программиста. На самом же деле компьютер, по меткому выражению одного из авторов самого понятия цифровых вычислительных машин, не более чем «полный идиот, обладающий феноменальной способностью к счету». В мультипоточном приложении использование нескольких потоков изначально закладывается в сам алгоритм. Однопоточные программы пишутся совершенно по другому алгоритму. Поэтому обещание того, что компилятор самостоятельно переделает однопоточное приложение в мультипоточное, по достоверности сравнимо с обещанием, что компилятор сам сядет и без программиста напишет нужную вам программу.
Вывод
Безусловно, будущее за многоядерными процессорами, но будущее это еще очень далеко. Вероятнее всего, тема нескольких ядер — это всего лишь способ пережить смутное время невозможности дальнейшего экстенсивного развития микропроцессоров. Как только производители процессоров найдут способ поднять частоту единственного ядра, мультиядерность отойдет на второй план. Подобно 64-битности, наделавшей шумихи в прошлом году, мультиядерность незаметно проберется-таки в микропроцессоры наших компьютеров как функция, в принципе, не особенно-то и нужная, но особо и не мешающая. Но выделять на ее введение сколь-либо заметные деньги из своего домашнего бюджета я лично не собираюсь. Для себя вы можете определить необходимость покупки многоядерного процессора очень просто. Если вы сейчас уже используете несколько процессоров в системе — новые процессоры вам нужны. Если вы часто кодируете видеофайлы или же сами пишете себе сложные вычислительные алгоритмы — они тоже придутся вам впору. Если же вы обычный домашний пользователь, впервые узнавший о мультипроцессорных системах из этой моей статьи, не торопитесь. Замену имеющегося у вас одноядерного микропроцессора на двухъядерный я рекомендую вам проводить в плановом порядке — тогда, когда ваш имеющийся процессор существенно устареет. Сейчас тратить деньги на новые процессоры вам не стоит — особого выигрыша в скорости вы не получите. Если, конечно, вы не привыкли даже в булочную на такси ездить.