木头人 发表于 2010-2-10 02:45:07

关于多线程下的容器使用 实例、原始指针、用引计数的分析

class U
{
public:
int Guid;//用户唯一ID
}

std::vector<class U> g_UserList;
std::vector<class U*> g_pUserList;
std::vector<shared_ptr<class U> > g_spUserList;

CMutex Mutex;

void GuidLock(int Guid) 可以根据 Guid 的值来使用不同实例的用户锁(实现方式不详述)

一、实体
class U GetUser()
{
Mutex.lock();
U u = g_UserList;
Mutex.unlock();
return u;
}

U u = GetUser();
GuidLock( u.Guid );

u.working();

GuidUnlock( u.Guid );

使用实体容器时全局锁的粒度可以达到最小, 在容器取出对象的代码之间上全局锁, 取出后再把实例独有的锁上锁
优点: 全局锁粒度小
缺点: 增加构造与析构花费, 需额外的实例对象锁

二、原指针
class U* GetUser()
{
Mutex.lock();
U u* = g_pUserList;
Mutex.lock();
return u;
}

U *u = GetUser();
GuidLock( u->Guid );

u->working();

GuidUnlock( u->Guid );

在使用原始指针后这样的代码变得不安全, 因为其它线程可能在 GetUser后 与 GuidLock 之前 delete User 造成崩溃
解决方案: 扩大Mutex粒度
class U* GetUser()
{
U u* = g_pUserList;
return u;
}

Mutex.lock();
U* pU = GetUser();

pU->working();

Mutex.unlock();

优点: 没有额外的构造与析构, 也不用对每个用户创建一个用户锁
缺点: 全局锁粒度非常大

三、引用计数指针
shared_ptr<class U> GetUser()
{
Mutex.lock();
shared_ptr<class U> spU = g_spUserList;
Mutex.unlock();
return spU;
}

shared_ptr<class U> spU = GetUser();
GuidLock( spU->Guid );

spU->working();

GuidUnlock( spU->Guid );

优点: 接近原始指针的性能, 只有引用计数的构造与析构消耗
缺点: 需额外的实例对象锁

总结:
就这样看来, 在 class U 的构造与析构远大于shared_ptr时, 无疑用 shared_ptr 效率最佳, 虽然原始指针不用为每个用户增加一个对象锁, 但只用一个全局锁的逻辑概念下除了在同一个线程并且使用自旋锁的情况下, 其它线程都没办法取得其它用户对象, 性能极为低效.
但引用计数指针还存在一个继承问题, 当 class U 是继承其它 class 的情况下
class U : public class Object
{

};

原始指针可以进行这样的操作
U *pU = GetUser();
Object *pO = pU;

但智能指针却不能这样
shared_ptr<U> spU = GetUser();
shared_ptr<Object> spO = spU;//shared_ptr<U> 不是继承 shared_ptr<Object>的子类

这样的情况其实可以通过 Object *pO = &(*spU); 临时解决, 但并不代表没有其它问题, 当把 pO 放入其它逻辑进行处理后该如何访问如下函数?
const char* GetUserName( shared_ptr<U> &pUser );

当然, 也可以写一个 const char* GetUserName( U *pU ); 来解决这个问题, 然而, 如果 GetUserName 并不是单纯把拿出其中一个成员, 而是要把 pU push_back 进一个待处理的事件列表 std::list<shared_ptr<U> > waitList 又该如何解决这个问题? 除非把 push_back 拉到上层逻辑, 封装性尽失.

虽然说实例比shared_ptr多了构造和析构的消耗, 但这种模式的确可以解决这样的问题
const char* GetUserName( Object &obj );
U u = GetUser();
O &o = u;
GetUserName( o );

[ 本帖最后由 木头人 于 2010-2-10 02:50 编辑 ]

wishel 发表于 2010-2-10 11:38:24

三、引用计数指针
shared_ptr<class U> GetUser()
{
Mutex.lock();
shared_ptr<class U> spU = g_spUserList;
Mutex.unlock();
return spU;
}

shared_ptr<class U> spU = GetUser(); // 1
GuidLock( spU->Guid ); // 2

spU->working();

GuidUnlock( spU->Guid );

======================================

如果1和2之间有另一个线程运行了SetUser()之类的函数,把g_spUserList置为NULL或者其他的值,还是会有问题。也就是说虽然阻止了raw指针中的delete问题,但还会有并发修改这个指针,导致这个指针为NULL或无效指针。仍然需要加锁来保证。

wishel 发表于 2010-2-10 11:41:04

关于智能指针的多态缺陷,(more?)effective c++中有一个条款有详细的分析。

木头人 发表于 2010-2-10 12:18:00

三、引用计数指针
shared_ptr<class U> GetUser()
{
Mutex.lock();
shared_ptr<class U> spU = g_spUserList;
Mutex.unlock();
return spU;
}

shared_ptr<class U> spU = GetUser(); // 1
GuidLock( spU->Guid ); // 2

spU->working();

GuidUnlock( spU->Guid );

======================================

如果1和2之间有另一个线程运行了SetUser()之类的函数,把g_spUserList置为NULL或者其他的值,还是会有问题。也就是说虽然阻止了raw指针中的delete问题,但还会有并发修改这个指针,导致这个指针为NULL或无效指针。仍然需要加锁来保证。

======================================

1和2之间 把 g_spUserList清空也不会对已经拿出来的 spU 有影响, 因为指针的取出和修改都由全局Mutex控制, 可以保证取出来的指针是正常的. 而且其它线程在SetUser过程中也会把该用户对象上锁, 只要 其它线程不会对 Guid 的值进行修改就可以保证并发的安全性.

> 另外, 我看过 more effective c++, 冒似没有对智能指针的多态缺陷分析的印象....今晚回家找找

wishel 发表于 2010-2-10 14:11:39

在对原始指针的说法中,“其它线程可能在 GetUser后 与 GuidLock 之前 delete User 造成崩溃”,也就是 说对该指针的用法 没有同步。
线程线程1:*ptr1 = x; 线程2:delete ptr2 ; or *ptr1 = y;

那么shared_ptr如果不进行同步,也有同样的安全问题,线程1:*share_ptr1 = x; 线程2:*share_ptr2 = y;

也就是说,前面所说raw指针的不安全问题,不是因为没有加引用计数,而是因为没有做同步。

木头人 发表于 2010-2-11 00:10:01

多谢楼上题醒, shared_ptr只是引用计数, 本身非线程安全. 改一下

shared_ptr<class U> GetUser()
{
shared_ptr<class U> spU = g_spUserList;
return spU;
}

Mutex.lock();
shared_ptr<class U> spU = GetUser();
Mutex.unlock();

GuidLock( spU->Guid );
spU->working();
GuidUnlock( spU->Guid );

[ 本帖最后由 木头人 于 2010-2-11 00:11 编辑 ]
页: [1]
查看完整版本: 关于多线程下的容器使用 实例、原始指针、用引计数的分析