Реализовать «воровство работы» не сложно в теории. Вам нужен набор очередей, содержащих задачи, которые работают, выполняя комбинацию вычислений и генерируя другие задачи, чтобы выполнять больше работы. И вам нужен атомарный доступ к очередям, чтобы помещать вновь созданные задачи в эти очереди. Наконец, вам нужна процедура, которую каждая задача вызывает в конце, чтобы найти больше работы для потока, который выполнил задачу; эта процедура должна искать в рабочих очередях, чтобы найти работу.
В большинстве таких систем кражи работы предполагается, что имеется небольшое количество потоков (обычно резервируемых реальными ядрами процессора) и что для каждого потока существует ровно одна очередь работ. Затем вы сначала пытаетесь украсть работу из собственной очереди, а если она пуста, попробуйте украсть у других. Хитроумно знать, какие очереди искать; Сканирование их поочередно для работы довольно дорого и может вызвать колоссальные разногласия между потоками в поисках работы.
Пока что все это довольно общие вещи с одним, кроме двух основных исключений: 1) переключение контекстов (например, установка регистров контекста процессора, таких как «стек») не может быть задано в чистом C или C ++. Вы можете решить эту проблему, согласившись записать часть своего пакета в машинный код конкретной целевой платформы. 2) Атомарный доступ к очередям для мультипроцессора не может быть сделан исключительно в C или C ++ (без учета алгоритма Деккера), поэтому вам нужно будет кодировать те, которые используют примитивы синхронизации на ассемблере, такие как X86 LOCK XCH или Compare and Swap. Теперь код, используемый для обновления очереди после получения безопасного доступа, не очень сложен, и вы можете легко написать это в несколько строк на языке C.
Однако, я думаю, вы обнаружите, что попытка кодировать такой пакет на C и C ++ с помощью смешанного ассемблера все еще довольно неэффективна, и в конечном итоге вы все равно будете в конечном итоге кодировать все это на ассемблере. Все, что осталось, это C / C ++ совместимые точки входа: -}
Я сделал это для нашего PARLANSE языка параллельного программирования, который предлагает идею сколь угодно большого числа параллельных вычислений, живущих и взаимодействующих (синхронизирующих) в любой момент. Он реализован за кулисами на X86 точно с одним потоком на процессор, а реализация полностью на ассемблере. Код для кражи работы, вероятно, содержит в общей сложности 1000 строк, и его сложный код, потому что вы хотите, чтобы он был очень быстрым в неконфликтном случае.
Реальная ложка дегтя для C и C ++ заключается в том, сколько места в стеке вы назначаете при создании задачи, представляющей работу? Программы на последовательном C / C ++ избегают этого вопроса, просто перераспределяя огромные объемы (например, 10 МБ) одного линейного стека, и никто не заботится о том, сколько этого пространства стека потрачено впустую. Но если вы можете создавать тысячи задач и выполнять их все в определенный момент времени, вы не сможете разумно выделить 10 Мбайт для каждой из них. Так что теперь вам нужно либо статически определить, сколько стекового пространства понадобится задаче (сложный по Тьюрингу), либо вам нужно выделить куски стека (например, на вызов функции), чего не делают широко доступные компиляторы C / C ++ (например, тот, который вы, вероятно, используете). Последний выход - ограничить создание задач, чтобы в любой момент ограничить их несколькими сотнями, и объединить несколько сотен действительно огромных стеков среди текущих задач. Вы не можете сделать последнее, если задачи могут заблокировать / приостановить состояние, потому что вы достигнете своего порога. Таким образом, вы можете сделать это, только если задачи only выполняют вычисления. Это выглядит как довольно серьезное ограничение.
Для PARLANSE мы создали компилятор, который распределяет записи активации в куче для каждого вызова функции.