之前只在C语言下,利用pthread搞过一些小demo玩。看实验室并行框架源码时发觉,全是C++风格的多线程,涉及到的pthread_create()为何不能以成员函数作为参数、锁成员变量、线程安全的类各种的问题,这些之前完全没有接触过。特此进行了两天的调研和思考,现在就简单整理一下这两天看过的资料。
由于就真的是光看没练,只是单纯翻了一堆大致原理,可能会有不少纰漏。阅读后觉得有不妥的地方,那很可能就真的是不妥的,笑。
之前转载了一篇文章,文中大致介绍C++类成员函数在内存中是怎么摆的。其实摆法和C语言就没什么区别,函数还是放在代码段,数据则是放在数据段+bss段+栈+堆中。
类成员函数,背后的原理大致上就相当于在函数调用中,自动多加了一个相关结构指针的参数,然后就可以访问到相关结构的成员变量。
多个对象及其成员函数的内存分布:
大致原理示例代码(当然,实际区别在于调用约定上):
#include
using namespace std;
struct testStruct
{
int i;
char c;
};
void testFunc(testStruct* myThis)
{
cout << "i: " << myThis->i << " c: " << myThis->c << endl;
}
int main()
{
testStruct ts0 = {1, 'a'};
testStruct ts1 = {2, 'b'};
testFunc(&ts0);
testFunc(&ts1);
}
原因就是上面提到的调用约定,pthread_create()的参数需要是cdecl调用约定的函数。
调用约定大致上就是,发生函数调用时,在汇编层面到底是如何运作的。在gcc下,C++比较基本的调用约定有两种:
所以调用约定基本上要解决这三个问题:
所以在gcc的thiscall调用约定下,类成员函数真的就是弄了个隐藏this指针参数。
由于之前只是简单地用写了多线程的demo,互斥锁都是直接定义成全局变量,加上之前对于成员函数在内存怎么摆也有疑惑,所以就搞的很乱。
其实类中定义互斥锁,和全局定义互斥锁,差别并不大。
明确以下这些点,我的疑惑就消除了:
点1:----------------------------------------------------------------
struct aboutMutex
{
pthread_mutex_t mutex;
int i;
aboutMutex()
{
do some thing;
}
};
void* testThread(void *id)
{
do some thing
}
void testFunc(testStruct* myThis)
{
aboutMutex am0;
aboutMutex am1;
int t0, t1, t2, t3;
// 线程1和线程2传参am0
pthread_create(&t0, NULL, testThread, &am0);
pthread_create(&t1, NULL, testThread, &am0);
// 线程3和线程4传参am1
pthread_create(&t2, NULL, testThread, &am1);
pthread_create(&t3, NULL, testThread, &am1);
}
线程1和线程2,对于testThread函数的执行,会由am0.mutex来同步,并都会操作am0.i;
线程3和线程4,对于testThread函数的执行,会由am1.mutex来同步,并都会操作am1.i;
类比到同一个class的不同对象,假设为o0和o1,
不同的线程操作同一个对象o0的成员函数时,会由o0.mutex来同步,类似于amo.mutex;
不同的线程操作同一个对象o1的成员函数时,会由o1.mutex来同步,类似于am1.mutex;
点2:----------------------------------------------------------------
其实这个源自于一个很基本的问题,
为什么全局和静态变量生命周期是跨越整个进程执行期间的?
又为什么栈变量生命周期就是单个函数执行期间?
我觉得关键其实在于,要理解在内存(虚拟内存,下同)意义上,变量还活着是什么意思,变量死了是什么意思?
我认为,变量在底层角度来说,就是一个内存位置。变量活着的意思,即变量还在内存占着位置;变量死了的意思,即变量已经不在内存中占着位置了。
而全局和静态变量,与栈变量的生命周期差别,背后原因是它们所在内存区域的特性。
数据段(还有bss段) 在进程没死之前,其中的变量就在那静静地摆着,变量与内存位置能看着绑定了,所以只要进程不死,全局和静态变量就还活着;
栈段则不同,当前函数执行完毕之后,属于当前函数的那一片栈就作废了,当前函数中的变量自然就已经没了自己的内存位置了。所以只要所属函数死了,相应的栈变量也就死了;
画个了示例图,进行了很大的简化,只有数据段和栈段:
当func0被调用时,内存状况:
当func0执行完,func1被调用时,内存状况:
点3:----------------------------------------------------------------
明白了变量的生命周期问题后,感觉点3也就很容易理解了。
锁要生效,需要
同步的线程所用的锁要是同一把;
确保锁变量还活着;
同一把锁这个基本没什么问题,pthread_create()创建线程并传参的时候,想要同步的线程给传过去同一把锁便是。
问题是怎么确保锁还活着,写简单demo的时候,直接会整个全局锁,那样肯定可以了,直接就是不死锁王,但是在栈上的锁呢?
研究完点2,重新理清楚思路之后,发觉这个问题挺简单的。还是总是用全局锁,以致于把锁和普通变量过度区别的问题,还是那句嘛,锁也就是变量而已。
其实在C和C++中,传指针是很日常的事情,这些被传指针的变量,和被传了指针的锁,本质上还不就是一样的,都是跨栈访问变量而已,区别也就是从什么栈跨到了什么栈。
也画了个传指针跨栈访问的示意图,老千层饼了:
所以问题变得简单了
要确保锁活着,那么只要锁所在的那块栈帧活着就行了啊,和跨栈访问局部变量压根就没区别。在多线程中,有个显而易见的简单解决方法,在父线程上弄锁,然后做好安全措施,让父线程别在子线程结束前死了,那锁的存活也就有保证了。