我正在用C ++开发AST解释的脚本语言。解释器具有一个简单的“世界停止运行”标志和清除垃圾收集器,每当触发收集收集器时,它都会向所有应用程序线程发送一个停止请求,然后等待所有这些线程被暂停。每个线程只有一个安全点可以满足 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
函数),则所有其他应用程序线程和垃圾收集器线程将等待该线程到达安全点,以便可以执行垃圾收集。
有办法避免这种情况吗?
答案 0 :(得分:2)
有办法避免这种情况吗?
不适用于您当前的设计,以及“本机”代码的表面上不透明的属性(看不见/触摸内部)。
您的设计很简单:每个线程有时都必须位于“安全”位置,在该位置它不会分配您的语言可以识别的对象,并且不会在那些无法定位的对象中保留指向此类对象的指针被GC看到。通过确保强制每个线程定期检查是否需要GC的线程协议,您可以确保在设计为该线程安全的位置进行操作。
您调用的本机函数根本不遵循您的协议。它们可能会做两件坏事:a)分配解释后的语言对象,以及b)在不透明状态下保持指向此类对象的指针(寄存器,GC无法看到的堆栈帧中的变量,在内存管理器分配之外的对象中分配的变量) ,...)的本机功能。
鉴于这些操作违反了协议,如果不理会分配器和本机代码,则可能无法解决此问题。
因此,您要么必须将协议更改为其他协议(并且仍然找出解决方案),要么更改分配器和本机代码的作用。
您可以通过以下方式解决a):坚持要求GC和内存分配器共享一个锁,以便在任何时候只能激活一个。这将阻止您的本机代码 从GC运行时分配。这可能会增加内存分配器的开销。可能不是,因为它可能必须针对运行解释代码的多个线程以及所有试图同时分配对象的线程进行防御。即使您具有线程本地分配器,在某些时候本地分配器也必须用完空间并尝试从所有线程共享的池中获取更多资源,例如,操作系统提供的池。
您可以通过坚持要求本机代码偶尔将其处于不透明状态的所有指针存储回公共位置,以使GC可以看到它们,并像解释程序线程一样暂停,来解决b)。
在本机线程中坚持指针安全性的更复杂方法是构建其内容的内存映射(最好是离线完成),并用布尔值标记每条机器指令(或包含代码的缓存行):此处的GC”或“此处不安全”。然后,GC停止每个线程,询问是否正在以本机代码运行,如果是,则获取PC并检出相应的布尔标志。如果安全,请继续使用GC。如果不是,则将线程单步执行到下一条指令,然后检查修订的PC。是的,这是非常棘手的逻辑。另外,如何确定哪些指令是“安全”还是“不安全”则是另一个(相当大的)问题。如果您不知道本机代码的某些部分的答案,则可以始终保持保守并标记为“此处不安全”。您仍指望本机代码不要进入没有任何“安全”要点的循环,或者至少不要经常这样做。
如果采用第二种方法,则也可以在解释器中使用。这样可以避免每个解释器在每个语句之后轮询GC标志的额外开销。当您调整解释器的速度时(您会发现想要在运行时立即这样做),您会发现轮询在运行时开销中所占的比例越来越小。