这一章就是告诉我们,智能指针很重要。(shared_ptr),在实践中几乎每定义一个类都会定义这个类的智能指针。
因为多线程状态下,一个对象可以被多个线程看到,那么对象销毁时机就很模糊,会产生竞态条件:比如
A想要析构对象,但怎么知道有没有别的线程正在执行A的成员函数;A想要执行对象的成员函数,怎么知道它还活着?
所以,shared_ptr是一劳永逸的办法。
对象创建很简单
构造对象做到线程安全,只需要做到构造期间不泄露this指针就可以。(不要再构造函数注册回调,不要把this传给另一个线程)。因为构造期间对象没有完成初始化,如果其他线程访问这个半成品,就会产生不知道的后果。
销毁很难
析构在单线程没有很大问题,主要注意空指针野指针的问题。mutex虽然可以保护临界区,但是析构函数破坏了这一假设,他会把mutex成员变量销毁;
shared_ptr可以实现对象安全析构,但是并不是100%安全的。它的引用计数是原子操作,但是对象的读写并不是。
同一个shared_ptr被多线程读是安全的;同一个shared_ptr被多线程写不安全;共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。shared_ptr其实由指向对象的指针和计数器组成,计数器加减操作是原子操作,所以这部分是线程安全的,但是指向对象的指针不是线程安全的。比如智能指针的赋值拷贝,首先拷贝指向对象的指针,再使引用次数加减操作,虽然引用次数加减是原子操作,但是指针拷贝和引用次数两步操作 并不是原子操作,线程不安全,需要手动加锁解锁。
所以该做的是:多个线程同时访问一个shared_ptr,就应该加mutex保护,不需要加读写锁,互斥锁最简单。因为临界区非常小,用互斥锁不会阻塞并发读。
读写比为 9:1 时,读写锁的性能约为互斥锁的 8 倍。
mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
问题:循环引用:用weak_ptr. owner持有指向孩子的hsared_ptr, 孩子持有指向owner的weak_ptr…
所以实践中,用RAII类来封装资源,不是用new,delete,防止内存泄漏。再在类中定义智能指针,防止重复析构。
另外,尽量减少使用跨线程的对象,用流水线,生产者消费者,任务队列这些有规律的机制,最低限度得共享数据,是本质的好处。