虽然前边写了两篇文章,提到了两个问题,一个是 idle 线程省去了一组常规关中断操作;一个是 _thread_exit
函数和 idle 线程有功能性重复行为。但写得虎头蛇尾的,没细说明白个中缘由。
今天就扒拉扒拉 _thread_exit
和 rt_defunct_execute
这两个函数的每一行代码,以及优化的理论基础分析。
销毁一个线程,需要完成的工作包括但不限于:
RT_USING_MODULE
销毁注册到线程的模块;RT_USING_SIGNALS
释放线程的信号;以上也是目前 _thread_exit
的实现,但是第 7 条的描述修改成:暂时把线程放到线程回收站(僵尸线程列表)里。
再细看 rt_defunct_execute
函数的实现,先从线程回收站里获取到线程句柄,然后实现了以上 7 条说明中除了 4 5 两条之外的其它所有工作。
可以把 _thread_exit
和 rt_defunct_execute
两个函数的工作合并简化呢?
静态线程对象和动态线程对象的区别就在需不需要释放对象本身占用的内存。其它的流程是完全一致的。这一特点,更是今天我们进行优化的理论基础。
_thread_exit
简化可行性假如先不考虑静态线程对象和动态线程对象的区别,无论是静态线程对象和动态线程对象都执行如下流程:
做最少最紧要的工作,把线程变成游离状态,保证线程不再被任务调度器调度。剩下的工作交给 idle 线程去完成。这样可以做到吗?
有几个问题需要考虑,
下面一一进行说明:
_thread_exit
函数的时候,这个定时器没有被启动!因为只有线程被阻塞的时候(延时或等待资源)才启动它,既然目标线程能走到 _thread_exit
函数肯定不是这两种阻塞状态。目前看,这个也可以放到 idle 线程。为什么把 idle 线程的优化放到这里讲?因为跟僵尸线程回收息息相关。这里的所作所为都跟上面说的是有机整体。
一、我们看看下面的代码。
rt_thread_t rt_thread_defunct_dequeue(void)
{
rt_thread_t thread = RT_NULL;
rt_list_t *l = &_rt_thread_defunct;
if (l->next != l)
{
thread = rt_list_entry(l->next,
struct rt_thread,
tlist);
rt_list_remove(&(thread->tlist));
}
return thread;
}
...
/* disable interrupt */
lock = rt_hw_interrupt_disable();
thread = rt_thread_defunct_dequeue();
if (!thread)
{
rt_hw_interrupt_enable(lock);
break;
}
...
以上的关中断操作,是因为 rt_thread_defunct_dequeue
中有全局变量 _rt_thread_defunct
。_rt_thread_defunct
是一个全局变量,用于存放僵尸线程的容器,只有两个地方引用了这个变量,其中,rt_thread_defunct_enqueue
函数往容器中加入一个元素,rt_thread_defunct_dequeue
提取一个元素,也就是一个增加元素写,一个减少元素写。前一种肯定是非 idle 线程里执行的,后一种肯定是 idle 线程里执行,也就是肯定是俩不同线程的写访问。
进一步拆解我们会发现, rt_thread_defunct_enqueue
函数只有一个写访问; rt_thread_defunct_dequeue
函数里,一个指针判断是读访问,一个查询也是读访问,还有一个删除元素是写访问。能看到这一点儿,笔者也有胆量说,只有两个“写访问”需要中断的保护,两个“读访问”,不需要中断保护!
无论 l->next
指针有没有被中断修改,都不影响 if
判断的执行流程。无论中断返回后, l->next
的实际值变成多少,缓存的指针值要么是修改前的 _rt_thread_defunct
地址,要么是修改后的其它线程的(tlist)地址,不会变成其他无意义的地址值。
如果 if
判断结果为真,rt_list_entry
这句更不用担心 l->next
被修改,因为根本不可能。 l->next
只有可能被下一句 rt_list_remove(&(thread->tlist));
修改。
总之,只有 rt_thread_defunct_enqueue
函数和 rt_list_remove(&(thread->tlist));
这一句需要关中断保护。
没想到这里写了这么多,也不知道有没有写清楚,缓口气继续说下面的。
二、_idle_has_defunct_thread
函数存在的意义
目前,只有启用 RT_USING_MODULE
模块后 _idle_has_defunct_thread
函数才会被定义。但是,它的存在的意义也只是获取到僵尸线程容器中的僵尸线程句柄。如果没有僵尸线程,它返回 RT_NULL。这一点儿和 rt_thread_defunct_dequeue
函数的一致的!
用 rt_thread_defunct_dequeue
函数代替 _idle_has_defunct_thread
函数也是理论有支撑,实际上行得通的。
三、卸载模块、释放信号需要在关中断保护吗?
这个问题目前笔者还不好说,但是,可以说的是,但凡线程变成僵尸线程的,任何给此线程发信号的行为都是不明智的。
四、静态线程对象 detach 过程,动态线程对象 delete 都不需要关中断保护。
rt_thread_detach
rt_thread_delete
两个函数同向对比 _thread_exit
rt_thread_detach
rt_thread_delete
三个函数也有很多类似的影子,不同的是:
rt_thread_detach
只能应用于静态线程对象,rt_thread_delete
只能用于动态线程对象。第二条没啥特别的,因为 _thread_exit
以及经受住两种不同对象的考验。关键是第一条的不同,需要我们对前边的新流程进程重新考量了。
假如某个线程 Y 销毁线程 X,执行 rt_thread_detach
rt_thread_delete
函数后,未及时切换到 idle 线程,也就是模块、信号、定时器、用户定义的清理任务等还未执行,切换到其他线程或者有资源可用了,会不会引起继续使用 X 的尴尬?
笔者唯一能想的起来的就是,X 线程因为延时或者资源不可用处于挂起挂起状态,线程内置定时器处于启动定时中, rt_thread_detach
rt_thread_delete
函数执行结束,恰好定时器 timeout 执行了 rt_thread_timeout
。这是完全要杜绝的。
因此,“线程内置定时器从定时器列表上卸载的工作”不能延迟到 idle 线程,要么保证执行 rt_thread_detach
rt_thread_delete
前线程内置定时器处于 stop 状态。后面这种很难做到。分析到这里, _thread_exit
rt_thread_detach
rt_thread_delete
三个函数至少的任务有:
有了上面线程内置定时器的思考,销毁模块和释放信号的时机有没有类似的问题呢?是不是也必须归还 _thread_exit
rt_thread_detach
rt_thread_delete
三个函数?笔者还是那句话,但凡线程变成僵尸线程的,任何给此线程发信号的行为都是不明智的。脑子不够用,理顺不清楚了
_thread_exit
rt_thread_detach
rt_thread_delete
三个函数的主要工作可统一为:
再次强调,如果能保证执行这三个函数前线程内置定时器已经被 stop 了,那么这里的第 3 条也可以挪到 idle 线程里。
idle 线程的常态行为只剩下判断是否有僵尸线程。如果没有僵尸线程不需要一次关中断操作;如果有僵尸线程,只有从容器里删除线程指针的时候需要关中断一次,其余的针对线程的操作均不需要关中断。
这黑底白字的代码块。。。。看着真的别扭。
黑底白字是故意的吗😂
@ccxzjz 论坛搞的啊,不是我弄的,黑底白字部分全是行内代码。
美工跟着某程序员跑了吧😭
@出出啊 已经在修改了😂
_idle_has_defunct_thread 这块有更深层次的原因。记得如果不抽取成函数,在带cache的A核芯片上会被优化跑飞掉……好像是这块,可能需要去查查历史记录才是的
@bernard rt_thread_defunct_dequeue 也是函数啊,一个返回真值,判断的真值,一个返回指针判断的指针。这里没有优化一说吧,再说了,_idle_has_defunct_thread 只有启用 module 的时候才用得上,不启用的时候是使用 rt_thread_defunct_dequeue 的。也就是目前带cache的A核芯片上也使用了 rt_thread_defunct_dequeue 的返回值做判断