Linux多线程服务端编程笔记 第一章

本文解决如下几个问题:

  1. 如何实现一个线程安全的容器,以及这个线程安全的容器什么时候是不安全的;
  2. 构造函数中,为保证线程安全禁止做哪些事情。
  3. 析构函数中不宜使用锁的原因。
  4. 使用指针时该如何判断指针是否还存活?
  5. 使用锁会降低程序的效率,使得并行的程序串行化,如何减少锁争用造成的延迟。
  6. shared_ptr的使用技巧与坑;
  7. 对象池中对象关系的探讨:如何降低对象之间的相互依赖。
  8. std::bind与std::function的简单理解。

1. stl中的容器大部分都不是线程安全的,如何将其变为线程安全呢?

解决方案:使用锁保护数据成员:

class Array {
private:
  mutable std::mutex lock;        //注意mutable关键字;
  std::vector data;          
}

成员函数的实现使用锁保护data成员。

问题:C++中指针访问Array时是不能保证线程安全的。
理由:C++的析构函数的存在,使得其他线程delete掉Array*时,其他线程还阻塞在lock中,析构函数完成,lock就不存在了,阻塞在lock的线程就出现了未定义行为。

2. 构造函数中,为保证线程安全禁止做哪些事情。

  1. 不能在构造函数中注册回调。
  2. 不要在构造函数中把this传递给跨线程对象;
  3. 即使在构造函数最后一行也不行。

解释:

  1. 在构造函数中注册回调的含义是:将自己的指针保存到容器Observable中,一旦有事件发生,就会调用自己的方法。
Foo(Observable* s) {
  s->register(this);
}

this传递出去后,会导致有可能当前对象没有构造完成就调用了成员方法,未定义行为。

  1. 与上一点相同。感觉含义很类似。
  2. 最后一行也不行是因为,当前类可能是父类,子类的对象依然没有初始化完成,导致未定义行为。

3. 析构函数中不宜使用锁的原因。

(1)调用析构函数的时候,正常逻辑来说这个对象已经没有其他线程在使用了,用锁也没有效果;
(2)即使使用了锁,析构函数抢到了锁,其他线程还在等待这个锁,析构函数中锁被析构掉了,其他线程就是未定义行为。

4. 使用指针时该如何判断指针是否还存活?

例子:以观察者模式为例,observer对象注册自己到Observable,后者保存有前者的指针,一旦某个事件发生,Observable就通过observer指针调用其成员方法。多线程情况下,Observable无法得知当前调用的observer指针是否还有效,即使使用锁也不行。

方案:需要一种方法能告诉Observable,observer指针是否存活的方法。什么都不做是不可能实现的,需要额外一个变量来表示变量是否存活,可以理解为是指针通过一个代理来访问实际内存,代理掌握了实际内存是否被释放的消息。
实现:本质就是shared_ptr与weak_ptr的实现。如果使用weak_ptr保存指针,可以清楚的知道指针是否存活:如果weak_ptr可以转化为shared_ptr,证明指针还有效,否则无效。

weak_ptr不增加引用计数,weak_ptr对象只能由shared_ptr/weak_ptr赋值构造而来。

不打算决定对象的生死,就使用weak_ptr管理对象指针;否则使用shared_ptr

