浅析线程安全的对象生命期的管理

11 篇文章 0 订阅
3 篇文章 0 订阅
1.前言:c++与其他面向对象语言不同,没有gc,c++要求程序员自己管理对象的生命,在并发编程环境下显得尤为困难。当一个对象能被多个线程同时看到时,那么对象的销毁时机就会变得模糊不清,可能出现多种竞争条件(race condition):
  • 在即将析构一个对象时,从何知道别的线程是否正在执行该对象的成员。

  • 如何保证在执行成员的函数期间,对象不会被另一个线程析构。

  • 在调用某个成员函数之前,如何得知这个对象是否还活着?它的析构函数会不会碰巧执行到一半?

2.线程安全的定义

​ 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。

3.实现一个简单的线程安全的计算类counter

//program 1.1.cpp
class Counter: boost::noncopyable
{
public:
    Counter() = default;
    int64_t value() const;
    int64_t getAndIncrease();
private:
    int64_t value_ = 0;
    mutable MutexLock mutex_;
};
int64_t Counter::value() const
{
    MutexLockGuard lock(mutex_);
    return value_;
}
int64_t Counter::getAndIncrease()
{
    MutexLockGuard lock(mutex_);
    int64_t ret = value_++;
    return ret;
}

​ 因为每个counter对象都有自己的mutex_,因此不存在锁争用。而且由于lock的析构会晚于返回对象的构造,因此可以有效保护这个共享数据。

​ 如果mutex_的类型是static,则会导致每个对象拥有相同的mutex,会导致锁争用。

​ 如果counter是动态创建的并通过指针访问的话,在多线程中,可能两个以上线程都会持有counter对象的指针,因此会出现销毁时候的race condition。

4.构造对象要做到线程安全

​ 要做到线程安全,对象构造期间不能泄露this指针,即

  • 不要在构造函数中注册任何回调;

  • 也不要在构造函数中把this指针传递给跨线程的对象;

  • 即使在构造函数最后一行也不行;

如果在构造函数执行期间,对象初始化还没有完成,如果其他对象持有了this指针,可能其他线程就会访问这个未成品对象,会造成难以预料的后果。

如下的例子,如果Foo是一个基类的情况下,基类的构造先于派生类,所以执行完Foo的最后一行代码,派生类仍然没有构造完成,如果在基类最后一行传递this指针,依然会出现上述问题。

class Observer{};
class Observable
{
public:
    void register_(Observer * s)
    {}
};
class Foo: public Observer
{
public:
    Foo();   //构造中不做任何回调函数和this指针的传递
    virtual void update();
    //注册另一个函数,构造完成后执行回调的注册工作
    void observer(Observable* s)
    {
        s->register_(this);
    }
};
​
int main()
{
    Foo *pFoo = new Foo;
    Observable* s = new Observable;
    pFoo->observer(s);  //二段式构造
}

​ 在需要回调函数或者传递指针时候,我们可以采用二段式构造,即构造函数+initialize()。还有助于构建noexcept的构造函数,通过Initialize()的返回值来判断对象的构造是否成功。

5.销毁太难

​ 对象的析构会导致成员函数用来保护临界区的互斥器析构掉,这样就不能保证互斥器在成员函数本身是有效的了,更别谈线程安全问题了。

Foo::~Foo()
{
    MutexLockGuard lock(mutex_);    //(1)        
}
​
void Foo::update()
{
    MutexLockGuard lock(mutex_);    //(2)
}

​ 假设A、B两个线程都可以看到Foo对象x,线程A即将销毁x,而线程B正准备调用x->update();

//thread A
delet x;
x = NULL;
​
​
//thread B
if (x)
{
    x->update();
}

​ 假设此时

​ A执行到了析构函数的(1)处,已经持有互斥锁,即将继续往下执行。

​ B通过了if (x) 检测,堵塞在(2)处。

