基本过程如下:
(1) 调用者调用本地stub中的一个过程
(2) 这个stub过程把有关的参数组装成一个消息包或一组消息包, 形成一条消息. 运行此执行过程的远程场点的IP地址和执行该过程的进程ID号也包含在这条消息中.
(3) 将这条消息发送给对应的RPC runtime(RPC运行库)子程序, 由这个子程序将消息发送到Server.
(4) 在接收到这条消息后, server端的RPC runtime子程序引用与被调用者对应的stub中的一个子程序, 并让它来处理消息.
(5) 与被调用者对应的stub中的这个子程序解压消息, 解析出相关参数, 并用本地调用方式执行所指定的过程.
(6) 返回调用结果, 调用者对应的stub子程序执行return语句返回到用户, 整个RPC过程结束.
. 实现线程安全的dll
线程局部存储TLS
Windows操作系统提供了Process/Thread的程序模型,其中Process是资源的分配对象
,掌握了程序所拥有的资源,而Thread则代表了程序的运行,是操作系统调度的对象。需
要注意,操作系统中,这两种东西都是一种KERNEL32对象。分别由Process DataBase和Th
read DataBase来表示。具体可以参考Matt Petrik的Windows 95 Programing Secret。
Thread Local Storage是一个实现Thread的全局数据的机制,并且这些数据仅仅在这
个Thread中可见,因为这些数据保存在该Thread的Thread DataBase中:在每一个Thread
DataBase中都定义了一个64元的DWORD数组用来保存这些数据。同时操作系统也提供了相应
的函数来完成对这些数据的操作,如:TlsAlloc,TlsFree,TlsSetValue,TlsGetValue。
在MFC中,也提供了TLS功能,为此MFC设计了一系列的类和程序来完成这个任务。具体
的程序在afxtls.cpp和afxtls_.h中。
涉及到的主要的类有:
class CTypedSimpleList : public CSimpleList
struct CThreadData : public CNoTrackObject
struct CSlotData
class CThreadSlotData
class CThreadLocal : public CThreadLocalObject
其中CThreadSlotData是封装TLS的最重要的类,CTypedSimpleList,CSlotData,CTh
readDAta都是为了封装TLS而设计的只具有辅助功能的类。CThreadLocal是更高层的封装。
首先让我们来对其数据封装方式进行分析,重要的类的定义及其分析如下所示:(为简
单起见,只列出数据成员而不再列出函数成员)
定义:
class CThreadSlotData
{
public:
DWORD m_tlsIndex;
int m_nAlloc;
int m_nRover;
int m_nMax;
CSlotData* m_pSlotData;
CTypedSimpleList<CThreadData*> m_list;
CRITICAL_SECTION m_sect;
};
分析:
在afxtls.cpp中定义了一个CThreadSlotData类的全局变量:_afxThreadData。在CTh
readLocal的成员函数中大量使用了这个全局变量来访问TLS功能。
DWORD m_tlsIndex
用来保存TLS数据的索引,也就是在Thread DataBase中64元数组中的偏移量,这个数据在
CThreadSlotData类的构造函数中初始化。
int m_nAlloc
int m_nRover
int m_nMax
这三个变量用来分配slot和记录相关状态,比如m_nAlloc用来保存当前已经分配的slot的
个数。线程为每一个TLS数据分配一个slot。
CSlotData* m_pSlotData;
用来记录已经分配的每一个slot的状态:已经使用或是尚未使用。
CTypedSimpleList<CThreadData*> m_list;
CThreadSlotData为每一个Thread实现一个并且只实现一个CThreadData对象,并且用链表
类对象m_list来管理它们。实际上,真正被保存到Thread DataBase中去的是这个CThread
Data对象的指针,而程序员要保存的TLS数据被保存到这个CThreadData对象的pData成员指
向的动态数组中。所有Thread的CThreadData对象通过CThreadData对象的pNext成员连成链
表,并由CTypedSimpleList<CThreadData*> m_list管理。
CRITICAL_SECTION m_sect;
由于所有Thread的TLS操作都要靠访问_afxThreadData来实现,这样就产成了多线程同步的
问题,m_sect就是用来进行线程同步的变量。保证每次只有一个Thread在访问_afxThread
Data中的成员变量。
定义:
struct CThreadData : public CNoTrackObject
{
CThreadData* pNext; // required to be member of CSimpleList
int nCount; // current size of pData
LPVOID* pData; // actual thread local data (indexed by nSlot)
};
分析:
CThreadData用来辅助CThreadSlotData来完成TLS功能。每一个Thread的TLS数据都要
靠一个CThreadData对象来管理和保存。
CThreadData* pNext
在CThreadSlotData中,CThreadData由一个链表来管理,pNext用来把各个Thread的CThre
adData对象连成链表。
int nCount
指出用于保存TLS数据指针的动态数组的长度。
LPVOID* pData
在CThreadData保存的实际上是各个TLS数据的指针,为此定义了一个指针数组,nCount用
来指示数组长度,pData用来指出数组的基地址。
定义:
struct CSlotData
{
DWORD dwFlags; // slot flags (allocated/not allocated)
HINSTANCE hInst; // module which owns this slot
};
分析:
CSlotData用来辅助CThreadSlotData来完成TLS功能。每一个Thread的TLS数据都要靠
一个CThreadData对象来保存,具体实现是把TLS数据的指针保存在CThreadData对象的动态
指针数组中(基地址由pData指出)。而这个数组中每一个成员的使用状况则由一个与之长度
相同的CSlotData数组来表示,具体由DWORD dwFlags来表明。
从上面的分析不难发现,MFC中TLS功能的封装是这样的,所有Thread的TLS数据指针都
保存在一个动态的指针数组中,而该数组的基地址由一个CThreadData对象的 pData指出。
同时,保存在Thread DataBase中的是这个CThreadData对象的指针,而不是TLS数据的指针
,并且其索引值均相同,都为CThreadSlotData类中的m_tlsIndex成员。而且,在CThread
SlotData中提供了一个链表来管理所有Thread的CThreadData对象。这样CThreadSlotData
类就能访问所有的Thread的TLS数据。见图tls.bmp。(为了方便,我把图放到了签名档中了
,就在下面)
下面来进一步说明如何使用TLS功能。
为了方便TLS的使用,MFC设计了CThreadLocal类。它是一个模板类,具体的定义如下
:
template<class TYPE>
class CThreadLocal : public CThreadLocalObject
{
// Attributes
public:
AFX_INLINE TYPE* GetData()
{
TYPE* pData = (TYPE*)CThreadLocalObject::GetData(&CreateObject);
ASSERT(pData != NULL);
return pData;
}
AFX_INLINE TYPE* GetDataNA()
{
TYPE* pData = (TYPE*)CThreadLocalObject::GetDataNA();
return pData;
}
AFX_INLINE operator TYPE*()
{ return GetData(); }
AFX_INLINE TYPE* operator->()
{ return GetData(); }
// Implementation
public:
static CNoTrackObject* AFXAPI CreateObject()
{ return new TYPE; }
};
在使用CThreadLocal时,只要用CThreadLocal<ClassType> name;即可构造一个类型为
ClassType的TLS数据,注意ClassType必须以CNoTrackObject为基类。实际上上述声明定义
了一个名称为name的CThreadLocal对象,但是通过这个CThreadLocal对象,即可生成并访
问类型为ClassType的TLS数据。
dll的多进程多线程安全的几种策略
1、动态库只有一个导出函数。
这种情况非常少,也是最容易处理的情况。这种情况下编写函数时,只需要考虑不要有冲突的全局数据就可以了。这里的全局数据包括了在堆中分配的数据块和静态全局变量等。如果存在这样的全局数据,那么进程中的不同线程访问这个函数就会造成冲突。
解决办法也很简单,就是尽量用堆栈(stack)来解决问题。由于堆栈的所有人是线程,所以它必然是线程安全的。当然也要注意避免堆栈溢出。
我们都知道,如果要在函数再次调用时保留前一次调用的状态,可以使用静态变量。但如果你要保持函数的线程安全,那么静态变量是不能用的,因为静态变量是全局的,是属于进程的,也就是属于进程内线程共享的。所以如果确实需要在同一线程中保持函数的状态,相当于在不同次调用间传递参数,可以考虑使用静态全局线程局部变量,即:
__declspec( thread ) int tls_i = 1;
该变量定义就使编译器保证了tls_i是对应于每个线程的,即每个线程都一个tls_i的副本(copy),这样必然就是线程安全的。
2、动态库导出了多个函数,而且多个函数间存在数据传递。
就像前面说的,一般DLL都导出多个函数,一个初始化,一个资源释放,其他为核心功能函数。这些函数间极有可能发生数据传递。如果一个初始化函数是在线程A中调用的,而核心功能函数是在线程B中调用的,那么线程A初始化函数的资源就无法对应线程B中的核心功能,此外还有核心功能函数间的数据传递,这样的DLL就不是线程安全的,必然导致错误。
解决办法是由用户(即使用DLL的人)保证这些导出函数是在一个线程中调用。但这样会很大程度上限制接口的设计和用户的使用自由度。所以最好的方法是函数只管自己的线程安全,不同函数传递数据用动态TLS,线程局部存储。
比如:
我在全局定义了一个变量,用于存储当前线程局部存储的index ID。
__declspec( thread ) int tls_i = 1;
当调用分配资源的函数时,调用动态TLS函数TlsAlloc,分配一个ID,将其记录在全局的线程安全的tls_i变量,并通过TlsSetValue函数将数据保存在线程安全的区域;当调用获取资源的函数时,通过TlsGetValue获取资源,处理完成后,调用Tlsfree对TLS index释放,以便新线程占有。
这样,只要DLL中每个函数保证其局部是线程安全的,函数间传递数据通过TLS(静态和动态),就可以实现整个DLL的线程安全。
3、限制访问DLL中某一函数的线程数目。
有时候,对于DLL中的某一个函数的访问线程数目是有限制的,超过了限制其他线程就得等一定的时间,一定的时间过后如果还不能得到执行机会,那就返回超时。这样的设计对用户来说是友好的,而且很实用,有的商业程序确实是按照允许用户访问的通道数目来计价的。
对DLL中的函数做这样的一个封装,一般是简单的待用Semaphore信号量,来解决。DLL初始化时调用CreateSemaphore函数对信号量进行初始化,其原型如下:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
// pointer to security attributes
LONG lInitialCount, // initial count
LONG lMaximumCount, // maximum count
LPCTSTR lpName // pointer to semaphore-object name
);
对于信号量,它每WaitForSingleObject一次(当然是要进入),其状态值(一个整数)就减1,使用完ReleaseSemaphore其状态值就加1,当其状态值为0时信号量就由有信号变为无信号。利用信号量的这一特性,我们在初始化时将信号量的初始值(第2个参数)设置为限制的线程访问数目。在要限制访问线程数目的函数内部,通过调用WaitForSingleOject获取控制权,并指定一个等待时间(这个由配置文件指定),根据情况超时返回,使用完ReleaseSemaphore释放对占用,让其他线程可以调用这个函数。
4、多进程情况下的多线程安全DLL。
前面3讲了有时候需要对某一函数的访问线程进行限制,而我们知道,DLL是可以被多个进行加载并调用的。那就是说如果我们只对一个进程进行了限制,那么在多进程调用的情况下,这样的限制被轻易攻破。
我们都知道,Semaphore信号量属于内核对象,也就是说其可以被多进程共享访问,也就说,如果我们给一个Semaphore指定了一个名字,在另一个进程中,我们只要调用OpenSemaphore函数用同一名字打开信号量就可以访问了。这样问题就解决了?
现实情况是,多进程情况下,一般不是简单的多进程共享一个Semaphore就可以了。多进程间需要互通很多信息。一般的解决办法是,采用共享数据段。
#pragma data_seg("share")
int share_data;
#pragma data_seg()
#pragma comment(linker,"/SECTION:share, RWS")
通过pragam编译器指令生成了一个名叫share的共享数据段,这样对于变量share_data就可以多进程共享的了。如果要多进程间交换数据,只要在data_seg中添加数据定义即可。
http://blog.chinaunix.net/u/25952/showart_274326.html
深拷贝即进行了类的成员变量赋值操作,还进行了引用对象的拷贝操作,而浅拷贝只进行类成员变量的赋值操作。还可以理解为如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源但复制过程并未复制资源的情况视为浅拷贝。
对于C++的类来说,拷贝构造函数不是总要写的,系统会提供默认的拷贝构造函数,就像默认构造函数一样。但是如果在拷贝构造函数里面发生了资源分配(比如堆空间申请)情况,就需要写自己的拷贝构造函数来。如:
string::string(const string& str)
{
if (this.data == str.data)
return;
if (str.data == null)
{
this.data = new char[1];
this.data[0] = 0;
}
else
{
this.data = new char[strlen(str.data) + 1];
strcpy(this.data, str.data);
}
}
关于什么是拷贝构造函数,按照标准有如下规则:
a) X& -> X::X(X&) -> X::X(X&, int i=0)
b) const X& -> X::X(const X&)
c) volatile X& -> X::X(volatile X&)
d) const volatile X& -> X::X(const volatile X&)
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
根据以上规则,我们可以知道一个类可以拥有多个拷贝构造函数。编译器提供的默认拷贝构造函数执行的是浅拷贝操作。
.应用程序的最大可用内存(32位平台)
我们都知道对于32位的操作系统平台最大的物理内存是4G(2的32次方),而windows内核需要分配2G的物理内存寻址空间,所以应用程序可用的最大内存为2G,那么如何提高内存的最大分配数值呢?1.切换操作系统的位数,从32位升级为64位;2.在启动项中设置/3G,这样应用程序的物理内存寻址空间就可以到3G。
.Core dump 文件的分析
当我们的程序崩溃时,内核有可能把该程序当前内存映射到core文件里,方便程序员找到程序出现问题的地方。
http://blog.chinaunix.net/u/6593/showart.php?id=150292
.MFC 非模态对话框 模态对话框
模态对话框消息循环是封闭的,即只在本窗口内进行,出发收到WM_DESTROY 或是 WM_CLOSE。而非模态对话框的消息处理是基于父窗体的。如何让非模态对话框转变成模态对话框 http://blog.csdn.net/cathyeagle/archive/2004/09/15/106004.aspx。同模态对话框创建非模态对话框可以通过Create函数。
. 进程内服务器与进程外服务器的区别
进程内服务器是以dll方式加载到应用程序进程中的,因此执行速度比进程外服务器快,同代理也可以实现RPC服务。进程外服务器是以exe或是服务的实现出现,进程外服务器可以分布在本机也可以分布在远程机器,但是进程外服务器跟进程通信需要通过进程间通信的方式进行,如共享内存,管道等,这样速度上比进程内服务器要慢,不过进程外服务器crash掉不会影响应用程序,所以健壮性比较好。