从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

目录
相关文章
|
4月前
|
消息中间件 存储 安全
|
5月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
86 1
|
5月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
101 5
|
5月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
180 6
|
5月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
70 0
|
6月前
|
Linux API C++
超级好用的C++实用库之互斥锁
超级好用的C++实用库之互斥锁
50 2
|
7月前
|
监控 安全 Java
Java多线程调试技巧:如何定位和解决线程安全问题
Java多线程调试技巧:如何定位和解决线程安全问题
169 2
|
6月前
|
安全 Java
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
170 0
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
49 17
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
61 26