В релизе GoOS 0.2.8 была внедрена поддержка нативных потоков пользовательского пространства, а затем поверх них создан собственный M:N runtime для горутин. Это усложнило работу сборщика мусора, так как теперь при освобождении объекта необходимо убедиться, что ссылка на него не осталась в стеке другого потока, в сохранённом контексте горутины, в XMM-регистре или в промежуточном состоянии переключения контекста. Задача теперь пересекает границы планировщика, аллокатора, ABI, TLS, виртуальной памяти и кода сохранения регистров. В отличие от однопоточной версии, где GC работал с одним стеком и набором регистров, появление второго потока позволило ему одновременно менять кучу на другом процессорном ядре, а горутины полностью отделили выполнение кода от стека нативного потока.
Ранее нативный процесс GoOS выполнялся одной задачей ядра, но новый ABI позволил создавать потоки внутри процесса. Процесс имеет 16 слотов для нативных потоков, включая основной в слоте 0. Для дополнительных потоков ядро создаёт стек размером 512 КиБ с защитной зоной 64 КиБ под ним. Если стек вырастет за границу, процесс получит fault. Идентификатор потока включает номер слота и поколение, чтобы старый ID не указывал на новый поток после перезаписи слота. Ошибка или аварийное завершение одного потока приводит к завершению всего процесса, тогда как ThreadExit предназначен для нормального завершения отдельного потока. Для определения текущего потока вместо вычисления по указателю стека (RSP), который стал ненадёжным после внедрения M:N планировщика, используется фиксированная база стека (FS), указывающая на блок управления потоком.
Для оптимизации работы с кучей при наличии нескольких потоков была введена система локальных slab-страниц. Каждый поток может владеть одной slab-страницей каждого из 14 классов размеров, что позволяет избежать глобальной блокировки при мелких выделениях. Крупные выделения остаются под общей блокировкой. Для синхронизации освобождений объектов, выделенных в чужих slab-страницах, используется вторая битовая карта mark в метаданных страницы, которая не используется сборщиком мусора между циклами. Если поток освобождает объект в чужой странице, он атомарно выставляет бит в этой карте. Перед сборкой мусора эти биты переносятся в основную карту занятости, что предотвращает двойное освобождение. В худшем случае один поток может удерживать до 14 лишних страниц (56 КиБ), а для восьми потоков верхняя оценка локального кэша составляет 448 КиБ.
Сборка мусора в многопоточном режиме требует остановки всех потоков (stop-the-world). Ядро удерживает задачи процесса, посылает запрос на перепланирование и ждёт сохранения пользовательского контекста каждого потока, включая RIP, RSP, 15 регистров общего назначения и 16 XMM-регистров. В корни GC были включены XMM-регистры, так как асинхронная остановка потока может произойти между любыми инструкциями, и компилятор может держать там живые Go-указатели. Если поток остановлен посередине изменения метаданных аллокатора, GC видит несогласованное состояние и отменяет цикл, не пытаясь «додумать» ситуацию. После успешной остановки runtime забирает у потоков принадлежащие им slab-страницы, сворачивает локальные счётчики в общие и применяет отложенные освобождения перед началом маркировки. Количество попыток остановки потоков ограничено четырьмя, после чего цикл GC прекращается.
Нативные потоки обеспечили параллелизм на нескольких ядрах, но для реализации привычной модели Go с горутинами, каналами и таймерами создан отдельный target с поддержкой горутин. GoOS использует закреплённую версию TinyGo 0.42.0, но очереди, нативные потоки, ожидания и связь с GC реализованы в runtime GoOS. В этой модели G — это горутина, M — нативный поток GoOS, способный выполнять горутины, а полноценного объекта P нет, вместо него используется разрешение на выполнение. Количество таких разрешений ограничивает реальный параллелизм и управляется через GOMAXPROCS. Предел выбирается по наименьшему из трёх ограничений: число доступных CPU, квота задач процесса и количество слотов нативных потоков (16). Основной поток процесса также участвует в этом механизме.