vector> x;
lock.lock();
for(auto xx : x) {
  shared_ptr obj(xx->lock());
  if(obj) {
    obj->update();
  }
  else {
        x.erase(xx);
  }

继续:即使能判断指针是否存活,即不会存在使用已经销毁或者正在销毁的指针了!但是不代表没有其他问题:
(1)锁争用造成的延时;
(2)死锁。

5. 使用锁会降低程序的效率,使得并行的程序串行化,如何减少锁争用造成的延迟。

锁争用:访问需要加锁的数据成员的代码都需要加锁,使得比较简单的函数也需要等待较长时间。
解决1:解决锁争用的方法是:尽量减少临界区的大小;
解决方法1:local copy的方式。(适用于拷贝代价不大的对象

读操作,临界区内拷贝出来,临界区外使用副本读取;

写操作,临界区外定义副本,完成要完成的操作,临界区内直接赋值或者swap;

写操作,临界区内拷贝出来,临界区外副本操作,临界区内swap;这样会有问题,可能会覆盖其他线程的修改。
写操作如果操作的是shared_ptr,可能会造成shared_ptr保存对象的析构操作(原来shared_ptr对象被赋值了,且引用计数为1),此时析构操作也是在临界区内。

写操作时析构移除临界区:临界区外定义副本,完成要完成的操作,临界区内swap;(此时析构移到了临时对象的身上)

6. shared_ptr的使用技巧与坑

坑:

  1. shared_ptr会延长对象的生命周期。
    解释:某些函数实参采用非引用类型shared_ptr类型,调用这个函数的时候就会发生shared_ptr的拷贝操作,使得对象指针的引用计数值变大。如果这个函数返回一个对象,这个对象也中也存在这个shared_ptr的指针,那么shared_ptr对象的声明周期就被延长了。

例子:std::bind函数,基本作用是,为一个函数指针提供默认参数。其实参就会被拷贝一份出来。(模板参数,不论什么类型都会发生拷贝行为)

  1. shared_ptr的拷贝代价比指针要大。
    解释:毕竟还要保存引用计数等变量,修改引用计数等行为。(建议使用引用传递)

  2. 不能同时使用两个shared_ptr,容易引起误会;
    类内(成员函数)使用shared_ptr与类外使用shared_ptr同时使用时,会造成析构两次的问题。
    解释:类内部使用share_ptr的需求可以使用:shared_from_this() 代替this;

使用技巧:

  1. 作为函数参数时,建议使用const reference传递;
  2. 在创建shared_ptr对象时,可以手动指定析构函数,这样可以保证可以跨dll来删除。
    解释:windows下的进程会有好几个堆,每个dll都会有一个堆,一个堆里申请的需要在这个堆释放,所以存在跨模块释放的问题;shared_ptr通过指定析构函数,使得释放时,可以释放对应堆的对象。
  3. shared_ptr的析构如果可能发生在关键进程,可以用一个专门的线程来处理析构,使用BlockQueue来转移对象到析构线程;
  4. ower持有指向child的shared_ptr,child持有指向ower的weak_ptr;
    解释:ower可以决定对象的生死,child只负责使用,不符合对象的生死。

7. 对象池中对象关系的探讨:如何降低对象之间的相互依赖。

场景:A类中包含了B类对象,对象池的话就是A类中包含了很多B类对象。B类对象可以是暂存在A类中的,用于回调;也可能是被A类所使用。

(1) 需求:A类中的B类对象如果不使用了及时释放掉,以节省内存。

解决方案:不使用了的概念是没有线程在使用了,可以使用指针来保存B类对象,shared_ptr来保存,使得引用计数为0时就释放掉。显然,使用shared_ptr的话,对象永远不会被释放掉。所以使用weak_ptr来保存。

class Item {};
class Factory {
private:
  std::map> data_;
  mutable std::mutex lock_;
public:
shared_ptr  get(const std::string& key);    //使用shared_ptr作为返回值,因为出去使用的对象认为不能随便释放掉。
};

get方法的实现就相当较为简单了:

shared_ptr Factory::get(const string& key) {
  shared_ptr ret;
  lock_.lock();
  auto itemptr = data_[key].lock();          //即使不存在,itemptr也是合理的weak_ptr
  if(! itemptr) {
      ret.reset(new Item());
      data_[key] = ret;                 //第一,weak_ptr只能由shared_ptr/weak_ptr赋值而来。 第二,两次查找map,效率不高,可以使用引用保存第一次查找的结果。       
}
  lock_.unlock();
  return ret;
}

shared_ptr与weak_ptr使得对象可以被及时释放。

(2)需求:资源的及时释放,保存有weak_ptr的对象也要及时清理掉内存,如何处理。

创建了一个Item给外部使用,保存在data_中以便不要重复创建;但是外部用完了,对象就自动销毁了,但是Factory还保存着资源的weak_ptr,没有意义了。要清理掉。
即:对象的析构不仅仅需要释放自己,还要处理保存有自己weak_ptr的对象

解决方案:使用shared_ptr定制的析构函数来处理。

void deleteItem(Item* item, Factory* factory) {
  factory->deleteItem(item->key());        //key是从Item类中获取。
  delete item;
}
(3)问题:定制析构需要只能有一个参数,且参数应该是传到shared_ptr中的对象指针,那现在要处理factory,多了一个参数,要咋处理呢?

解决方案:std::bind函数缩减函数参数

auto deleter = std::bind(deleteItem, _1, this);        //普通函数作为第一个参数,不需要使用&,静态成员函数与成员函数使用时需要。

这里的this只是一个例子,代表是Factory* 就可以了,因为前面shared_ptr在Factory中构造而来,所以使用this。

(4)指针是不能随便出现的,出现了就会存在内存释放问题,也存在指针是否是野指针的问题。上面的deleteItem函数不合理。

解决:由于在函数内部无法判断factory指向的对象是否还存在,所以不能直接调用。上面第四点说明了判断指针是否存在可以使用shared_ptr与weak_ptr来决定。

至于用哪个,得看Factory*是不是在这里必须存在,显然,这里只是清理Factory内部数据,如果Factory对象不在了,就不清理就好了。所以使用weak_ptr;

所以:

void deleteItem(Item* pItem, weak_ptr pFactory) {
  share_ptr pFactoryShare = pFactory.lock();
  if(pFactoryShare) {
    pFactoryShare->deleteItem(pItem->getKey());    //pFactoryShare指向的对象一定存在。
  }
  delete pItem;
}
(5)如何将this变为shared_ptr或者weak_ptr,以方便在类成员函数内部调用shared_ptr为参数的函数?

上面第6点说明了,内部不可以直接使用shared_ptr,以防止外部也使用shared_ptr,造成两次析构的出现。

所以可以使用如下方式来实现:(思路固定。背下来就好)

class Factory : public std::enable_shared_from_this {    //必须继承这个类;
     //类内部需要使用shared_ptr的地方,使用shared_from_this()来代替。
     //类内部需要使用weak_ptr的地方,使用std::weak_ptr(shared_from_this())就好了(就是使用shared_from_this()来生成了下weak_ptr())
};

通过相互使用weak_ptr,Item与Factory谁也不管谁,谁挂了也无所谓。

8. std::bind与std::function的简单理解。

使用起来比较简单,不再介绍,只介绍简单的理解。

std::function可以理解为可以统一函数格式,可以为函数重新命令。什么普通函数,类静态函数,类成员函数,经过function都变成了普通函数的格式,随便调用。(类成员函数的调用需要加上类对象指针)

std::bind不仅可以实现std::function函数的作用,还可以实现减少参数数量,添加默认参数等功能。
std::bind一个很常见的使用方式:为类静态函数和成员函数指定别名,简化类静态函数的调用方式。为成员函数提前加好类对象指针在第一个参数,后边再调用这个函数的时候,直接和调用类成员函数一样了。

你可能感兴趣的:(Linux多线程服务端编程笔记 第一章)