博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

<<Linux多线程服务器端编程>> 第一章

Posted on 2016-03-23 18:19  bw_0927  阅读(171)  评论(0)    收藏  举报

http://www.zyfforlinux.cc/2015/02/04/linux%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E7%BC%96%E7%A8%8B-%E7%AC%AC%E4%B8%80%E7%AB%A0/

 

第一章

1.1当析构函数遇到多线程

当一个对象被多个线程同时看到,那么对象的销毁时机就变得模糊了。
下面是可能出现的竞态条件:

  • 析构一个对象的时候,如何得知其他线程正在执行该对象的成员函数?
  • 如何保证执行成员函数期间,对象不会在另外一个线程中被析构?
  • 在调用某个对象的成员函数之前,如何得知这个对象没有被析构? 它的析构函数不会碰巧执行到一半?
    解决这些竞态条件最好的办法是使用shared_ptr 智能指针

1.1.1 线程安全的定义

  • 多线程同时访问时,其表现出正确的行为
  • 无论操作系统如何调度这些线程,无论这些线程执行顺序如何交织
  • 调用端代码无序额外的同步或其他协调动作
    C++标准库中大多数class都不是线程安全的,包括std::string std::vector std::map等,因为这些class通常需要在
    外部加锁才能提供多个线程同时访问。

1.1.2 MutexLock和MutexLockGuard

RAII手法(resource acquisition is initialization)(资源获取就是初始化)
使用局部对象管理资源的技术通常称为“资源获取就是初始化”。这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用。
用白话说就是:在类的构造函数中分配资源,在析构函数中释放资源。这样,当一个对象创建的时候,
构造函数会自动地被调用;而当这个对象被释放的时候,析构函数也会被自动调用。于是乎,一个对象的生命期结束后将会不再占用资源,资源的使用是安全可靠的。
RAII下,不仅仅资源安全,也是异常安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 一般作为其它类的数据成员
class MutexLock
{
public:
MutexLock()
{
pthread_mutex_init(&mutex_,NULL);
}

~MutexLock()
{
pthread_mutex_destroy(&mutex_);
}

void Lock()
{
pthread_mutex_lock(&mutex_);
return;
}

void Unlock()
{
pthread_mutex_unlock(&mutex_);
return;
}

//不能拷贝,不能构造,可以通过继承boost库中的nocopyable类实现
private:
MutexLock(const MutexLock&);
MutexLock& operator=(const MutexLock&);
pthread_mutex_t mutex_;
};


//RAII手法 将lock和unlock封装在构造和析构函数中

class MutexLockGuard
{
public:
explicit MutexLockGuard(MutexLock &mutex):mutex_(mutex)
{
mutex_.Lock();
}

~MutexLockGuard()
{
mutex_.Unlock();
}

private:
MutexLockGuard(const MutexLockGuard&);
MutexLockGuard& operator=(const MutexLockGuard&);
MutexLock &mutex_;
};

1.2 对象的构造很简单

对象的构造做到线程安全要求在构造器件不暴露this指针:

  • 不要在构造函数中注册任何回调函数
  • 不要在构造函数中将this指针传递给跨线程的对象
1
2
3
4
5
6
7
8
9
10
//错误的做法
class Foo:public Observer
{
public:
Foo(Observer*s)
{
s->register_(this); //错误的做法,非线程安全
}
virtual void update();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//正确的做法
class Foo:public Observer
{
public:
Foo();
virtual void update();
void observer(Observer *s)
{
s->register_(this);
}
};
Foo *pFoo = new Foo();
Observer *s = getSubject();
pFoo->observer(s); //二段式构造 线程安全,线程不会访问到一个还没有构造完成的对象

相对来说对象的构造做到线程安全还是比较容易的。

1.3 销毁太难

1.3.1 mutex不是办法

当一个线程正在delete一个对象互斥的释放资源,另一个线程正在调用这个对象的一个方法互斥进入临界区
如果delete先执行了,那么互斥变量就被销毁了,那么另外一个线程可能永远阻塞,也有可能进入临界区,也有可能发生其它更糟糕的情况

1.4 线程安全的Observer有多难

对象(x和y)的关系有三种:composition(组合/复合) aggregation(聚集) association(关联)
compostion(x是y的成员):这个关系在多线程里不会遇到什么麻烦,因为对象x的生命期由y来控制,y析构的时候会把x析构掉。
association(y引用了x,y持有x的指针或者是引用)
aggregation和association相同但是association有逻辑上的整体和部分关系其它没有什么不同
在对象的后两种关系中C++很难察觉到对象是否存活,一种简单的办法就死使用对象池,对象使用后不再释放这样就可以避免访问对象失效了
但是对象池同样也存在很多问题,通过加锁来解决这些竞态条件似乎不太好加锁,最好的解决办法就是提供一个isAlive()之类的函数来告诉
对象还存不存在(可惜指针和引用都不是对象)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Observer设计模式
#include <boost/utility.hpp>
#include <list>
#include <iostream>


class Observer:boost::noncopyable
{
public:
virtual ~Observer(){};
virtual void update() = 0;
};

class Observable:boost::noncopyable
{
public:
void register_(Observer *x);
void unregister(Observer *x);
void notifyObserver(){
for(Observer *x : Observer_){
x->update();
}
}
private:
std::list<Observer*> Observer_;
};

1.5原始指针有何不妥

什么是空悬指针? p1 和 p2指向堆上的一个对象object,p1和p2分别位于不同的线程A和B中,此时线程A释放了对象object,并把p1置为NULL(避免重复释放)
此时的p2就是空悬指针

解决办法一: 引入中间层,p1和p2指向这个中间层,让这个中间层永久有效,这个中间层指向object,当object释放后设置中间层对象的值为0,p1和p2可以通过这个
值来判断对象是否存活,存在一个问题中间层何时释放
解决办法二: 引用计数器,当引用计数器的值为0就释放中间层对象(传说中的引用计数型智能指针)
解决办法三:万能解决方案C++11提供的shared_ptr和weak_ptr

1.6神器 shared_ptr/weak_ptr

