2

我正在用 C++ 开发一种 AST 解释的脚本语言。解释器有一个简单的 stop-the-world 标记和清除垃圾收集器,每当触发收集时,它会向所有应用程序线程发送停止请求,然后等待所有线程暂停。每个线程只有一个可以接受gc请求的安全点,放置在一个方法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函数),所有其他应用程序线程垃圾收集器线程将等待该线程到达安全点,以便可以执行垃圾收集。有没有办法避免这种情况?

4

1 回答 1

2

有没有办法避免这种情况?

不适用于您当前的设计,以及“本机”代码的明显不透明属性(看不到/触摸内部)。

你的设计很简单:每个线程必须偶尔在一个“安全”的地方,它不会分配你的语言可以知道的对象,并且它不会在你看不到的地方保存指向这些对象的指针GC。您通过坚持强制每个线程定期检查是否需要 GC 的线程协议来确保在您设计的地方对该线程是安全的。

您被调用的本机函数根本不遵循您的协议。它们会做两件坏事:a) 分配解释语言对象,b) 持有指向处于不透明状态的此类对象的指针(寄存器、GC 看不到的堆栈帧中的变量、分配在内存管理器分配之外的对象中的变量, ...) 的本机函数。

鉴于这些操作违反了协议,如果您不理会分配器和本机代码,您可能无法解决此问题。

因此,您要么必须将协议更改为其他协议[并且仍然找出解决方案],要么更改分配器和本机代码的功能。

您可以通过坚持 GC 和内存分配器共享一个锁来解决 a),这样任何时候都只能有一个处于活动状态。这将阻止您的本机代码在 GC 运行时进行分配。这可能会给您的内存分配器增加额外的开销;也许不是,因为它可能必须防御多个线程运行解释代码并且所有线程都试图同时分配对象。即使你有一个线程本地分配器,在某些时候本地分配器必须用完空间并尝试从所有线程共享的池中获取更多空间,例如操作系统提供的池。

您可以通过坚持本机代码偶尔将其在其不透明状态下持有的所有指针存储回公共位置,以便 GC 可以看到它们,并像解释器线程一样暂停来解决 b)。

在本机线程中坚持指针安全的更复杂的方法是构建其内容的内存映射(最好离线完成),用布尔值标记每个机器指令(或包含代码的缓存行):“此处对 GC 安全”或“这里对 GC 不安全”。然后 GC 停止每个线程,询问是否在本地代码中运行,如果是,则获取 PC 并检查相应的布尔标志。如果安全,继续进行 GC。如果没有,单步执行下一条指令并检查修改后的 PC。是的,这是一个非常棘手的逻辑。您如何确定哪些指令是“安全的”与“不安全的”是一个额外的(相当大的)问题;如果本机代码的某些部分您不知道答案,您可以随时保守并标记“此处对 GC 不安全”。

如果你采用第二种方法,你也可以在你的解释器中使用。这将避免每个解释器线程在每个语句之后轮询 GC 标志的额外开销。当您调整解释器的速度时(您会发现一旦运行它就想这样做),您会发现轮询在运行时开销中所占的比例越来越大。

于 2018-08-18T21:55:31.297 回答