Jason Pan

C++ 智能指针 -- 看 Envoy 如何用 weak_ptr

潘忠显 / 2020-08-13


weak_ptr 的基本用法

weak_ptr 的作用表达临时所有权,即:某个对象只有存在时才需要被访问,而且随时可能被删除。

让我们先看一下 weak_ptr 的构造方式:

constexpr weak_ptr() noexcept; 
weak_ptr (const weak_ptr& x) noexcept; 
template <class U> weak_ptr (const weak_ptr<U>& x) noexcept; 
template <class U> weak_ptr (const shared_ptr<U>& x) noexcept; 

= 操作符与复制构造类似。

从构造函数可以看出,** weak_ptr 只能通过 weak_ptrshared_ptr 进行构造**。

如果要使用 weak_ptr 指向的信息块的管理的对象指针,需要使用 lock() 函数先转成 shared_ptr 。如果管理的对象已经被释放了,lock会返回一个空指针:

std::shared_ptr<T> lock() const noexcept;

除了 lock() 函数之外,还可以使用以下两个函数:

shared_ptr 一文中有提到多线程下 use_count() 只是一个大约值

In multithreaded environment, the value returned by use_count is approximate (typical implementations use a memory_order_relaxed load) 多线程环境下, use_count 返回的值是近似的(典型实现使用 memory_order_relaxed 加载)

weak_ptr 的说明中没有提这一点,实际上是不是准确值呢?

Envoy中的使用 weak_ptr 的例子

Envoy 的 SingletonManagerImpl 的成员函数 get() 中有这样一段代码:

  // using InstanceSharedPtr = std::shared_ptr<Instance>; 
  // std::string name; 
  // SingletonFactoryCb cb; 
  // std::unordered_map<std::string, std::weak_ptr<Instance>> singletons_; 
  if (nullptr == singletons_[name].lock()) {
    InstanceSharedPtr singleton = cb();
    singletons_[name] = singleton;
    return singleton;
  } else {
    return singletons_[name].lock();
  }

上述代码使用一个unordered_map存储了 单例名字--> 单例的weak_ptr 的映射关系。

使用过程:

  1. 如果没有对应名的实例,则使用 shared_ptr 创建一个对象并返回
  2. 否则,返回shared_ptr

作用:

如果产生了单例之后,如果大家的 shared_ptr 都释放了,则会删掉单例,下次再使用的时候,会检查指针是否有指向未被删除的对象,如果没有则需要创建。

有没有发现什么问题?

这里有一个明显的线程安全问题:

  1. 两个线程同时请求时,可能同时调用lock()函数返回为空
  2. 连个线程都去创建实例,并先后赋值到给singletons_[name]
  3. 函数退出,只有一个线程中创建的那个是真正写到管理器的

这样全局就同时存在两个该工厂产生的实例。

不过,Envoy的代码中也有注释描述了这个问题:

It is assumed the singleton manager is only used on the main thread so it is not thread safe. Asserts verify that.

如何保证只能在主线程中使用?

使用 ASSERT ,在get方法中加入了判断,只能是创建SingletonManagerImpl的线程才能使用这一单例管理器:

// run_tid_(thread_factory.currentThreadId())
assert(run_tid_ == thread_factory_.currentThreadId());