C语言实现this指针
this指针作为面向对象中的重要机制,可以指向当前对象自己的属性和方法。在之前的C语言实现面向对象的专栏中,this指针仅仅采用了一个公用的全局变量来存储,这种设计方法不仅在多处理机多线程环境下会产生问题,甚至在单处理机环境下也会导致问题(中断)。举个例子:
上面的例子中,this函数同时被两个函数访存,且中断事件导致代码失去顺序性,产生跳转,这和多线程轮询执行一样,必然导致this指针被覆盖或篡改的问题。
下面来解决这个问题:
首先肯定不能用一个统一的this指针为每个模块共享使用,应当为每个模块独立创建一个私有的this指针,说到私有,也就是每个C文件下的this指针是独立存在的,不受其他文件的this指针所影响。在C语言中,如果你用常规方法创建同名的全局变量,会报重复定义的错误,可以通过添加static关键字来表示此全局变量存放于静态存储区中,且只在本文件中可以被访问,其他C文件无法访问到此变量,如下例:
这样就解决了每个模块私有访问自己的this指针的问题。
如何保证在多线程或中断环境下,this指针仍能保证线程安全,下面来解决第二个问题:
众所周知,多线程和中断之所以能正确的执行而不影响其他函数,是因为在跳转到中断函数或线程任务函数之前,执行了保护现场工作,即将CPU状态寄存器和PC的值保留在当前函数栈中,然后再执行中断函数或线程任务。执行完毕后,先从栈空间中把之前保存的现场信息还原,再继续执行。参照上面的流程可知,this指针产生数据一致性问题的本质原因是没有做到在函数调用时保存现场,在线程任务中或中断函数中产生对象函数调用则将当前对象指针压入This栈中,在调用对象方法时,第一行用于从This栈栈顶弹出自身对象,此时栈顶指针将指向上次的this指针,通过这种方式即可实现this指针的独立性。
在对象调用自身方法时,先通过setThis(n)操作将当前对象压入This栈中,再调用自身方法,在方法中,第一行用于获取栈顶指针并保存在temp中,即刚才压入栈中的对象,随后的所有对象操作都通过temp来调用。通过栈来设计this指针,在中断或多线程情况下,假如A执行了setThis(a)将当前对象a压入栈中,但是没来得及调用方法取出栈顶对象,就被B中断信号打断了,B中断调用setThis(b)将b压入栈中,而后b对象调用方法并正确从栈顶弹出自身对象b,继续执行方法,中断函数执行完毕后,回到A任务,此时A再从栈顶获取的对象正是自己的对象a。
由于中断事件是以中断函数为单位执行,所以在中断嵌套的情况下,是一级一级的从函数栈中弹出,不存在像多线程轮询时的无序性,即A任务执行了一半又去执行B任务,B任务可能刚开始执行一会又去执行C任务,C任务执行了一半又回到A任务.....。而中断是能确保最高级别中断函数执行完毕后再回到上一级函数继续执行,这样一级级向上从函数栈中弹出。因此以上操作在中断情况下能正确执行,下面来讨论多线程轮询情况下的解决办法。
由于多线程情况比较复杂,举一个例子来说明上述方法在多线程情况下的问题和解决方案。
例:现有3个线程A,B,C,以及三个同类对象a,b,c
(1).A线程首先调用setThis(a)操作将a对象压入This栈
(2).没等a对象调用方法从This栈顶获取自身对象,A线程时间片消耗完毕,切换到B线程执行
(3).B线程执行setThis(b)操作将b对象压入This栈,此时This栈中元素为:b,a
(4).b对象调用方法,第一行先从栈顶弹出自身对象后,再继续执行方法
(5).B线程方法执行了一半,时间片消耗完毕,切换到C线程执行,此时This栈中的元素为:a
(6).C线程执行setThis(c)将c对象压入This栈,此时This栈中元素为:c,a
(7).在c对象调用方法从This栈顶弹出自身对象之前,线程C时间片消耗完毕,切换到A线程执行,此时This栈中的元素为:c,a
(8).A线程继续执行,a对象调用方法,第一行从This栈顶弹出对象,但此时弹出的对象是c,并非自身对象,这必然导致a对象调用方法出错。
那么如何解决这个问题呢,其实十分简单,熟悉线程安全的开发者立刻就想到了信号量。这里使用一个同步信号量来保证setThis()操作和对象调用方法从This栈弹栈操作这两个操作成为原子操作,要么两个操作都执行,要么都不执行。在setThis()函数压栈时,flag置为true,在方法调用的第一行弹栈操作时,将flag置为false。如果A线程执行了setThis(a)将对象a压入栈中,并置flag为true,然后切换到B线程,执行setThis(b)时,执行while(flag);等待锁被释放,即忙等,当然也可直接yield(),主动放弃此轮CPU调度。线程B忙等到时间片消费完毕或主动放弃CPU调度后,切换回A线程,A线程继续,调用方法从This栈中弹出a对象,并置flag为false,a继续执行方法,A线程时间片耗尽后切换到B线程,B线程继续while(flag),由于此时flag为false,可以正常操作。
总结下两种情况:
中断:无需上锁,可正确入栈出栈,CPU利用率高
多线程:需要上锁,同类对象忙等情况下,CPU利用率低。yield()情况下,CPU利用率高。
不过不用担心效率问题,上面的例子为多线程切换的极端情况,很少会发生,一般情况下线程时间片为毫秒级别,完全能够保证对象入栈和出栈能在时间片周期内完成,即不会产生等待。而在上述的极端情况下,A线程在时间片结束的那一刻调用了setThis将对象a入栈,但没来得及调用对象方法从栈中获取对象a,就切换到B线程执行了,此时B线程如果调用同类对象,要么等待,要么主动让出CPU给A线程继续执行,解开同步锁。在对象调用方法的流程如下:
call()是一个宏函数,里面执行了两步:
(1) 设flag=true,调用setThis(str),将当前str对象压入String类的This栈中
(2) 调用.equals("123")方法,方法内第一行从This栈栈顶取出str对象并设flag=false然后继续执行方法
可见在对象调用方法时,入栈和出栈操作是紧挨着的两条语句,即flag被置为true后,马上就执行入栈出栈,而入栈和出栈的时间复杂度是o(1),也就是在flag置为true后,仅需几条指令就能完成入栈出栈,并置flag=false。为了再次提升效率,可将pop()函数改为内联函数或宏函数,减少函数入栈指令和函数出栈指令对时间片的开销。就像这样:
可见入栈出栈总共就2条语句,在通过inline修饰,减少了函数跳转的开销,保证在绝大多数情况下不会出现忙等。
针对两种情况的setThis()函数实现不一样,中断情况下无需任何判断,直接入栈,而多线程情况下需要加入while(flag);或if(flag) yield();操作。
下面我将用c语言演示通过栈实现this的代码:
封装基于顺序栈的This对象:
This.h文件:
This.c文件:
创建String类测试This对象:
MyString.h文件:
MyString.c文件:
在main.c中调用以测试结果:

下面演示多线程下测试结果:
测试方法:让多个线程中的多个String对象输出自己的字符串值,字符串对象的获取通过This栈获取,如果正确输出,则多线程环境下无误。
测试正常无误,如下视频所示:

码字码了一整天,就当复习下数据结构和操作系统了。像往常一样,此工程直接分享出来,带师门可自行测试和移植优化:
链接:https://pan.baidu.com/s/15EfFJOFMp7OEawPj5oYl3w
提取码:ALYA