184
1
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Многопоточность. Снизу вверх. Процессор

Время чтения 7 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
184
1
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Всем привет! Меня зовут Дмитрий Бахтенков, и я .NET-разработчик. Добро пожаловать в цикл статей «Многопоточность. Снизу вверх». Этот цикл я посвящаю работе потоков на разных уровнях абстракции: от самого низкого — ядра и потоков в процессоре — до самого высокого на примере высокоуровневого языка программирования.

Эта статья — про процессоры. Я расскажу, как процессоры выполняют различные инструкции параллельно с помощью технологий многопоточности и многоядерности.

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

Многопоточность. Снизу вверх. Процессор

Поток

А что же такое «поток» в целом? Поток — это набор каких-то инструкций, которые идут друг за другом и должны выполняться последовательно. Термин можно немного расширить, сказав «поток исполнения инструкций».

На примере

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

Теперь представьте, что нужно решить несколько задачек и делаете вы это со своим другом. Вы берете первую задачу, а он — вторую. И одновременно решаете свои задачки, выполняете свои наборы инструкций и получаете результат. В момент вычислений вы — один поток исполнения инструкций, а ваш друг — второй поток исполнения инструкций.

А теперь к делу!

Аппаратная часть

Внутри процессора

Процессор — это «мозги» компьютера. Тут происходят все сложные вычисления и выполнение различных команд. Для простоты определим, что процессор «складывает байтики», а то, если погружаться в тему инструкций, можно немного сойти с ума.

Процессор состоит из множества функциональных частей, но мы остановимся на тех, что непосредственно относятся к многопоточности, — на ядрах.

Внутри каждого ядра процессора есть следующие элементы:

  • Исполнительные блоки — это части процессора, которые относятся к непосредственному исполнению инструкций. Например, блок ALU нужен для выполнения арифметических операций с целыми числами, а FPU — для чисел с плавающей запятой.
  • Управляющие блоки — это части процессора, которые получают инструкции, декодируют их и выбирают, когда и на каком исполнительном блоке они будут выполнены. Пример управляющего блока — планировщик задач, который как раз определяет, когда и где выполнить инструкцию.

Упрощенно процессор выглядит примерно так:

С составом разобрались, теперь переходим к делу.

Многопоточность в процессоре

Разберем, как работает многопоточность на уровне процессора. А для этого провалимся в ядро.

Итак, напоминаю, что поток — это просто последовательность инструкций. Для наглядности возьму три базовых вида инструкций, которые могут быть в потоке:

  • Арифметическая операция без плавающей запятой (выполняется на ALU).
  • Арифметическая операция с плавающей запятой (выполняется на FPU).
  • Ожидание данных из памяти.

У нас есть два потока с инструкциями, и мы хотим, чтобы эти инструкции выполнялись параллельно. Но ядро может выполнять один тип операции в один момент времени! Так как тогда происходит выполнение команд?

Допустим, у нас есть ядро с поддержкой двух аппаратных потоков, и на него назначено два потока, A и B.

Поток A содержит:

    • Инструкция 1: загрузить данные из памяти.
    • Инструкция 2: выполнить арифметическую операцию с плавающей запятой.

Поток B содержит:

    • Инструкция 1: выполнить сложение в ALU.
    • Инструкция 2: переместить данные в память.

Планировщик «думает» примерно так:

  1. Пока поток А загружает данные из памяти, я могу сделать что-то из потока В. Выполню сложение, если ALU свободен.
  2. Задача потока B закончена, и теперь он работает с памятью. А поток А как раз прочитал данные и ждет выполнения инструкции. Сделаю ее на FPU.

С учетом многоядерности процессоров на самом деле это выглядит примерно так:

Инструкции из разных потоков чередуются между собой при выполнении, из-за чего создается ощущение параллельности их выполнения. Еще на разных исполнительных блоках могут выполняться разные инструкции — например, пока ALU занят, можем выполнить инструкцию для FPU из другого потока.

Проблема общих ресурсов

В процессорах существует кеш. Он создан для быстрого доступа к данным, чтобы не было необходимости постоянно «ходить» в оперативную память. У него обычно несколько уровней:

  • L1 (Primary Cache) — самый маленький кеш, локальный для каждого ядра. Доступ к данным оттуда очень быстрый, но сам кеш очень маленький.
  • L2 — этот кеш немного больше и немного медленнее, чем L1. Он может быть общим для группы ядер в многоядерных процессорах.
  • L3 — общий кеш. Он используется для передачи информации между всеми ядрами и хранения часто запрашиваемых данных.

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

Детализируем наш процессор:

А вот так выглядит иерархия памяти:

Там, где есть общие ресурсы, возникают конфликты доступа к этим ресурсам. Они могут меняться, устаревать, а из-за этого появляются проблемы с конечным результатом операций. И тут на помощь приходит когерентность (согласованность) кеша — Cache Coherence.

Отвлечемся на аналогии. 

Представьте, что вы сделали шаблон какого-то документа в MS Word и несколько человек его распечатали. И тут выяснилось, что шаблон необходимо поменять. Вы исправили свой документ, но людей не предупредили — и они некорректно его заполнили.

Ваш документ — это L3-кеш, а люди — это ядра процессора, которые эти данные сохранили в кеши L2 или L1 для использования. Если при модификации данных не сообщить, что они изменились, возникнут проблемы.

Итак, когерентность — это свойство кеша, которое означает целостность данных для копий общего ресурса. Кеш называется когерентным, если выполняются следующие условия:

  • Информирование о записи: изменение данных в любом из кешей должно быть распространено на другие копии этих данных (этой линии кеша) в соседних кешах.
  • Видимость чтения: операции чтения и записи в одну и ту же ячейку памяти должны быть видимы для всех процессоров в одинаковом порядке.

Для соблюдения этих свойств существуют протоколы когерентности, например MESI.

Этот протокол добавляет некоторые статусы для данных в памяти:

  • Modified (M) — данные были получены из основной памяти (например, из оперативной) и изменены. Эти данные нельзя изменять, пока они не синхронизируются с основной памятью. После синхронизации с основной памятью состояние данных будет Shared.
  • Exclusive (E) — данные находятся только в текущем кеше, и они совпадают с основной памятью. Если ядро прочитает данные из этого кеша, они получат статус Shared. Если запишет, статус будет Modified.
  • Shared (S) — данные совпадают с основной памятью, а также хранятся в других кешах. Если в одном из кешей данные изменятся, статус изменится на Modified.
  • Invalid (I) — данные устарели и не используются. Когда данные помечаются статусом M или E, их копии помечаются как Invalid.

Как только данные появляются в одном кеше, им назначается статус E:

Ядро забирает эти данные и помещает их в локальный кеш, статус теперь — Shared:

Раз данные в одном кеше изменились, его копии стали неактуальны. Таким образом, в основном кеше у нас статус Modified, а в локальном — Invalid:

Когда ядро получит новые данные, они снова станут Shared:

С помощью такой модели статусов процессор понимает, можно или нельзя записывать данные в кеши, а также являются ли они устаревшими. Именно поэтому операции записи/чтения меняют состояния данных, которые видимы для всех других потоков со своими кешами.

Заключение

Это статья — введение в многопоточность на аппаратном уровне. В первую очередь она предназначена для разработчиков, которые редко работают с потоками в процессоре напрямую и хотят немного расширить кругозор. А для тех, кто хочет углубиться в аппаратную часть, я рекомендую почитать про SMT и их различия у AMD и Intel — кажется, это будет интересно 🙂 

Спасибо за прочтение статьи! Также я веду телеграм-канал Flexible Coding, где рассказываю о своем опыте в программировании!

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники