Я разрабатываю интерпретируемый AST язык сценариев на C ++. Интерпретатор имеет простой сборщик мусора «останови мир», который при каждом запуске отправляет запрос на остановку всем потокам приложения, а затем ожидает их остановки. Каждый поток имеет только одну безопасную точку, где он может соблюдать дсЗапрос, размещенный в методе exec()
он вызывается каждый раз, когда выполняется строка интерпретируемого кода, например:
void Thread::exec(const Statement *stmt){
if(runtime->gcPauseRequested){
this->paused = true;
gcCallback.notify_one(); //notify GC that this thread is now waiting
gcConditionVariable.wait(gcLock); //wait for GC to be finished
this->paused = false;
}
// execute statement...
}
и сборщик мусора:
void MemoryManager::gc(){
runtime->gcPauseRequested = true;
while(!allThreadsArePaused()){
gcCallback.wait(gcCallbackLock);
}
runtime->gcPauseRequested = false;
//garbage collect and resume threads...
}
Вот проблема: язык поддерживает вызовы собственных функций, но с текущей системой, если поток выполняет собственный вызов, который занимает много времени (например, собственный sleep
функция), все остальные темы приложения а также поток сборщика мусора будет ожидать, пока этот поток не доберется до безопасной точки, чтобы можно было выполнить сборку мусора.
Есть ли способ избежать этого?
Есть ли способ избежать этого?
Не с вашим текущим дизайном, а с явно непрозрачными свойствами (не видно / не трогать внутри) вашего «нативного» кода.
Ваш дизайн прост: каждый поток должен иногда находиться в «безопасном» месте, где он не выделяет объекты, которые могут быть известны вашему языку, и не содержит указателей на такие объекты в местах, которые не видны GC. Вы гарантируете это, настаивая на протоколе потока, который заставляет каждый поток периодически проверять, нужен ли GC, в месте, которое вы разработали так, чтобы быть безопасным для этого потока.
Вызываемые вами собственные функции просто не следуют вашему протоколу. Они могут делать две плохие вещи: а) размещать объекты интерпретируемого языка и б) удерживать указатели на такие объекты в непрозрачном состоянии (регистры, переменные в кадрах стека, не видимые GC, переменные в объектах, размещенных вне того, что выделяет ваш менеджер памяти …) нативной функции.
Учитывая, что эти действия нарушают протокол, вы, вероятно, не сможете это исправить, если оставите распределитель и собственный код в покое.
Таким образом, вы должны либо изменить свой протокол на что-то другое [и все же найти решение], либо изменить то, что делают распределитель и собственный код.
Вы можете решить а), настаивая на том, чтобы ГХ и распределитель памяти совместно использовали блокировку, чтобы в каждый момент времени была активна только одна. Это предотвратит ваш нативный код
от распределения во время работы GC. Это может добавить дополнительную нагрузку на ваш распределитель памяти; возможно, нет, поскольку он, вероятно, должен иметь защиту от нескольких потоков, выполняющих интерпретированный код, и от всех попыток выделить объекты одновременно. Даже если у вас есть локальный распределитель потоков, в какой-то момент этот локальный распределитель должен исчерпать пространство и попытаться получить больше из пула, совместно используемого всеми потоками, например, тем, который предоставляет ОС.
Вы можете решить б), настаивая на том, что нативный код иногда сохраняет все указатели, которые он держит в своем непрозрачном состоянии, обратно в общедоступное место, где их может видеть GC, и делает паузу, как это делают потоки интерпретатора.
И более сложный способ настаивать на безопасности указателя в нативных потоках состоит в том, чтобы создать карту памяти (лучше всего делать в автономном режиме) их содержимого, которая маркирует каждую машинную инструкцию (или строку кэша, содержащую код) логическим значением: «безопасно для GC здесь» или «не безопасно для GC здесь». Затем GC останавливает каждый поток, спрашивает, работает ли он в собственном коде, если это так, выбирает ПК и проверяет соответствующий логический флаг. Если безопасно, переходите к GC. Если нет, перейдите к следующей инструкции и проверьте исправленный компьютер. Да, это довольно хитрая логика. А то, как вы понимаете, какие инструкции являются «безопасными» и «небезопасными», является дополнительной (довольно большой) проблемой; если есть части нативного кода, ответ на которые вы не знаете, вы всегда можете пойти консервативно и отметить «здесь небезопасно для GC». Вы по-прежнему рассчитываете на то, что нативный код не войдет в какой-то цикл, в котором нет «безопасных» точек, или, по крайней мере, не будете делать это очень часто.
Если вы выберете этот второй подход, вы также можете использовать его в своем переводчике. Это позволило бы избежать дополнительных издержек каждого потока интерпретатора, опрашивающего флаг GC после каждого оператора. Настроив интерпретатор на скорость (вы обнаружите, что хотите сделать это, как только запустите его), вы обнаружите, что опросы становятся все большей частью накладных расходов времени выполнения.
Других решений пока нет …