  • shared_ptr是强引用,只要有一个指向x对象的shared_ptr存在,x对象就不会被析构。
  • weak_ptr 不控制对象的生命期,但是可以知道对象是否存活,如果对象存活则可以提升位shared_ptr,提升失败返回空的shared_ptr
  • shared_ptr/weak_ptr是保证基本的线程安全,安全级别和std:string和STL容器一样。

1.7系统的避免各种指针错误

内存问题:

  • 缓冲区溢出问题
    用std::vector/std::string或自己编写的Buffer class来管理缓冲区,自动记住缓冲区的长度,通过成员函数而不是指针来修改缓冲区
  • 空悬指针/野指针
    用shared_ptr/weak_ptr
  • 重复释放
    用scoped_ptr只在对象析构的时候释放一次
  • 内存泄漏
    用scoped_ptr对象析构的时候自动释放内存
  • 不配对的new[]/delete
    把new[]替换为std::vector/scoped_array
  • 内存碎片  

1.8使用神器的Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Observable:boost::noncopyable
{
public:
void register_(weak_ptr<Observer> x);
void unregister(weak_ptr<Observer> x);
void notifyObserver();
private:
mutable MutexLock mutex_;
std::vector<weak_ptr<Observer>> Observer_;
};

void Observable::notifyObserver()
{
MutexLockGuard lock(mutex_);
std::vector<weak_ptr<Observer>>::iterator it = Observer_.begin();
while(it != Observer_.end()){
shared_ptr<Observer> obj(it->lock()); //进行提升
if(obj) //提升成功就说明对象存在,进行更新
{
obj->update();
++it;
}
else //提升失败则从vector中去除
{
it = Observer_.erase(it);
}
}
}

依然存在的问题:

  • Observer必须由shared_ptr管理
  • 不是完全线程安全的,Observer的析构函数会调用subject->unregister(this),如果subject不存在了就调用失败了,
    为了解决这个问题就要求Observable本事是shared_ptr管理的.
  • 锁争用 Observable的rigister unregister notifyObserver都需要使用互斥器,但是互斥器只有一个,并且后者执行的时候是无上限的

1.9shared_ptr的线程安全

shared_ptr本身的线程安全级别是和内建类型,标准容器,std::string一样的

  • 一个shared_ptr对象实体可被多个线程同时读取
  • 两个shared_ptr对象实体可以被两个线程同时写入
  • 如果多个线程读写同一个shared_ptr对象那么需要加锁
    多个线程访问shared_ptr 正确的做法使用mutex保护

1.10shared_ptr技术和陷阱

    • 容器中放着shared_ptr的对象,对象的析构函数中从容器中去除shared_ptr,这导致了shared_ptr永远无法释放,出发手动从容器中去除
    • shared_ptr的拷贝开销比原始指针要高,多数情况下应该使用const reference方式传递
    • 对象的析构是同步的,当最后一个指向x的shared_ptr离开作用域释放后,x会同时在同一个线程析构,这个线程不一定是对象诞生的线程
      这是一把双刃剑,如果对象的析构比较耗时那么就会拖垮关键线程,这个时候可以使用一个专门的线程来析构对象,通过一个BlockingQueue
      把对象的析构转移到那个专用线程。