从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

目录
打赏
0
0
0
0
47
分享
相关文章
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
1月前
|
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
64 20
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
3月前
|
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
94 1
|
3月前
|
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
69 6
|
4月前
|
java线程接口
Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。 Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。 通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。
58 1
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
196 11
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
74 4
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
174 0
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
140 0