​ 此次析构函数会把mutex_销毁掉,那么(2)将会永远堵塞下去,或者有可能进入临界区,然后core dump。所以作为数据成员的MutexLock只能同步本class和其他数据成员的读写,并不能保护安全的析构,因为它的生命周期最多和对象一样长,而析构发生在对象身亡之后。另一方面,当同时读写两个class的对象时候,可能出现死锁的可能性。

void swap(Counter &a, Counter &b)
{
    MutexLockGuard alock(a.mutex_);
    MutexLockGuard block(b.mutex_);
    int64_t value = a.value_;
    a.value_ = b.value_;
    b.value_ = a.value_;
}

​ 如果此时A线程调用swap(a,b),于此同时B线程调用swap(b,a)。就有可能出现A线程中对a加锁了,B线程中对b加锁了,从而出现死锁。

6.原始指针不足

​ 一个动态创建的对象是否活着,单单依靠指针是看不出出来的。指针就是指向了一块内存,这块内存上的对象如果已经销毁了,就不能访问了,不能访问又怎么知道对象此时的状态呢?

空悬指针

​ 当有两个指针p1、p2同时指向堆上同一个对象Object,如果通过p1指针将Object对象销毁了(尽管把p1置为nullptr),那p2就成了空悬指针。

How to handle it

​ 我们可以引入另外的一层间接性。用对象来管理共享资源,即handle/body惯用技法(idioom)。

7.神器shared_ptr/weak_ptr

  • shared_ptr控制对象生命周期,shared_ptr是强引用,只要有一个指向x对象打shared_ptr存在。该x对象就不会析构。当指向对象x的最后一个shared_ptr析构或者reset()的时候,x保证会被销毁。

  • weak_ptr不控制对象的生命周期,但它知道对象是否还活着。如果还活着,那么它可以提升为一个有效的shared_ptr;如果对象已经死了,提升会失败,返回一个空的shared_ptr。“提升/lock()”行为是线程安全的。

  • shared_ptr/weak_ptr的计数在主流平台上是原子操作,没有用锁,性能不俗。

  • shared_ptr/weak_ptr的线程安全级别与std::string和stl容器一样。

  • weak_ptr 还可以用来避免 shared_ptr的循环引用。

class Observable
{
public:
    void register_(weak_ptr<Observer> s){}
    void notifyObservers();
private:
    mutable MutexLock mutex_;
    std::vector< weak_ptr<Observer> > Observers_;
    typedef std::vector< weak_ptr<Observer> >::iterator iterator;
};
​
void Observable::notifyObservers()
{
    MutexLockGuard lock(mutex_);
    iterator it = Observers_.begin();
    while (it != Observers_.end())
    {
        shared_ptr<Observer> obj(it->lock());    //尝试从weak_ptr提升为shared_ptr
        if(obj)
        {
            obj->update();
            ++it;
        }
        else
        {
            it = observers_.erase(it);
        }
    }
}

在上面的代码解决了obverser模式部分线程安全。

  • observable持有observer的weak_ptr,可以判断observer是否活着,并且不会限制observer的生命期;

  • notifyObservers()中的shared_ptr是从weak_ptr,所以是线程安全的。

下面引用自陈硕的文章,原文链接https://blog.csdn.net/solstice/article/details/8547547

(shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。根据文档(http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm#ThreadSafety), shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

• 一个 shared_ptr 对象实体可被多个线程同时读取(文档例1);

• 两个 shared_ptr 对象实体可以被两个线程同时写入(例2),“析构”算写操作;

• 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁(例3~5)。

请注意,以上是 shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。

后文(p.18)则介绍如何高效地加锁解锁。本文则具体分析一下为什么“因为 shared_ptr 有两个数据成员,读写操作不能原子化”使得多线程读写同一个 shared_ptr 对象需要加锁。这个在我看来显而易见的结论似乎也有人抱有疑问,那将导致灾难性的后果,值得我写这篇文章。本文以 boost::shared_ptr 为例,与 std::shared_ptr 可能略有区别。

shared_ptr 的数据结构

shared_ptr 是引用计数型(reference counting)智能指针,几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法(除此之外理论上还有用循环链表的办法,不过没有实例)。具体来说,shared_ptr<Foo> 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针(其类型不一定是原始指针,有可能是 class 类型,但不影响这里的讨论),指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。

图 1:shared_ptr 的数据结构。

为了简化并突出重点,后文只画出 use_count 的值:

以上是 shared_ptr<Foo> x(new Foo); 对应的内存数据结构。

如果再执行 shared_ptr<Foo> y = x; 那么对应的数据结构如下。

但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。

中间步骤 1,复制 ptr 指针:

中间步骤 2,复制 ref_count 指针,导致引用计数加 1:

步骤1和步骤2的先后顺序跟实现相关(因此步骤 2 里没有画出 y.ptr 的指向),我见过的都是先1后2。

既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。

多线程无保护读写 shared_ptr 可能出现的 race condition

考虑一个简单的场景,有 3 个 shared_ptr<Foo> 对象 x、g、n:

  • shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr

  • shared_ptr<Foo> x; // 线程 A 的局部变量

  • shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量

一开始,各安其事。

线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。

同时编程 B 执行 g = n; (即 write g),两个步骤一起完成了。

先是步骤 1:

再是步骤 2:

这是 Foo1 对象已经销毁,x.ptr 成了空悬指针!

最后回到线程 A,完成步骤 2:

多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。

当然,race condition 远不止这一种,其他线程交织(interweaving)有可能会造成其他错误。

 

shared_ptr除了读写不是线程安全之外,下面这些问题也需要我们注意

  • 意外延迟对象的生命周期

  • 函数参数(拷贝等操作额外的开销)

  • 析构动作在创建时被捕获

  • 析构所在的线程

8.小论C++11的unique_ptr

std::unique_ptr 是通过指针占有并管理另一对象,并在 unique_ptr 离开作用域时释放该对象的智能指针。在下列两者之一发生时用关联的删除器释放对象:

  • 销毁了管理的 unique_ptr 对象

  • 通过 operator= 或 reset() 赋值另一指针给管理的 unique_ptr 对象。

只有非 const 的 unique_ptr 能转移被管理对象的所有权给另一unique_ptr 。若对象的生存期为 const std::unique_ptr 所管理,则它被限定在创建指针的作用域中。

9.如何实现一个线程安全的对象池

​ 假设有stock类,代表一个股票的价格,每个股票有唯一的字符串标识,比如Google的key是“NASDAQ:GOOG”。Stock对象是个主动对象,它能不断获取新的价格,如果多处用同一只股票,那么Stock对象应该被共享。如果某一只股票没有再任何地方用到,其对应的Stock对象应该析构,以释放资源,这个隐含了“引用技术”;

class StockFactory: boost::noncopyable
{
public:
    shared_ptr<Stock> get(const string &key);
private:
    mutable MutexLock mutex_;
    std::map<string, weak_ptr<Stock> > stocks_;
};
​
​
shared_ptr<Stock> StockFactory::get(const string &key)
{
    shared_ptr<Stock> pStock;
    MutexLockGuard lock(mutex_);
    weak_ptr<Stock>& wkStock = stocks_[key];  //如果key不存在会默认的构造一个
    pStock = wkStock.lock();
    if (!pStock)
    {
        pStock.reset(new Stock(key));
        wkStock = pStock;
    }
    return pStock;
}

​ 虽然我们使用了weak_ptr对Stock进行了了弱引用,每个Stock可以正常的销毁,但是程序依然出现了内存泄露。你可以看到在这个程序中,Stocks的大小只增加但是不会减少,每有一个Stock,它便会创建一个键值对来保存,但是当一个Stock对象死去后(Stock对象并不能确定map是否还活着),却没有在map中除去这个键值对,如果我们在每个Stock中保存一个Stocks的弱引用,这个问题或许能过解决,但是我们有如何能在这个对象什么时候死去的时候在Stocks中去除Stock?修改析构函数吗?

​ 通过shared_ptr的定制析构的功能,shared_ptr的构造函数可以有一个额外的模板参数,传入一个函数指针或者仿函数d,在析构对象时执行。

class StockFactory: boost::noncopyable
{
public:
    shared_ptr<Stock> get(const string &key);
private:
    mutable MutexLock mutex_;
    std::map<string, weak_ptr<Stock> > stocks_;
    void deleteStock(Stock *Stock)
    {
        if (Stock)
        {
            MutexLockGuard lock(mutex_);   //此处有race condition
            stocks_.erase(stock->key());
        }
        delete Stock;
    }
};
​
​
shared_ptr<Stock> StockFactory::get(const string &key)
{
    shared_ptr<Stock> pStock;
    MutexLockGuard lock(mutex_);  
    weak_ptr<Stock>& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
        pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, this, _1));
        wkStock = pStock;
    }
    return pStock;
}

