从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(中)

简介: 从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)

从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(上):https://developer.aliyun.com/article/1522526

1.4 atomic+CAS

       C++11提供了原子操作,我们知道,线程不安全的主要原因就是访问某些公共资源的时候,操作不是原子的,如果让这些操作变成原子的后,就不会存在线程安全问题了。

CAS原理:

原子操作的原理就是CAS(compare and swap)。

  • CAS包含三个操作数:内存位置(V),预期原值(A)和新值(B)。
  • 如果内存位置的值与预期原值相等,那么处理器就会自定将该位置的值更新为新值。
  • 如果内存位置的值与预期原值不相等,那么处理器不会做任何操作。

       val是临界资源,两个线程t1和t2同时对这个值进行加加操作,每个线程都是将该值先拿到寄存器eax中。

  • 线程将val值拿到寄存器eax中时,同时将该值放入原值V中。
  • 在修改val值之前,CPU会先判断eax中的值与原值V中的值是否相等,如果相等则修改并且更新值,如果不相等则不修改。

伪代码原理:

while(1)
{
  eax = val; // 将val值取到寄存器eax中
  if(eax == V) // 和原值相同可以修改
  {
    eax++;
    V = eax; // 修改原值
    val = eax; // 修改val值
    break; // 访问结束,跳出循环
  }
}
  • t1和t2虽然同时运行,但是时间粒度划分到极小的时候,CPU仍然是一个个在执行。

t1线程将val值拿到寄存器中,并且赋原值,经过判断发现和原值相同,所以修改val值,并放回到val的地址中。

此时t2线程被唤醒,它将val值拿到寄存器中后与最开始的原值V相比,发现不相同了,所以就不进行修改,而且继续循环,知道寄存器中的值和原值相等才会改变。

  • 原子操作虽然保证了线程安全,但是另一个无法写的的线程会不停的循环,而这也会占用一定的CPU资源。

CAS具体的原理有兴趣可以自行去了解,深入了解后写在简历是加分项。


atomic也是一个类,所以也有构造函数:

经常使用的是atomic(T val),在创建的时候传入我们想要进行原子操作的变量。

int a = atomic(1);

此时变量a的操作就都成了原子操作了,在多线程访问的时候可以保证线程安全。

成员函数:

该类重载了++,-- 等运算符,可以直接对变量进行操作。

看看没用atomic也没加锁的:

int main()
{
  mutex mtx;
  int x = 0;
  int n = 100000;
  int m = 2;
  vector<thread> v(m);
  for (int i = 0; i < m; ++i)
  {
    // 移动赋值给vector中线程对象
    v[i] = thread([&](){
      for (int i = 0; i < n; ++i)
      {
        ++x;
      }
    });
  }
  for (auto& t : v)
  {
    t.join();
  }
  cout << x << endl;
  return 0;
}

两个线程互相抢着加,就会出现有一个线程没加的情况,看看加锁的:

再看看用atomic的:

和加锁效果一样。

1.5 condition_variable

C++11中同样也有条件变量,用来实现线程的同步。

构造函数:

在创建条件变量的时候不用传入参数,同样是不允许被拷贝的。


其他成员函数:

放入等待队列:

wait(unique_lock& lock),该接口是将调用它的线程放入到条件变量的等待队列中。

wait(unique_lock& lck, Predicate pred),该接口和上面的作用一样,只是多了一个pred参数,当这个参数为true的话不放入等待队列,为false时放入等待队列。


       这里传入的锁是unique_lock而不是lock_guard。这是因为,当一个线程申请到锁进入临界区,但是条件不满足被放入条件变量的等待队列中时,会将申请到的锁释放。


lock_guard只能在对象生命周期结束时自动释放锁。


unique_lock可以在任意位置释放锁。


如果使用了lock_guard的话就无法在进入等待队列的时候释放锁了。


wait_for和wait_until都是等待指定时间,一个是在等待队列中待指定时间,另一个是在等待队列中带到固定的时间点后自定唤醒。


notify_one唤醒等待队列中的一个线程,notify_all唤醒等待队列中的所有线程。

1.6 分别打印奇数和偶数

写一个程序:支持两个线程交替打印,一个打印奇数,一个打印偶数。

分析:

  • 首先创建一个全局的变量val,让两个线程去访问该变量并且进行加一操作。
  • 考虑到线程安全,所以需要给对应的临界区加互斥锁mutex
  • 又是交替打印,所以要使用条件变量condition_variable来控制顺序,为了方便管理,使用的锁是unique_lock

