Linux多线程服务器编程笔记-1

C++中实现线程安全的对象创建、回调与析构

  • 写出线程安全的并不难,使用同步原语保护内部状态即可
  • STL大多类都不是线程安全的,需要在外部加锁保证多个线程同时访问

安全的对象创建

唯一的要求就是不要在构造期间泄露this指针,即

- 不要在构造函数中注册任何回调
- 不要把this指针传给跨线程的对象
- 即使在最后一行也不可以,因为这个类可能是基类,它的构造函数最后一行不等于构造完成

原因:在执行构造函数期间对象没有完成初始化,如果this指针被写了泄露给了其他对象,那么别的线程可能会访问这个半成品

安全的对象销毁

对象析构函数的竞态条件

1.析构某个对象时,如何得知是否有别人在用它?
2.析构某个对象时,它是否可能正在被另一个线程析构?
3.调用某个对象的成员函数时,如何保证它依旧活着?析构函数会不会碰巧执行到一半?

对象析构函数的设计难点

1.mutex不是办法,因为锁只是用来保护对象内部数据的,不能来保护对象本身,不能解决任何问题

2.面向对象中广泛使用的三种对象关系,都依赖于对象知道“其他对象是否还活着”的能力

a. composition
b. aggregation
c. association

其中,composition在多线程中不会有什么麻烦,因为对象x的生命周期由其owner唯一控制,但后两者,对象的联系是借由指针或引用实现的,因此可能会出现上面提到的竞态条件。

解决办法 :shared_ptr / weak_ptr

即在对象间中引入一层中间层,不再使用裸指针,而是智能指针,作为一个辅助的管理者(引用计数)。

  • shared_ptr控制对象的生命周期,是强引用,只要有一个指向x对象的shared_ptr存在,x就不会析构,当引用计数降为0时或reset()时,x保证会被销毁。
  • weak_ptr不控制对象生命周期,但知道对象是否或者,如果对象没死,那么可以提升为有效的shared_ptr,从而引用对象,并保证在这个期间对象不析构,如果对象死了,那么会返回空的shared_ptr提升行为是线程安全的。
  • shared_ptrweak_ptr的计数是原子操作,性能不俗

但是

  • shared_ptrweak_ptr本身的线程安全级别与std::string和STL的容器一样,见下面的讨论

关于shared_ptr的讨论

  • shared_ptr并不线程安全:可以多个线程同时读,不能多个线程同时写(析构算写)
  • shared_ptr的线程安全和它指向对象的线程安全是两回事
  • 访问共享的shared_ptr,需要加上互斥锁

    void read(){
      shared_ptr localPtr;
      {
          MutexLockGuard lock(mutex);
          localPtr = globalPtr; // read globalPtr
      }
      // use localPtr since here,读写localPtr无需加锁,因为是栈上对象,只有当前线程可见
      do_it(localPtr); // 这里函数的参数应是reference to const,避免复制
    }
  • weak_ptrshared_ptr用错,可能会导致一些对象永远不被析构,通常做法是owner拥有指向child的shared_ptr,child持有owner的weak_ptr
  • 拷贝开销:因为拷贝会创建一份副本,需要修改引用计数,开销较大,但需要拷贝的地方不多,通常shared_ptr作为参数时都是const reference

RAII 资源获取即初始化

  • C++区别于其他编程语言的最重要的特性
  • 每一个明确的资源配置动作(如new)都应该在单一语句中执行,并在该旅居中立刻将配置获得的资源交给handle对象(如shared_ptr),程序中一般不出现delete

线程安全的对象池

实现一个StcokFactory, 根据key返回Stock对象

版本1: 利用weak_ptrshared_ptr管理

class StockFactory : boost::noncopyable
{
 public:
  boost::shared_ptr get(const string& key)
  {
    boost::shared_ptr pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr& wkStock = stocks_[key]; // 如果这里是shared_ptr,则对象永远不会被析构
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key));
      wkStock = pStock; // wkStock is a reference 
    }
    return pStock;
  }

 private:
  mutable muduo::MutexLock mutex_;
  std::map > stocks_;
};

问题: 存在轻微的内存泄漏,map中的key:weak_ptr会一直存在,在key有限时没什么问题,在key范围很大时会造成内存泄漏

版本2: 利用shared_ptr的定制析构功能,在析构对象的同时析构

class StockFactory : boost::noncopyable
{
 public:

