多线程程序中,我们经常会遇到这种情况,主线程启动时加载一些参数到内存中的某个对象或者数据结构中,将这个对象或数据结构作为参数传入各个子线程中,为了避免对象的复制与拷贝,往往传入的是指针,子线程启动,进行业务逻辑处理,需要根据key值获取hashtable中的value,value = m_pParam->get(key),代码如下所示
//用hashtable保存程序运行所需的参数
hashtable hashParam;
void loadParam(hashtable & hashParam)
{
//加载参数
return;
}
//模拟线程类
class ThreadX
{
public:
ThreadX(hashtable *pParam):m_pParam(pParam)
{...}
T * get(int key)
{
return m_pParam->get(key);
}
/*其他成员*/
private:
hashtable *m_pParam;
/*其他成员*/
};
主线程将构造好的hashtable传入子线程,这里的子线程对hashtable的操作是只读操作,保证其他线程也不会对hashtable的数据进行修改,所以这里的操作没有加锁。随着需求的变更,现在hashtable中的数据需要支持动态刷新,即之前的参数可能会有变动(数据库中的参数、配置文件信息等),在程序运行中通过发送信号量,由主线程对参数进行重新加载。通常的做法是,对m_hashParam+互斥锁进行封装,设结构应为SynParam,在操作时上锁:主线程加载参数,lock() -> 读取参数到内存 -> m_hashParam->put(key, value) -> unlock(),重新加载参数时,同样的方式处理。子线程同样保存封装后的结构体SynParam的指针,m_pSynParam,并在操作时,进行加锁保护。这样在程序收到参数刷新的信号量时,主线程对参数进行刷新,而子线程读取到的是最新的数据。注意在子线程中 m_SynParam->get(key)时,同样需要加锁,这样才能保证读取到底数据是正确的,原理等同于一个线程写多个线程读的经典问题。在程序中参数刷新的频率远低于子线程中对参数结构的读取,虽然能够保证参数每次都读取到最新的,但是加锁的代价实在太高,会影响到程序的效率。
现在考虑另一种方式,主线程依然加载数据到内存中,假设加载到hashParam_A中,设 phashParam = &hashParam_A,主线程中创建子线程,将phashParam作为参数传递给子线程,注意,子线程中保存phashParam的地址,m_pphash = &phashParam,即pphash = &(&m_hashParam),在子线程中,获取key值操作表示如: value = (*pphash)->get(key); 这里同样只读操作,并没有加锁:
void loadParam(hashtable & hashParam)
{
//加载参数
return;
}
class ThreadX
{
public:
ThreadX(hashtable *pParam):m_pParam(&pParam)
{...}
/*其他成员*/
T * get(int key)
{
value = (*m_ppParam)->get(key);
}
private:
hashtable **m_ppParam; //注意这里是二级指针
/*其他成员*/
};
hashtable hashParam_A;
loadParam(hashParam_A);
hashtable * pParam = &hashParam_A;
ThreadX thd(pParam);
在参数刷新时,主线程加载内存到另一个同样的结构体 m_hashParamB中,这时令 pParam = &hashParamB,pParam的值改变了,而子线程中的m_ppParam指向phash,所以*pphash的值也改变了,实际指向hashParamB,即新的内存结构:
hashtable hashParam_B;
loadParam(hashParam_B);
hashtable * pParam = &hashParam_B;
注意phash=&hashParam_B没有加锁的原因是因为在32位的平台,这里是一个原子操作,所以可以保证子线程value = (*m_ppParam)->get(key); 时得到最新的参数值。如果是64位的平台,对pParam = &hashParam_B的指针赋值操作,可能会被分解成2条指令,可能会导致在某个子线程获取value,对*m_ppParam解引用时,*ppParam指向的表示一个错误的地址!