​ Race condition 发生在 StockFactory::deleteStock() 这个成员函数里,如果进入 deleteStock 之后,在 lock 之前,有别的线程调用了相同 key 的 StockFactory::get(),会造成此 key 被从 stocks_ 哈希表中错误地删除,因此会重复创建 Stock 对象。程序不会 crash 也不会有 memory leak,但是程序中存在两个相同 key 的 Stock 对象,违背了对象池应有的语意。

​ 修复这个 race condition 的办法很简单,在 deleteStock() 中,拿到 lock 之后,检查一下 weak_ptr 是否 expired(),然后只在 expired() 为 true 的情况下从 stocks_ 中删掉 key。

void deleteStock(Stock* stock)
  {
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      auto it = stocks_.find(stock->key());
      assert(it != stocks_.end());
      if (it->second.expired())
      {
        stocks_.erase(it);
      }
    }
    delete stock;
  }

​ 但是此时我们保存了一个StockFactory this的指针保存在boost::function中,这里会有线程安全问题。

enable_shared_from_this

​ 在这里我们遇到了一个难题,我们需要一个shared_ptr/weak_ptr来解决生命周期问题。但是get()本身是一个成员函数,如何获得指向当前对象的shared_ptr<StockFactory>对象呢?

​ 我们可以采用‘’。这是一个以其派生类为模板类型的实参的基类模板,继承它,this指针就能变身成为shared_ptr。

class StockFactory: public std::enable_shared_from_this<StockFactory>,
                    boost::noncopyable
{
    .......
};
​
​
shared_ptr<Stock> StockFactory::get(const string &key)
{
        ...
        pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock,shared_from_this(), _1));
        ....
}

boost::function中存有shared_ptr<StockFactory>,所以可以保证调用deleteStock时候StockFactory还活着,这样我们就解决了时间周期的问题,但是还有一个问题就是StockFactory的周期似乎被延迟了。这个时候,我们就需要将shared_ptr转型成weak_ptr,然后在回调的时候尝试提升为shared_ptr,确定对象是否存活后,在执行回调。

class StockFactory: public std::enable_shared_from_this<StockFactory>,
                    boost::noncopyable
{
public:
    shared_ptr<Stock> get(const string &key)
    {
    shared_ptr<Stock> pStock;
    MutexLockGuard lock(mutex_);
    weak_ptr<Stock>& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
        pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock,weak_ptr<StockFactory>(shared_from_this()), _1));//当转型成为weak_ptr就会不在延迟生命周期
        wkStock = pStock;
    }
    return pStock;
}
private:
    static void deleteStock(const weak_ptr<StockFactory>& wkFactory, Stock*stock)
    {
        shared_ptr<StockFactory> factroy(wkFactory.lock());
        //如果factroy还存在就进行删除工作
        if (factroy)
        {
            factroy->removeStock(stock);
        }
        delete stock;
    }
    void removeStock(Stock *stock)
    {
        if (stock)
        {
            MutexLockGuard lock(mutex_);
            stocks_.erase(stock->key());
        }
    }
private:
    mutable MutexLock mutex_;
    std::map<string, weak_ptr<Stock> > stocks_;
};
​

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值