  boost::shared_ptr get(const string& key)
  {
    boost::shared_ptr pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key),
                   boost::bind(&StockFactory::deleteStock, this, _1)); // 定制析构功能,绑定了一个对象的成员函数
      wkStock = pStock;
    }
    return pStock;
  }

 private:

  void deleteStock(Stock* stock)
  {
    printf("deleteStock[%p]\n", stock);
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      stocks_.erase(stock->key());  // 如果stocks_先于stock死亡,这里会core dump
    }
    delete stock;  // 因为自己定制析构功能,需要手动写delete
  }
  mutable muduo::MutexLock mutex_;
  std::map > stocks_;
};

问题: 如果stocks_先于stock死亡,stock析构,回调stocks_erase函数时会报错,这个问题的本质和上面讨论的问题是一样的,即对象的this指针是不足够判断对象生命周期的,上面的方法里,改用weak_ptr + shared_ptr解决了问题,这里也一样

版本3: 利用 enable_shared_from_this 使用shared_ptr代替this指针

class StockFactory : public boost::enable_shared_from_this,
                     boost::noncopyable
{
 public:

  boost::shared_ptr get(const string& key)
  {
    boost::shared_ptr pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key),
                   boost::bind(&StockFactory::deleteStock,
                               shared_from_this(),
                               _1));
      wkStock = pStock;
    }
    return pStock;
  }

 private:

  void deleteStock(Stock* stock)
  {
    printf("deleteStock[%p]\n", stock);
    if (stock)
    {
      muduo::MutexLockGuard lock(mutex_);
      stocks_.erase(stock->key());  // This is wrong, see removeStock below for correct implementation.
    }
    delete stock; 
  }
  mutable muduo::MutexLock mutex_;
  std::map > stocks_;
};

问题: 因为stock注册了StockFactoryshared_ptr(bind的时候会发生指针的复制),导StockFactory会等到所有stock都析构之后才析构,意外地延长了生命。

版本4: 利用enable_shared_from_this 使用weak_ptr代替this指针,实现弱回调 : "如果对象还活着,就调用它的成员函数,否则忽略之"

class StockFactory : public boost::enable_shared_from_this,
                     boost::noncopyable
{
 public:
  boost::shared_ptr get(const string& key)
  {
    boost::shared_ptr pStock;
    muduo::MutexLockGuard lock(mutex_);
    boost::weak_ptr& wkStock = stocks_[key];
    pStock = wkStock.lock();
    if (!pStock)
    {
      pStock.reset(new Stock(key),
                   boost::bind(&StockFactory::weakDeleteCallback,
                               boost::weak_ptr(shared_from_this()),
                                // 必须有这步转型,才不会延长StockFactory的生命周期
                                // boost:bind拷贝的是实参类型不是形参类型
                               _1));
      wkStock = pStock;
    }
    return pStock;
  }

 private:
  static void weakDeleteCallback(const boost::weak_ptr& wkFactory,
                                 Stock* stock)
  {
    printf("weakDeleteStock[%p]\n", stock);
    boost::shared_ptr factory(wkFactory.lock());
    if (factory)
    {
      factory->removeStock(stock);
    }
    else
    {
      printf("factory died.\n");
    }
    delete stock; 
  }

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

 private:
  mutable muduo::MutexLock mutex_;
  std::map > stocks_;
};

这下无论Stock还是StockFactory谁先析构都不会影响程序正常运行,利用智能指针解决了两个对象相互引用的问题。
当然,通常Factory对象是一个singleton,并且正常运行期间不会销毁,这里只是为了展示弱回调计数。

多线程编程的Tips

1.包装底层的同步原语,借助对象的构造和析构加锁和解锁

int getvalue() const{
    MutexLockGuard lock(mutex_); // 对象的构造即加锁
    return value_; // 读取数据
} // 作用域结束,Guard自动析构,解锁

2.如果一个函数要所著相同类型的两个对象,那么为了保证始终按相同的顺序加锁,可以比较mutex对象的地址始终加锁地址较小的那个,比如:

void swap(counter& a, counter& b){
    MutexLockGuard aLock(a.mutex_);
    MutexLockGuard bLock(b.mutex_);
    // 交换a,b的数据
}

如果线程A执行swap(a,b)而线程B执行swap(b,a)将会发生死锁。

3.虽然本章讲如何安全使用、析构跨线程的对象,但是尽量不要用跨线程的对象,用流水线,生产者消费者,任务队列等有规律的机制,最低限度的共享数据,是比较好的多线程编程方法

你可能感兴趣的:(Linux多线程服务器编程笔记-1)