智能指针shared_ptr的线程安全、互斥锁

                                        智能指针和线程安全的问题


<1>  智能指针shared_ptr本身(底层实现原理是引用计数)是线程安全的

智能指针的引用计数在手段上使用了atomic原子操作,只要shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。首先原子是线程安全的,所有智能指针在多线程下引用计数也是安全的,也就是说智能指针在多线程下传递使用时引用计数是不会有线程安全问题的。

<2>  智能指针指向的对象的线程安全问题,智能指针没有做任何保障

  • 遇到的问题

对于智能指针shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象,当智能指针发生拷贝的时候,标准库的实现是先拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子操作,隐患就出现在这里。两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,假设引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了,违背了原子性。

 

  • 下多线程编程中的三个核心概念,可以作为面试中原因分析的讲解

 

        (1)原子性的举例

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

     (2)可见性的举例

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。

CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。

这一点是操作系统或者说是硬件层面的机制,所以很多应用开发人员经常会忽略。

   (3)顺序性举例

顺序性指的是,程序执行的顺序按照代码的先后顺序执行。处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。讲到这里,有人要着急了——什么,CPU不按照我的代码顺序执行代码,那怎么保证得到我们想要的效果呢?实际上,大家大可放心,CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。

  • 解决办法——加入互斥锁

使用互斥锁对多线程读写同一个shared_ptr进行加锁操作(多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁))

一旦一个线程获得了锁对象,那么在临界区时一直是受保护的,具体表现为该线程一直占着资源不放。

     临界区的说明

 

         有时我们会遇到两个进/线程共同使用同一个资源的情况,这个资源就称为临界区。临界区是指某一时间只能有一个线程执行的一个代码段

  • 加入互斥锁的代码展示
    • 方法1:直接操作 mutex,即直接调用 mutex 的 lock / unlock 函数

 

#include <iostream>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  mutex.lock();

  int i = ++count;
  std::cout << "count == " << i << std::endl;

  // 前面代码如有异常,unlock 就调不到了。
  mutex.unlock();
}

int main() {
  // 创建一组线程。
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  // 等待所有线程结束。
  threads.join_all();
  return 0;
}
  • 方法2:使用 lock_guard 自动加锁、解锁。原理是 RAII,和智能指针类似

C++利用了一个非常好的特性:当一个对象初始化时自动调用构造函数,当一个对象到达其作用域结尾时,自动调用析构函数。所以我们可以利用这个特性解决锁的维护问题:把锁封装在对象内部!此时,在构造函数时获得锁,在语句返回前自动调用析构函数释放锁。其实这种做法有个专有的名称,叫做RAII

 

#include <iostream>
#include <boost/thread/lock_guard.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  // lock_guard 在构造函数里加锁,在析构函数里解锁。
  boost::lock_guard<boost::mutex> lock(mutex);

  int i = ++count;
  std::cout << "count == " << i << std::endl;
}

int main() {
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  threads.join_all();
  return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章