В параллельных вычислениях кража работы — это стратегия планирования для многопоточных компьютерных программ. Она решает проблему выполнения динамически многопоточного вычисления, которое может «порождать» новые потоки выполнения, на статически многопоточном компьютере с фиксированным числом процессоров (или ядер ). Она делает это эффективно с точки зрения времени выполнения, использования памяти и межпроцессорного взаимодействия.
В планировщике захвата работы каждый процессор в компьютерной системе имеет очередь рабочих элементов (вычислительных задач, потоков) для выполнения. Каждый рабочий элемент состоит из серии инструкций, которые должны быть выполнены последовательно, но в ходе своего выполнения рабочий элемент может также порождать новые рабочие элементы, которые могут быть выполнены параллельно с его другой работой. Эти новые элементы изначально помещаются в очередь процессора, выполняющего рабочий элемент. Когда процессору не хватает работы, он просматривает очереди других процессоров и «крадет» их рабочие элементы. По сути, захват работы распределяет работу по планированию по простаивающим процессорам, и пока все процессоры имеют работу, никаких накладных расходов на планирование не происходит. [1]
Кража работы контрастирует с разделением работы , другим популярным подходом к планированию для динамической многопоточности, где каждый рабочий элемент планируется на процессоре, когда он порождается. По сравнению с этим подходом, кража работы уменьшает объем миграции процесса между процессорами, поскольку такая миграция не происходит, когда все процессоры должны что-то сделать. [2]
Идея перехвата работы восходит к реализации языка программирования Multilisp и работе над параллельными функциональными языками программирования в 1980-х годах. [2] Она используется в планировщике для языка программирования Cilk , [3] фреймворке fork/join Java , [4] библиотеке параллельных задач .NET , [5] и среде выполнения Rust Tokio . [6] [7]
Work stealing разработан для «строгой» модели fork–join параллельных вычислений, что означает, что вычисление можно рассматривать как направленный ациклический граф с одним источником (начало вычисления) и одним стоком (конец вычисления). Каждый узел в этом графе представляет либо вилку , либо соединение . Вилки производят несколько логически параллельных вычислений, называемых по-разному «нитями» [2] или «нитями». [8] Ребра представляют последовательные вычисления. [9] [примечание 1]
В качестве примера рассмотрим следующую тривиальную программу fork-join в синтаксисе, подобном Cilk :
функция f(a, b): c ← вилка g(a) г ← ч(б) присоединиться вернуть с + dфункция g(a): вернуть × 2функция h(a): б ← вилка г(а) с ← а + 1 присоединиться вернуть б + с
Вызов функции f(1, 2) приводит к следующему графу вычислений:
В графе, когда два ребра покидают узел, вычисления, представленные метками ребер, логически параллельны: они могут выполняться либо параллельно, либо последовательно. Вычисление может продолжаться только после узла соединения , когда вычисления, представленные его входящими ребрами, завершены. Работа планировщика теперь заключается в назначении вычислений (ребер) процессорам таким образом, чтобы все вычисления выполнялись до завершения в правильном порядке (ограниченном узлами соединения), желательно как можно быстрее.
Рандомизированная версия алгоритма кражи работы, представленная Блюмофом и Лейзерсоном, поддерживает несколько потоков выполнения и распределяет их по процессорам . Каждый из процессоров имеет двухстороннюю очередь (deque) потоков. Назовем концы deque «top» и «bottom».
Каждый процессор, имеющий текущий поток для выполнения, выполняет инструкции в потоке одну за другой, пока не встретит инструкцию, которая вызывает одно из четырех «особых» поведений: [2] : 10
Первоначально вычисление состоит из одного потока и назначается некоторому процессору, в то время как другие процессоры начинают простаивать. Любой процессор, который становится простаивающим, запускает фактический процесс кражи работы, что означает следующее:
Обратите внимание, что в правиле для spawn Блюмоф и Лейзерсон предлагают, чтобы «родительский» поток выполнял свой новый поток, как будто выполняя вызов функции (в C-подобной программе f(x); g(y); вызов функции f завершается до того, как выполняется вызов g ). Это называется «похищением продолжения», потому что продолжение функции может быть похищаемо во время выполнения порожденного потока, и является алгоритмом планирования, используемым в Cilk Plus . [8] Это не единственный способ реализовать похитение работы; альтернативная стратегия называется «похищением потомка» и ее проще реализовать в виде библиотеки , без поддержки компилятора . [8] Похищение потомка используется Threading Building Blocks , Microsoft Task Parallel Library и OpenMP , хотя последний дает программисту контроль над используемой стратегией. [8]
Было предложено несколько вариантов кражи работы. Рандомизированный вариант, предложенный Блюмофе и Лейзерсоном, выполняет параллельное вычисление за ожидаемое время на процессорах; здесь — работа , или количество времени, необходимое для выполнения вычисления на последовательном компьютере, а — промежуток , количество времени, необходимое на бесконечно параллельной машине. [примечание 2] Это означает, что в ожидании требуемое время — это максимум постоянный множитель, умноженный на теоретический минимум. [2] Однако время выполнения (в частности, количество выполненных краж) может быть экспоненциальным в худшем случае. [10] Локализованный вариант, в котором процессор пытается украсть свою собственную работу, когда она свободна, также был проанализирован теоретически и практически. [11] [12]
Вычисление, запланированное версией перехвата работы Блюмофе–Лейзерсона, использует стековое пространство , если бы это было использование стека тем же вычислением на одном процессоре, [2] что соответствует собственному более раннему определению эффективности пространства авторами. [13] Эта граница требует продолжения перехвата; в планировщике перехвата потомков она не выполняется, как можно увидеть из следующего примера: [8]
для i = 0 до n: fork f(i) join
В реализации с кражей потомков все «разветвленные» вызовы f помещаются в очередь работ, которая таким образом увеличивается до размера n , который может быть сделан произвольно большим.
Алгоритм кражи работы, описанный ранее, и его анализ предполагают вычислительную среду, в которой вычисление запланировано на наборе выделенных процессоров. В среде многопрограммирования (многозадачности) алгоритм должен быть изменен, чтобы вместо этого запланировать вычислительные задачи на пул рабочих потоков , которые, в свою очередь, планируются на фактические процессоры планировщиком операционной системы . В любой момент времени планировщик ОС назначит процессу кражи работы некоторое число P A ≤ P из P процессоров в компьютере, поскольку другие процессы могут использовать оставшиеся процессоры. В этой настройке кража работы с пулом из P рабочих потоков имеет проблему, заключающуюся в том, что рабочие, действующие как воры, могут вызвать лайвлок : они могут блокировать выполнение рабочих, которые на самом деле порождают полезные задачи. [14] [15]
Для этой ситуации был разработан вариант перехвата работы, который выполняет вычисления за ожидаемое время.
где P avg — среднее количество процессоров, выделенных для вычисления планировщиком ОС за время выполнения вычисления. [16] Многопрограммный планировщик работ отличается от традиционной версии в двух отношениях:
Попытки улучшить многопрограммный кражу работы были сосредоточены на проблемах локальности кэша [12] и улучшенных структурах данных очереди. [17]
Несколько алгоритмов планирования для динамически многопоточных вычислений конкурируют с кражей работы. Помимо традиционного подхода разделения работы, существует планировщик, называемый параллельным глубинным (PDF), который улучшает границы пространства кражи работы, [18] а также обеспечивает лучшую производительность в некоторых ситуациях, когда ядра чипа мультипроцессора совместно используют кэш. [1]