代码实现:

int main()
{
  int val = 0;
  int n = 10; // 打印的范围
  mutex mtx; // 创建互斥锁
  condition_variable cond; // 创建条件变量
 
  thread t1([&](){
    while (val < n)
    {
      unique_lock<mutex> lock(mtx); // 加锁
      while (val % 2 == 0)// 判断是否是偶数
      {
        // 是偶数则放入等待队列中等待
        cond.wait(lock);
      }
      // 是奇数时打印
      cout << "thread1:" << this_thread::get_id() << "->" << val++ << endl;
      cond.notify_one(); // 唤醒等待队列中的一个线程去打印偶数
    }
  });
 
  this_thread::sleep_for(chrono::microseconds(100));
 
  thread t2([&](){
    while (val < n)
    {
      unique_lock<mutex> lock(mtx);
      while (val % 2 == 1)
      {
        cond.wait(lock);
      }
      cout << "thread2:" << this_thread::get_id() << "->" << val++ << endl;
      cond.notify_one();//唤醒等待队列中的一个线程去打印奇数
    }
  });
 
  t1.join();
  t2.join();
 
  return 0;
}

       上面代码两个线程执行的函数对象是lambda表达式,所以创建线程对象时,调用的是移动构造函数。

  • wait()的第二个参数是false的时候,该线程被挂起到等待队列中,是true的时候不挂起,而且执行向下执行。
  • 第二个参数的false和true可以是返回值,如代码就是使用的lambda表达式的返回值。

线程t1负责打印奇数,t2负责打印偶数,两个线程通过条件变量的控制交替打印。

还可以这么用:

int main()
{
  int val = 0;
  int n = 10; // 打印值的范围
  mutex mtx;
  condition_variable cond;
  bool ready = true;
 
  // t1线程打印奇数
  thread t1([&](){
    while (val < n)
    {
      {
        unique_lock<mutex> lock(mtx);
        cond.wait(lock, [&ready](){return !ready; });
        cout << "thread1:" << this_thread::get_id() << "->" << val << endl;
        val += 1;
        ready = true;
        cond.notify_one();
      }
      //this_thread::yield();
      this_thread::sleep_for(chrono::microseconds(10));
    }
  });
 
  // t2线程打印偶数
  thread t2([&]() {
    while (val < n)
    {
      unique_lock<mutex> lock(mtx);
      cond.wait(lock, [&ready](){return ready; });
      cout << "thread2:" << this_thread::get_id() << "->" << val << endl;
      val += 1;
      ready = false;
      cond.notify_one();
    }
  });
 
  t1.join();
  t2.join();
 
  return 0;
}

成功按照预期打印。

从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(下):https://developer.aliyun.com/article/1522544

目录
相关文章
|
3天前
|
设计模式 存储 C++
【C++/STL】:stack/queue的使用及底层剖析&&双端队列&&容器适配器
【C++/STL】:stack/queue的使用及底层剖析&&双端队列&&容器适配器
25 2
|
3天前
|
编译器 C++ 容器
【C++/STL】:list容器的深度剖析及模拟实现
【C++/STL】:list容器的深度剖析及模拟实现
10 2
|
2天前
|
编译器 C语言 C++
C++ STL中list迭代器的实现
C++ STL中list迭代器的实现
C++ STL中list迭代器的实现
|
3天前
|
存储 C++ 容器
【C++/STL】:list容器的基本使用
【C++/STL】:list容器的基本使用
9 1
|
3天前
|
存储 算法 C++
【C++/STL】:vector容器的基本使用
【C++/STL】:vector容器的基本使用
14 1
|
21小时前
|
自然语言处理 C语言 C++
程序与技术分享:C++写一个简单的解析器(分析C语言)
程序与技术分享:C++写一个简单的解析器(分析C语言)
|
1天前
|
C++ 容器
C++ STL标准库 《map容器详解》
C++ STL标准库 《map容器详解》
7 0
|
1天前
|
存储 C++ 容器
C++ STL标准库 《map容器详解》
C++ STL标准库 《map容器详解》
7 0
|
2天前
|
算法 搜索推荐 C++
C++之STL常用算法(遍历、查找、排序、拷贝、替换、算数生成、集合)
C++之STL常用算法(遍历、查找、排序、拷贝、替换、算数生成、集合)
10 0
|
2天前
|
C++
【C++航海王:追寻罗杰的编程之路】STL—next_permutation函数
【C++航海王:追寻罗杰的编程之路】STL—next_permutation函数
4 0