C++线程

1、 C++11多线程thread

  • 重点
  1. join和detach的使用场景
  2. thread构造函数参数
    1. 绑定c函数
    2. 绑定类函数
  3. 线程封装基础类
  4. 互斥锁mutex
  5. condition notify、wait
  6. lock_guard/unique_lock
  7. function和bind
  8. 异步future/packaged_task/promise
  9. 线程池的实现,线程池涉及的技术点

1.1、线程thread

1.1.1、语法

  • 默认构造函,构造一个线程对象,在这个线程中不执行任何处理动作

    • thread() noexcept;
      
      // 示例
      #include <thread>
      #include <iostream>
      
      int main()
      {
      	std::thread t();
      	return 0;
      }
      
  • 创建线程对象,并在该线程中执行函数f中的业务逻辑,args是要传递给函数f的参数

    • 任务函数f的可选类型有很多,具体如下:

      • 普通函数类成员函数匿名函数仿函数(这些都是可调用对象类型)
      • 可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
    • template< class Function, class... Args >
      explicit thread( Function&& f, Args&&... args );
      
  • 移动构造函数,将 other 的线程所有权转移给新的thread 对象。之后 other 不再表示执行线程。

    • thread( thread&& other ) noexcept;
      
  • 拷贝构造:使用=delete显示删除拷贝构造, 不允许线程对象之间的拷贝

    • thread( const thread& ) = delete;
      
  • **示例:**包含上述几种创建线程的方式

    • #include <thread>
      #include <iostream>
      #include <string>
      
      void func(int i)
      {
      	std::cout << i << std::endl;
      }
      
      // 定义一个仿函数类
      class MyFunctor {
      public:
      	void operator()(int x) {
      		std::cout << "仿函数被调用,参数 x = " << x << std::endl;
      	}
      };
      
      class Person
      {
      public:
      	Person(std::string _name) : name(_name)
      	{
      	}
      	void Print(int age)
      	{
      		std::cout << "name = " << name << std::endl;
      		std::cout << "age = " << age << std::endl;
      		
      	}
      protected:
      	std::string name;
      };
      
      int main()
      {
      	int a = 1;
      	std::thread t1(func, a);  // 普通函数
      
      	Person * p1 = new Person("天天");
      	Person p2("开心");
      	// 类成员函数,注意第一个参数要加取地址(&),不加有些情况会编译报错
      	std::thread t2(&Person::Print, p1, 18);   // p1为指针
      	std::thread t3(&Person::Print, &p2, 20);   // p2为变量
          
          
      	// 匿名函数
      	std::thread t4([](std::thread::id main_thread_id)->void {
      		std::cout << "主函数函数线程ID = " << main_thread_id << std::endl;
      		std::cout << "匿名函数线程ID   = " << std::this_thread::get_id() << std::endl; 
      		}, 
      		std::this_thread::get_id());
      
      	 // 仿函数
      	// MyFunctor functor;
      	std::thread t5(MyFunctor(), 42);
      
      	t1.join();
      	t2.join();
      	t3.join();
      	t4.join();
      	t5.join();
      	delete p1;
      	return 0;
      }
      

1.1.2、公共成员函数

1.1.2.1、get_id()
  • 应用程序启动之后默认只有一个线程,这个线程一般称之为主线程或父线程,通过线程类创建出的线程一般称之为子线程,每个被创建出的线程实例都对应一个线程ID,这个ID是唯一的,可以通过这个ID来区分和识别各个已经存在的线程实例,这个获取线程ID的函数叫做get_id()
  • this_thread::get_id():获取当前线程ID
  • 函数原型如下:
std::thread::id get_id() const noexcept;
  • 示例:

    • #include <iostream>
      #include <thread>
      #include <chrono>
      using namespace std;
      
      void func()
      {
          cout << "子线程线程ID: " << this_thread::get_id() << endl;
      }
      
      int main()
      {
          cout << "主线程的线程ID: " << this_thread::get_id() << endl;
          thread t1(func);
          thread t2(func);
          cout << "线程t1的线程ID: " << t1.get_id() << endl;
          cout << "线程t2的线程ID: " << t2.get_id() << endl;
          t1.join();
          t2.join();
      }
      
1.1.2.2、join()
  • join()字面意思是连接一个线程,意味着主动地等待线程的终止(线程阻塞)。在某个线程中通过子线程对象调用join()函数,调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后**join()**会清理当前子线程中的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。

  • 再次强调,我们一定要搞清楚这个函数阻塞的是哪一个线程,函数在哪个线程中被执行,那么函数就阻塞哪个线程。该函数的函数原型如下:

    • void join();
      
1.1.2.3、detach()
  • **detach()**函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。

  • detach调用之后,目标线程就成为了守护线程,驻留后台运行,与之关联的std::thread对象 失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权。当线程主函数执 行完之后,线程就结束了,运行时库负责清理与该线程相关的资源。

  • 该函数函数原型如下:

    • void detach()
      
  • 实例:

    • #include <iostream>
      #include <thread>
      #include <chrono>
      using namespace std;
      
      void func()
      {
          cout << "子线程线程ID: " << this_thread::get_id() << endl;
      }
      
      int main()
      {
          cout << "主线程的线程ID: " << this_thread::get_id() << endl;
          thread t1(func);
          thread t2(func);
          cout << "线程t1的线程ID: " << t1.get_id() << endl;
          cout << "线程t2的线程ID: " << t2.get_id() << endl;
          t1.detach();
          t2.detach();
      }
      
  • 注意:

    • 线程分离函数detach()不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制了
    • 比如:不能通过join()阻塞主线程等待子线程中的任务执行完毕,或者调用get_id()获取子线程的线程ID。
    • 有利就有弊,鱼和熊掌不可兼得,建议使用join()。
1.1.2.4、joinable()
  • **joinable()**函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型

    • 返回值为true:主线程和子线程之间有关联(连接)关系
    • 返回值为false:主线程和子线程之间没有关联(连接)关系
  • 该函数函数原型如下:

    • bool joinable() const noexcept;
      
  • 主线程和子线程断开关联的四种情况

    • 在创建的子线程对象的时候,如果没有指定任务函数(thread t),那么子线程不会启动,主线程和这个子线程也不会进行连接
    • 使用detach()函数进行了线程脱离
    • join()等待线程结束后对资源进行回收后
    • 使用move()函数进行控制权转移
  • 实例:

    • #include <iostream>
      #include <thread>
      #include <chrono>
      using namespace std;
      
      void func()
      {
      }
      
      int main()
      {
          thread t1;
          // 在创建的子线程对象的时候,如果没有指定任务函数,那么子线程不会启动,主线程和这个子线程也不会进行连接
          thread t2(func);
          thread t3(func);
          thread t4(func);
          thread t5 = move(t4);
      
          //// 线程脱离
          t2.detach();
      
          cout << "t1没有指定任务函数             t1.joinable(): " << t1.joinable() << endl;
          cout << "t2调用detach()脱离主线程       t2.joinable(): " << t2.joinable() << endl;
          cout << "                               t3.joinable(): " << t3.joinable() << endl;
          cout << "t4调用move()将控制权转移给了t5 t4.joinable(): " << t4.joinable() << endl;
          cout << "                               t5.joinable(): " << t5.joinable() << endl;
          t3.join();
          cout << "t3调用了join()资源回收         t3.joinable(): " << t3.joinable() << endl;
          t5.join();
      }
      
  • 结果:

    • t1没有指定任务函数                t1.joinable(): 0
      t2调用detach()脱离主线程          t2.joinable(): 0
                                     t3.joinable(): 1
      t4调用move()将控制权转移给了t5     t4.joinable(): 0
                                     t5.joinable(): 1
      t3调用了join()资源回收            t3.joinable(): 0
      
1.1.2.5、获取CPU核心数
  • thread线程类还提供了一个静态方法,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

  • 该函数函数原型如下:

    • static unsigned hardware_concurrency() noexcept;
      
  • 示例:

    • #include <iostream>
      #include <thread>
      using namespace std;
      
      int main()
      {
          int num = thread::hardware_concurrency();
          cout << "CPU number: " << num << endl;
      }
      
    1.1.2.6、注意事项
    • 当需要给任务函数的参数为引用时,需要在使用std::ref()包裹参数,不然会报错

    • 在C++中,向std::thread传递参数时,如果参数是引用类型,必须使用std::ref来包装参数。这是因为std::thread在创建新线程时会将参数复制到线程的内部存储中。如果不使用std::refstd::thread会认为你要传递的是一个值,而不是一个引用的概念。

    • std::ref 的作用

      • std::ref 是一个包装器,它告诉 std::thread 构造函数不要拷贝参数,而是传递引用。
      • std::ref 实际上是一个模板类,它将引用包装成一个可以被拷贝的对象。
    • 示例:

      • #include <iostream>
        #include <thread>
        using namespace std;
        
        void func(const int & i)
        {
            cout << i << endl;
        }
        
        int main()
        {
            int a = 1;
            thread t1(func, std::ref(a));
            t1.join();
            return 0;
        }
        

1.2、互斥量mutex

  • 进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写对于后两种情况来说,如果不做任何的人为干涉就会出现各种各样的错误数据。这是因为线程在运行的时候需要先得到CPU时间片,时间片用完之后需要放弃已获得的CPU资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的,就如同下图中的这个例子一样,理想很丰满现实却很残酷。

  • 在这里插入图片描述

  • 解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在C++11中一共提供了四种互斥锁:

    • std::mutex:独占的互斥锁,不能递归使用
    • std::timed_mutex:带超时的独占互斥锁,不能递归使用
    • std::recursive_mutex:递归互斥锁,不带超时功能
    • std::recursive_timed_mutex:带超时的递归互斥锁

1.2.1、std::mutex 独占的互斥锁

  • std::mutex简介:
    • 包含 <mutex> 头文件
    • std::mutex 是 C++ 标准库提供的一个同步原语,用于防止多个线程同时访问共享资源,从而避免数据竞争(race condition)。std::mutex 提供了一种互斥机制,允许一个线程锁定(lock)某个资源,并在使用完之后解锁(unlock),以确保同一时间只有一个线程可以访问该资源
    • 构造函数,std::mutex不允许拷贝构造也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
1.2.1.1、lock()
  • lock()函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用

  • 函数原型如下

    • void lock();
      
  • 独占互斥锁对象有两种状态:锁定未锁定

    • 如果互斥锁是打开的,调用lock()函数的线程会得到互斥锁的所有权,并将其上锁,其它线程再调用该函数的时候由于得不到互斥锁的所有权就会被lock()函数阻塞
    • 当拥有互斥锁所有权的线程将互斥锁解锁,此时被lock()阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。

    除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁

1.2.1.2、try_lock()
  • 二者的区别在于try_lock()不会阻塞线程lock()会阻塞线程
    • 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
    • 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false
  • 当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。
1.2.1.3、unlock()
  • 解锁,释放对互斥量的所有权

  • 函数原型如下

    • void unlock();
      
1.2.1.4、线程同步
  • 通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:

    • 找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
    • 找到和共享资源有关的上下文代码,也就是临界区
    • 在临界区的上边**调用互斥锁类的lock()**方法
    • 在临界区的下边**调用互斥锁的unlock()**方法
  • 线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。

  • 示例1:展示lock()加锁与unlock()解锁

    • #include <iostream>
      #include <chrono>
      #include <thread>
      #include <mutex>
      using namespace std;
      
      int num = 0;  // 为 g_num_mutex 所保护
      mutex num_mutex;  // 独占的互斥锁
      
      // 对num++,并按顺序打印
      void func(string thread_name)
      {
          for (int i = 0; i < 3; ++i)
          {
              num_mutex.lock();  // 加锁
              ++num;
              cout << thread_name << " -> " << num << endl;
              num_mutex.unlock(); // 解锁
              this_thread::sleep_for(chrono::seconds(1));  // 休眠1秒
          }
      }
      
      int main()
      {
          thread t1(func, "线程1");
          thread t2(func, "线程2");
          t1.join();
          t2.join();
      }
      
  • 结果

    • 线程1 -> 1
      线程2 -> 2
      线程2 -> 3
      线程1 -> 4
      线程2 -> 5
      线程1 -> 6
      
  • 示例2:展示lock()与try_lock()的区别

    • #include <iostream>
      #include <chrono>
      #include <thread>
      #include <mutex>
      using namespace std;
      
      int lock_num = 0;
      int try_lock_num = 0;
      
      mutex lock_mutex;
      mutex try_lock_mutex;
      
      void lock_func()
      {
          for (int i = 0; i < 10000; i++)
          {
              lock_mutex.lock();
              lock_num++;
              lock_mutex.unlock();
          }
      }
      
      void try_lock_func()
      {
          for (int i = 0; i < 10000; i++)
          {
              if (try_lock_mutex.try_lock())  // 尝试加锁
              {
                  try_lock_num++;
                  try_lock_mutex.unlock();
              }
          }
      }
      
      
      int main()
      {
          thread lock_threads[10];
          for (int i = 0; i < 10; i++)
          {
              lock_threads[i] = thread(lock_func);
          }
      
          thread try_lock_threads[10];
          for (int i = 0; i < 10; i++)
          {
              try_lock_threads[i] = thread(try_lock_func);
          }
          
          for (auto &th : lock_threads) 
              th.join();
          for (auto &th : try_lock_threads)
              th.join();
          cout << "lock()加锁结果:" << lock_num << endl;
          cout << "try_lock()加锁结果:" << try_lock_num << endl;
      }
      
  • 结果

    • // 第一次结果
      lock()加锁结果:100000
      try_lock()加锁结果:21414
      
      // 第二次结果
      lock()加锁结果:100000
      try_lock()加锁结果:17265
      
      // 第三次结果
      lock()加锁结果:100000
      try_lock()加锁结果:28063
      
  • 结果分析

    • 当使用lock()加锁时,如果获取不到锁就会阻塞,直到拿到锁才会继续进行,使用每次的++操作都会执行到,最终结果是10 * 10000 = 100000
    • 当使用try_lock加锁时,如果获取不到锁,会返回false,进行下一次循环++操作不会进行,只有拿到锁才会返回true进行++操作,所以结果小于100000
1.2.1.5、lock_guard()
  • lock_guard是C++11新增的一个模板类,使用这个类,可以简化互斥锁lock()和unlock()的写法,同时也更安全。

  • 函数原型如下:

    • // 类的定义,定义于头文件 <mutex>
      template< class Mutex >
      class lock_guard;
      
      // 常用构造函数
      explicit lock_guard( mutex_type& m );
      
  • lock_guard在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()操作而导致线程死锁。lock_guard使用了RAII技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。

  • 对上述1.2.1.4、线程同步中示例1中功能函数进行修改

    • #include <iostream>
      #include <chrono>
      #include <thread>
      #include <mutex>
      using namespace std;
      
      int num = 0;  // 为 g_num_mutex 所保护
      mutex num_mutex;  // 独占的互斥锁
      
      // 对num++,并按顺序打印
      void func(string thread_name)
      {
          for (int i = 0; i < 3; ++i)
          {
              lock_guard<mutex> lock(num_mutex);  // 加锁,推出循环后自动解锁
              ++num;
              cout << thread_name << " -> " << num << endl;
              this_thread::sleep_for(chrono::seconds(1));  // 休眠1秒
          }
      }
      
      int main()
      {
          thread t1(func, "线程1");
          thread t2(func, "线程2");
          t1.join();
          t2.join();
      }
      
  • 通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,但是这种方式也有弊端,在上面的示例程序中整个for循环的体都被当做了临界区,多个线程是线性的执行临界区代码的,因此临界区越大程序效率越低,还是需要根据实际情况选择最优的解决方案。

1.2.1.6、unique_lock()
  • std::unique_lock 是 C++ 标准库中的一个同步工具,用于管理互斥锁(mutex)的生命周期。它提供了比 std::lock_guard 更灵活的锁管理功能,支持延迟锁定、手动解锁、锁的所有权转移等特性。

  • 灵活的构造

    • 可以在构造时立即加锁。
    • 可以选择在构造时不加锁,稍后手动加锁。
  • 延迟锁定

    • 使用 std::defer_lock 策略在构造时不加锁。
  • 手动解锁

    • 支持手动解锁,提供更高的灵活性。
  • 锁的所有权转移

    • 支持通过移动语义将锁的所有权从一个 std::unique_lock 对象转移到另一个对象。
  • 条件变量

    • 常与 std::condition_variable 一起使用,因为条件变量需要控制锁的释放和重新获取。
  • 时间控制和尝试锁定

    • 支持尝试锁定(try_lock)和在一定时间内锁定(try_lock_fortry_lock_until)。
  • 示例:

    • #include <iostream>
      #include <chrono>
      #include <thread>
      #include <mutex>
      using namespace std;
      
      int num = 0;  // 为 g_num_mutex 所保护
      mutex num_mutex;  // 独占的互斥锁
      
      // 对num++,并按顺序打印
      void func(string thread_name)
      {
          for (int i = 0; i < 3; ++i)
          {
              unique_lock<mutex> lock(num_mutex);  // 构建时加锁
              /*
              unique_lock<mutex> lock(num_mutex, defer_lock);   // 构建时不加锁
              lock.lock();  // 手动加锁
              */
              ++num;
              cout << thread_name << " -> " << num << endl;
              lock.unlock();  // 提前手动解锁,非必须的,不手动解锁循环结束会自动解锁
              this_thread::sleep_for(chrono::seconds(1));  // 休眠1秒
          }
      }
      
      int main()
      {
          thread t1(func, "线程1");
          thread t2(func, "线程2");
          t1.join();
          t2.join();
      }
      
  • 与lock_guard的区别

    • 灵活性
      • std::lock_guard:构造时立即加锁,析构时自动解锁,不支持手动解锁或延迟锁定。
      • std::unique_lock:支持在构造时选择是否加锁,支持手动解锁和延迟锁定,提供更高的灵活性。
    • 延迟锁定
      • std::lock_guard:不支持延迟锁定。
      • std::unique_lock:支持使用 std::defer_lock 策略在构造时不加锁。
    • 手动解锁
      • std::lock_guard:不支持手动解锁,离开作用域时自动解锁。
      • std::unique_lock:支持手动解锁和重新加锁。
    • 锁的所有权转移
      • std::lock_guard:不支持锁的所有权转移。
      • std::unique_lock:支持通过移动语义将锁的所有权从一个对象转移到另一个对象。
    • 条件变量
      • std::lock_guard:不适合与 std::condition_variable 一起使用,因为条件变量需要控制锁的释放和重新获取。
      • std::unique_lock:适合与 std::condition_variable 一起使用,支持在等待条件变量期间释放和重新获取锁。
    • 时间控制和尝试锁定
      • std::lock_guard:不支持尝试锁定或时间控制。
      • std::unique_lock:支持尝试锁定(try_lock)和在一定时间内锁定(try_lock_fortry_lock_until)。
    总结
    • std::unique_lock 提供了比 std::lock_guard 更灵活的锁管理功能,适用于需要更复杂锁管理的情况,如延迟锁定、手动解锁、锁的所有权转移以及与条件变量配合使用等场景。std::lock_guard 则更适合简单的、作用域限定的锁管理需求。
    • 使用unique_lock需要付出更多的时间、性能成本

1.2.2、std::recursive_mutex 递归互斥锁

  • 递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题

  • 在下面的例子中使用独占非递归互斥量会发生死锁:

    • #include <iostream>
      #include <thread>
      #include <mutex>
      using namespace std;
      
      struct Calculate
      {
          Calculate() : m_i(6) {}
      
          void mul(int x)
          {
              lock_guard<mutex> locker(m_mutex);
              m_i *= x;
          }
      
          void div(int x)
          {
              lock_guard<mutex> locker(m_mutex);
              m_i /= x;
          }
      
          void both(int x, int y)
          {
              lock_guard<mutex> locker(m_mutex);
              mul(x);
              div(y);
          }
      
          int m_i;
          mutex m_mutex;
      };
      
      int main()
      {
          Calculate cal;
          cal.both(6, 3);
          return 0;
      }
      
    • 上面的程序中执行了cal.both(6, 3);调用之后,程序就会发生死锁,在**both()**中已经对互斥锁加锁了,继续调用mult()函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁(在C++中程序会异常退出,使用C库函数会导致这个互斥锁永远无法被解锁,最终阻塞所有的线程)。

    • 要解决这个死锁的问题,一个简单的办法就是使用递归互斥std::recursive_mutex,它允许一个线程多次获得互斥锁的所有权。

    • 修改之后的代码如下:

      • #include <iostream>
        #include <thread>
        #include <mutex>
        using namespace std;
        
        struct Calculate
        {
            Calculate() : m_i(6) {}
        
            void mul(int x)
            {
                lock_guard<recursive_mutex> locker(m_mutex);
                m_i *= x;
            }
        
            void div(int x)
            {
                lock_guard<recursive_mutex> locker(m_mutex);
                m_i /= x;
            }
        
            void both(int x, int y)
            {
                lock_guard<recursive_mutex> locker(m_mutex);
                mul(x);
                div(y);
            }
        
            int m_i;
            recursive_mutex m_mutex;
        };
        
        int main()
        {
            Calculate cal;
            cal.both(6, 3);
            cout << "cal.m_i = " << cal.m_i << endl;
            return 0;
        }
        
  • 虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:

    • 使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生
    • 递归互斥锁比非递归互斥锁效率要低一些。
    • 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。

1.3、std::timed_mutex

  • std::timed_mutex超时独占互斥锁,主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。

  • std::timed_mutexstd::_mutex多了两个成员函数:try_lock_for()和try_lock_until():

  • void lock();
    bool try_lock();
    void unlock();
    
    // std::timed_mutex比std::_mutex多出的两个成员函数
    template <class Rep, class Period>
      bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);
    
    template <class Clock, class Duration>
      bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
    
    • try_lock_for函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
    • try_lock_until函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
    • 关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回false
  • 案例1timed_mutextry_lock_for

    • #include <iostream>
      #include <thread>
      #include <mutex>
      using namespace std;
      
      timed_mutex g_mutex;
      
      void work()
      {
          chrono::seconds timeout(1);
          while (true)
          {
              // 通过阻塞一定的时长来争取得到互斥锁所有权
              if (g_mutex.try_lock_for(timeout))
              {
                  cout << "当前线程ID: " << this_thread::get_id() 
                      << ", 得到互斥锁所有权..." << endl;
                  // 模拟处理任务用了一定的时长
                  this_thread::sleep_for(chrono::seconds(5));
                  // 互斥锁解锁
                  g_mutex.unlock();
                  break;
              }
              else
              {
                  cout << "当前线程ID: " << this_thread::get_id() 
                      << ", 没有得到互斥锁所有权..." << endl;
                  // 模拟处理其他任务用了一定的时长
                  this_thread::sleep_for(chrono::milliseconds(5));
              }
          }
      }
      
      int main()
      {
          thread t1(work);
          thread t2(work);
      
          t1.join();
          t2.join();
      
          return 0;
      }
      
  • 案例1timed_mutextry_lock_until

    • #include <iostream>
      #include <thread>
      #include <mutex>
      using namespace std;
      
      timed_mutex g_mutex;
      
      void work()
      {
          while (true)
          {
              // 通过阻塞一定的时长来争取得到互斥锁所有权
              auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(1);
              if (g_mutex.try_lock_until(timeout))
              {
                  cout << "当前线程ID: " << this_thread::get_id()
                      << ", 得到互斥锁所有权..." << endl;
                  // 模拟处理任务用了一定的时长
                  this_thread::sleep_for(chrono::seconds(5));
                  // 互斥锁解锁
                  g_mutex.unlock();
                  break;
              }
              else
              {
                  cout << "当前线程ID: " << this_thread::get_id()
                      << ", 没有得到互斥锁所有权..." << endl;
                  // 模拟处理其他任务用了一定的时长
                  this_thread::sleep_for(chrono::milliseconds(1));
              }
          }
      }
      
      int main()
      {
          thread t1(work);
          thread t2(work);
      
          t1.join();
          t2.join();
      
          return 0;
      }
      
  • 关于递归超时互斥锁std::recursive_timed_mutex的使用方式和std::timed_mutex是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而std::timed_mutex只允许线程获取一次互斥锁所有权。

  • 另外,递归超时互斥锁std::recursive_timed_mutex也拥有和std::recursive_mutex一样的弊端,不建议频繁使用。

2、条件变量

  • 条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来使用,C++11提供了两种条件变量:
    • condition_variable:需要配合std::unique_lockstd::mutex进行wait操作,也就是阻塞线程的操作。
    • condition_variable_any:可以和任意带有lock()、unlock()语义的mutex搭配使用,也就是说有四种:
      • std::mutex:独占的非递归互斥锁
      • std::timed_mutex:带超时的独占非递归互斥锁
      • std::recursive_mutex:不带超时功能的递归互斥锁
      • std::recursive_timed_mutex:带超时的递归互斥锁
  • 条件变量通常用于生产者和消费者模型,大致使用过程如下:
    • 拥有条件变量的线程获取互斥量
    • 循环检查某个条件,如果条件不满足阻塞当前线程,否则线程继续向下执行
      • 产品的数量达到上限,生产者阻塞,否则生产者一直生产。。。
      • 产品的数量为零,消费者阻塞,否则消费者一直消费。。。
    • 条件满足之后,可以调用notify_one()或者notify_all()唤醒一个或者所有被阻塞的线程
      • 由消费者唤醒被阻塞的生产者,生产者解除阻塞继续生产。。。
      • 由生产者唤醒被阻塞的消费者,消费者解除阻塞继续消费。。。

2.1、condition_variable

  • 成员函数

    • condition_variable的成员函数主要分为两部分:线程等待(阻塞)函数线程通知(唤醒)函数这些函数被定义于头文件 <condition_variable>。
  • 等待函数

    • 调用wait()函数的线程会被阻塞

    • // 函数1
      void wait (unique_lock<mutex>& lck);
      // 函数2
      template <class Predicate>
      void wait (unique_lock<mutex>& lck, Predicate pred);
      
    • 函数1:调用该函数的线程直接被阻塞

    • 函数2:该函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数

      • 该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
      • 表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行
      • 独占的互斥锁对象不能直接传递给wait()函数,需要通过模板类unique_lock进行二次处理(将unique_lock对象传给wait)
  • 如果线程被该函数阻塞,这个线程会释放占有的互斥锁的所有权,当阻塞解除之后这个线程会重新得到互斥锁的所有权,继续向下执行(这个过程是在函数内部完成的,了解这个过程即可,其目的是为了避免线程的死锁)。

  • **wait_for()函数和wait()**的功能是一样的,只不过多了一个阻塞时长,假设阻塞的线程没有被其他线程唤醒,当阻塞时长用完之后,线程就会自动解除阻塞,继续向下执行。

template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
                    const chrono::duration<Rep,Period>& rel_time);
	
template <class Rep, class Period, class Predicate>
bool wait_for(unique_lock<mutex>& lck,
               const chrono::duration<Rep,Period>& rel_time, Predicate pred);

  • **wait_until()函数和wait_for()**的功能是一样的,它是指定让线程阻塞到某一个时间点,假设阻塞的线程没有被其他线程唤醒,当到达指定的时间点之后,线程就会自动解除阻塞,继续向下执行。

    • template <class Clock, class Duration>
      cv_status wait_until (unique_lock<mutex>& lck,
                            const chrono::time_point<Clock,Duration>& abs_time);
      
      template <class Clock, class Duration, class Predicate>
      bool wait_until (unique_lock<mutex>& lck,
                       const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);
      
  • 通知函数

    • notify_one():随机唤醒一个被当前条件变量阻塞的线程
    • notify_all():唤醒全部被当前条件变量阻塞的线程
  • void notify_one() noexcept;
    void notify_all() noexcept;
    
  • 1.2 生产者和消费者模型
    我们可以使用条件变量来实现一个同步队列,这个队列作为生产者线程和消费者线程的共享资源,示例代码如下:

    • #include <iostream>
      #include <thread>
      #include <mutex>
      #include <list>
      #include <functional>
      #include <condition_variable>
      using namespace std;
      
      class SyncQueue
      {
      public:
          SyncQueue(int maxSize) : m_maxSize(maxSize) {}
          
          void put(const int& x)
          {
              unique_lock<mutex> locker(m_mutex);
              // 判断任务队列是不是已经满了
              while (m_queue.size() == m_maxSize)
              {
                  cout << "任务队列已满, 请耐心等待..." << endl;
                  // 阻塞线程
                  m_notFull.wait(locker);
              }
              // 将任务放入到任务队列中
              m_queue.push_back(x);
              cout << x << " 被生产" << endl; 
              // 通知消费者去消费
              m_notEmpty.notify_one();
          }
      
          int take()
          {
              unique_lock<mutex> locker(m_mutex);
              while (m_queue.empty())
              {
                  cout << "任务队列已空,请耐心等待。。。" << endl;
                  m_notEmpty.wait(locker);   // 传入的是一个std::unique_lock对象
              }
              // 从任务队列中取出任务(消费)
              int x = m_queue.front();
              m_queue.pop_front();
              // 通知生产者去生产
              m_notFull.notify_one();
              cout << x << " 被消费" << endl;
              return x;
          }
      
          bool empty()
          {
              lock_guard<mutex> locker(m_mutex);
              return m_queue.empty();
          }
      
          bool full()
          {
              lock_guard<mutex> locker(m_mutex);
              return m_queue.size() == m_maxSize;
          }
      
          int size()
          {
              lock_guard<mutex> locker(m_mutex);
              return m_queue.size();
          }
      
      private:
          list<int> m_queue;     // 存储队列数据
          mutex m_mutex;         // 互斥锁
          condition_variable m_notEmpty;   // 不为空的条件变量
          condition_variable m_notFull;    // 没有满的条件变量
          int m_maxSize;         // 任务队列的最大任务个数
      };
      
      int main()
      {
          SyncQueue taskQ(50);
          auto produce = bind(&SyncQueue::put, &taskQ, placeholders::_1);
          auto consume = bind(&SyncQueue::take, &taskQ);
          thread t1[3];
          thread t2[3];
          for (int i = 0; i < 3; ++i)
          {
              t1[i] = thread(produce, i+100);
              t2[i] = thread(consume);
          }
      
          for (int i = 0; i < 3; ++i)
          {
              t1[i].join();
              t2[i].join();
          }
      
          return 0;
      }
      
  • 条件变量condition_variable类的**wait()**还有一个重载的方法,可以接受一个条件,这个条件也可以是一个返回值为布尔类型的函数,条件变量会先检查判断这个条件是否满足,如果满足条件(布尔值为true),则当前线程重新获得互斥锁的所有权,结束阻塞,继续向下执行;如果不满足条件(布尔值为false),当前线程会释放互斥锁(解锁)同时被阻塞,等待被唤醒。

  • 上面示例程序中的put()、take()函数可以做如下修改:

    • put()函数

      • void put(const int& x)
        {
            unique_lock<mutex> locker(m_mutex);
            // 根据条件阻塞线程
            m_notFull.wait(locker, [this]() {
                return m_queue.size() != m_maxSize;
            });
            // 将任务放入到任务队列中
            m_queue.push_back(x);
            cout << x << " 被生产" << endl;
            // 通知消费者去消费
            m_notEmpty.notify_one();
        }
        
    • take()函数

      • int take()
        {
            unique_lock<mutex> locker(m_mutex);
            m_notEmpty.wait(locker, [this]() {
                return !m_queue.empty();
            });
            // 从任务队列中取出任务(消费)
            int x = m_queue.front();
            m_queue.pop_front();
            // 通知生产者去生产
            m_notFull.notify_one();
            cout << x << " 被消费" << endl;
            return x;
        }
        
        
    • 修改之后可以发现,程序变得更加精简了,而且执行效率更高了,因为在这两个函数中的while循环被删掉了,但是最终的效果是一样的,推荐使用这种方式的wait()进行线程的阻塞。

2.2、condition_variable_any

  • 成员函数
    condition_variable_anycondition_variable成员函数与头文件相同

  • 等待函数

    • // 函数1
      template <class Lock> void wait (Lock& lck);
      // 函数2
      template <class Lock, class Predicate>
      void wait (Lock& lck, Predicate pred);
      
    • 函数1:调用该函数的线程直接被阻塞

    • 函数2:该函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数

      • 该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
      • 表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行
  • 可以直接传递给wait()函数的互斥锁类型有四种,分别是:

    • std::mutex:独占的非递归互斥锁
    • std::timed_mutex:带超时的独占非递归互斥锁
    • std::recursive_mutex:不带超时功能的递归互斥锁
    • std::recursive_timed_mutex:带超时的递归互斥锁
  • 生产者和消费者模型

    • 使用条件变量condition_variable_any同样可以实现上面的生产者和消费者的例子,代码只有个别细节上有所不同:

    • #include <iostream>
      #include <thread>
      #include <mutex>
      #include <list>
      #include <functional>
      #include <condition_variable>
      using namespace std;
      
      class SyncQueue
      {
      public:
          SyncQueue(int maxSize) : m_maxSize(maxSize) {}
      
          void put(const int& x)
          {
              lock_guard<mutex> locker(m_mutex);
              // 根据条件阻塞线程
              m_notFull.wait(m_mutex, [this]() {
                  return m_queue.size() != m_maxSize;
              });
              // 将任务放入到任务队列中
              m_queue.push_back(x);
              cout << x << " 被生产" << endl;
              // 通知消费者去消费
              m_notEmpty.notify_one();
          }
      
          int take()
          {
              lock_guard<mutex> locker(m_mutex);      // 使用了lock_guard加锁
              m_notEmpty.wait(m_mutex, [this]() {     // 直接传入锁
                  return !m_queue.empty();
              });
              // 从任务队列中取出任务(消费)
              int x = m_queue.front();
              m_queue.pop_front();
              // 通知生产者去生产
              m_notFull.notify_one();
              cout << x << " 被消费" << endl;
              return x;
          }
      
          bool empty()
          {
              lock_guard<mutex> locker(m_mutex);
              return m_queue.empty();
          }
      
          bool full()
          {
              lock_guard<mutex> locker(m_mutex);
              return m_queue.size() == m_maxSize;
          }
      
          int size()
          {
              lock_guard<mutex> locker(m_mutex);
              return m_queue.size();
          }
      
      private:
          list<int> m_queue;     // 存储队列数据
          mutex m_mutex;         // 互斥锁
          condition_variable_any m_notEmpty;   // 不为空的条件变量
          condition_variable_any m_notFull;    // 没有满的条件变量
          int m_maxSize;         // 任务队列的最大任务个数
      };
      
      int main()
      {
          SyncQueue taskQ(50);
          auto produce = bind(&SyncQueue::put, &taskQ, placeholders::_1);
          auto consume = bind(&SyncQueue::take, &taskQ);
          thread t1[3];
          thread t2[3];
          for (int i = 0; i < 3; ++i)
          {
              t1[i] = thread(produce, i + 100);
              t2[i] = thread(consume);
          }
      
          for (int i = 0; i < 3; ++i)
          {
              t1[i].join();
              t2[i].join();
          }
      
          return 0;
      }
      
  • 总结:

    • 以上介绍的两种条件变量各自有各自的特点,condition_variable 配合 unique_lock 使用更灵活一些,可以在在任何时候自由地释放互斥锁,而condition_variable_any 如果和lock_guard 一起使用必须要等到其生命周期结束才能将互斥锁释放。但是,condition_variable_any 可以和多种互斥锁配合使用,应用场景也更广,而 condition_variable 只能和独占的非递归互斥锁(mutex)配合使用,有一定的局限性。
    • 这里需要注意的是,wait函数中会释放mutex,而lock_guard这时还拥有mutex,它只会在出了作用域 之后才会释放mutex,所以这时它并不会释放,但执行wait时会提前释放mutex。
    • 从语义上看这里使用lock_guard会产生矛盾,但是实际上并不会出问题,因为wait提前释放锁之后会处 于等待状态,在被notify_one或者notify_all唤醒后会先获取mutex,这相当于lock_guard的mutex在 释放之后又获取到了,因此,在出了作用域之后lock_guard自动释放mutex不会有问题。 这里应该用unique_lock,因为unique_lock不像lock_guard一样只能在析构时才释放锁,它可以随时释 放锁,因此在wait时让unique_lock释放锁从语义上更加准确。
    • 注意:condition_variablewait函数的第一个参数传入的是unique_lock对象,而condition_variable_anywait函数的第一个参数传入的是互斥锁对象

3、原子变量(atomic)

1. 引言

在多线程编程中,数据一致性和避免数据竞争是关键问题。C++11 引入了原子变量(Atomic Variables)来解决这些问题,确保操作的原子性和线程安全。

2. 原子变量的基本概念
  • 原子性(Atomicity):原子操作是不可分割的,即在执行过程中不会被其他线程中断。这确保了操作的完整性。
  • 数据竞争(Data Race):当多个线程同时访问同一内存位置,且至少有一个线程在写入时,可能发生数据竞争,导致未定义行为。
3. 原子变量的定义

原子变量是通过 std::atomic 模板类定义的。它可以包装基本数据类型(如 int, bool 等)。

#include <atomic>

std::atomic<int> atomic_int(0);  // 定义一个原子整数变量
std::atomic<bool> atomic_bool(false);  // 定义一个原子布尔变量
4. 原子操作

原子变量支持多种原子操作,包括读、写、加减、交换等。

  • store:写入值

    atomic_int.store(42);
    
  • load:读取值

    int value = atomic_int.load();
    
  • exchange:原子交换

    int old_value = atomic_int.exchange(99);  // 返回旧值,并将新值写入
    
  • compare_exchange_strongcompare_exchange_weak:比较并交换

    bool success = atomic_int.compare_exchange_strong(old_value, new_value);
    
  • fetch_addfetch_sub:原子加法和减法

    int new_value = atomic_int.fetch_add(10);  // 返回旧值,并在旧值基础上加10
    
  • **++--:自增和自减操作

    ++atomic_int;  // 相当于 fetch_add(1)
    --atomic_int;  // 相当于 fetch_sub(1)
    
5. 内存序(Memory Order)

内存序控制内存访问的顺序和可见性。C++ 提供了多种内存序选项:

  • memory_order_relaxed:最宽松的内存序,不保证任何同步和顺序。
  • memory_order_acquire:确保在此操作之后的所有读写操作都不会被重排到此操作之前。
  • memory_order_release:确保在此操作之前的所有读写操作都不会被重排到此操作之后。
  • memory_order_acq_rel:结合 acquirerelease,适用于 compare_exchange
  • memory_order_seq_cst:最强的一致性保证,所有线程都能看到一致的操作顺序。
6. 示例代码

以下是一个简单的多线程计数器示例,使用原子变量确保线程安全:

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 10000; ++i) {
        ++counter;  // 原子自增操作
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Counter value: " << counter.load() << std::endl;
    return 0;
}
7. 内存序示例

以下是一个使用不同内存序的示例:

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> data(0);
std::atomic<bool> ready(false);

void producer() {
    data.store(42, std::memory_order_relaxed);  // 使用 relaxed 内存序
    ready.store(true, std::memory_order_release);  // 使用 release 内存序
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) {  // 使用 acquire 内存序
        // 等待数据准备好的信号
    }
    std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl;
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();

    return 0;
}
8. 选择合适的内存序
  • memory_order_relaxed:适用于不需要同步的操作。
  • memory_order_acquire:适用于读取操作,确保此后的操作能看到之前的写入。
  • memory_order_release:适用于写入操作,确保此前的操作对其他线程可见。
  • memory_order_acq_rel:适用于读-修改-写操作,确保操作的顺序和可见性。
  • memory_order_seq_cst:提供最强的同步保证,适用于需要一致性保证的场景。
9. 总结

原子变量是 C++ 中处理并发编程的重要工具,通过确保操作的原子性和避免数据竞争,可以显著提高多线程程序的可靠性和性能。理解和正确使用原子操作及其内存序是编写高效并发代码的关键。

4、异步操作

  1. std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果
  • std::future中提供了三个等待函数
    • wait():死等直到子线程任务执行完毕将返回值写入到future对象中
    • wait_for():只会让线程阻塞一定的时长,但是这样并不能保证对应的那个子线程中的任务已经执行完毕了。
    • **wait_until()wait_for()**函数功能是差不多,前者是阻塞到某一指定的时间点,后者是阻塞一定的时长。
    • 当**wait_until()和wait_for()**函数返回之后,并不能确定子线程当前的状态,因此我们需要判断函数的返回值,这样就能知道子线程当前的状态了,返回值与含义如下表:
常量解释
future_status::deferred子线程中的任务函仍未启动
future_status::ready子线程中的任务已经执行完毕,结果已就绪
future_status::timeout子线程中的任务正在执行中,指定等待时长已用完
  • 上述等待过程其实完全可以不需要,直接调用get()就可获取future中的结果
    • get()是一个阻塞函数,当获取到future中的值时才会解除阻塞,并返回获取到的值
  1. std::promise

    • std::promise是一个协助线程赋值的类,它能够将数据和future对象绑定起来,为获取线程函数中的某个值提供便利。

    • std::promise类内部管理着一个future类对象,调用get_future()就可以得到这个future对象了

    • set_value()设置promise的值,并马上处于就绪状态

    • set_value_at_thread_exit存储要传出的 value 值,但是不立即令状态就绪在当前线程退出时,子线程资源被销毁,再令状态就绪。

    • 通过promise传递数据的过程一共分为5步:

      • 在主线程中创建std::promise对象
      • 将这个std::promise对象通过引用的方式传递给子线程的任务函数
      • 在子线程任务函数中给std::promise对象赋值
      • 在主线程中通过std::promise对象取出绑定的future实例对象
      • 通过得到的future对象取出子线程任务函数中返回的值。
    • 示例1:set_value

      • /*
        示例程序的中子线程的任务函数指定的是一个匿名函数,在这个匿名的任务函数执行期间通过p.set_value(100);传出了数据并且激活了状态,数据就绪后,外部主线程中的int value = f.get();解除阻塞,并将得到的数据打印出来,5秒钟之后子线程休眠结束,匿名的任务函数执行完毕。
        */
        
        #include <iostream>
        #include <thread>
        #include <future>
        using namespace std;
        
        int main()
        {
            promise<int> pr;
            thread t1([](promise<int> &p) {
                p.set_value(100);            // 设置值以后,16行get可以立刻获取到值,并解除阻塞
                this_thread::sleep_for(chrono::seconds(3));
                cout << "睡醒了...." << endl;
            }, ref(pr));
        
            future<int> f = pr.get_future();
            int value = f.get();   // get是个阻塞函数,获取到值解除阻塞并返回获取到值
            cout << "value: " << value << endl;
        
            t1.join();
            return 0;
        }
        
      • 结果:

      • value: 100
        睡醒了....
        
    • 示例2:set_value_at_thread_exit

      • /*
        子线程的这个匿名的任务函数中通过p.set_value_at_thread_exit(100);在执行完毕并退出之后才会传出数据并激活状态,数据就绪后,外部主线程中的int value = f.get();解除阻塞,并将得到的数据打印出来,因此子线程在休眠5秒钟之后主线程中才能得到传出的数据。
        */
        
        #include <iostream>
        #include <thread>
        #include <future>
        using namespace std;
        
        int main()
        {
            promise<int> pr;
            thread t1([](promise<int> &p) {
                p.set_value_at_thread_exit(100);   // 设置值,当线程结束get才会获取到值
                this_thread::sleep_for(chrono::seconds(3));
                cout << "睡醒了...." << endl;
            }, ref(pr));
        
            future<int> f = pr.get_future();
            int value = f.get();
            cout << "value: " << value << endl;
        
            t1.join();
            return 0;
        }
        
      • 结果:

      • 睡醒了....
        value: 100
        
    • 注意:在这两个实例程序中有一个知识点需要强调,在外部主线程中创建的promise对象必须要通过引用的方式传递到子线程的任务函数中,在实例化子线程对象的时候,如果任务函数的参数是引用类型,那么实参一定要放到**std::ref()**函数中,表示要传递这个实参的引用到任务函数中。

  2. std::packaged_task

    • std::packaged_task类包装了一个可调用对象包装器类对象(可调用对象包装器包装的是可调用对象,可调用对象都可以作为函数来使用)

    • 这个类可以将内部包装的函数和future类绑定到一起,以便进行后续的异步调用,它和std::promise有点类似,std::promise内部保存一个共享状态的值,而std::packaged_task保存的是一个函数。

    • 案例

      • #include <iostream>
        #include <thread>
        #include <future>
        using namespace std;
        
        int main()
        {
            // 第一步:包装一个函数
            packaged_task<int(int)> task([](int x) {
                return x += 100;
                });
        
            // 第二步:执行这个函数
            task(10);
            // thread t1(ref(task), 100); // 也可以使用线程执行
        
            // 第三步:获取future对象
            future<int> f = task.get_future();
            
            // 第四步:获取值
            int value = f.get();
            
            // int value = task.get_future().get(); // 三四合并
            
            cout << "value: " << value << endl;
        
            return 0;
        }
        
  3. std::async

    • std::async函数比前面提到的std::promise和packaged_task更高级一些,因为通过这函数可以直接启动一个子线程并在这个子线程中执行对应的任务函数,异步任务执行完成返回的结果也是存储到一个future对象中,当需要获取异步任务的结果时,只需要调用future 类的get()方法即可,如果不关注异步任务的结果,只是简单地等待任务完成的话,可以调用future 类的wait()或者wait_for()方法。

    • 案例:

      • #include <iostream>
        #include <thread>
        #include <future>
        using namespace std;
        
        int main()
        {
            cout << "主线程ID: " << this_thread::get_id() << endl;
            // 调用函数直接创建线程执行任务
            future<int> f = async([](int x) {
                cout << "子线程ID: " << this_thread::get_id() << endl;
                return x += 100;
                }, 100);
            this_thread::sleep_for(chrono::seconds(5));
            cout << f.get();
        
            return 0;
        }
        
      • 结果:

      • 主线程ID: 8348
        子线程ID: 5508
        200
        
  4. 终结:

    • 使用async()函数,是多线程操作中最简单的一种方式,不需要自己创建线程对象,并且可以得到子线程函数的返回值。
    • 使用std::promise类,在子线程中可以传出返回值也可以传出其他数据,并且可选择在什么时机将数据从子线程中传递出来,使用起来更灵活。
    • 使用std::packaged_task类,可以将子线程的任务函数进行包装,并且可以得到子线程的返回值。

    参考:多线程异步操作 | 爱编程的大丙

5、可调用对象包装器、绑定器

5.1、std::function 可调用对象包装器

  1. std::function是可调用对象的包装器。它是一个类模板,可以容纳除了类(非静态)成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。

  2. 基本用法

    • std::function必须要包含一个叫做functional的头文件,可调用对象包装器使用语法如下

    • #include <functional>
      std::function<返回值类型(参数类型列表)> diy_name = 可调用对象;
      
    • 基本使用方法:

      • #include <iostream>
        #include <functional>
        using namespace std;
        
        int add(int a, int b)
        {
            cout << a << " + " << b << " = " << a + b << endl;
            return a + b;
        }
        
        class T1
        {
        public:
            static int sub(int a, int b)
            {
                cout << a << " - " << b << " = " << a - b << endl;
                return a - b;
            }
        };
        
        class T2
        {
        public:
            int operator()(int a, int b)
            {
                cout << a << " * " << b << " = " << a * b << endl;
                return a * b;
            }
        };
        
        int main(void)
        {
            // 绑定一个普通函数
            function<int(int, int)> f1 = add;
            // 绑定一个静态类成员函数
            function<int(int, int)> f2 = T1::sub;
            // 绑定一个仿函数
            T2 t;
            function<int(int, int)> f3 = t;
        
            // 函数调用
            f1(9, 3);
            f2(9, 3);
            f3(9, 3);
        
            return 0;
        }
        
    • 结果

      • 9 + 3 = 12
        9 - 3 = 6
        9 * 3 = 27
        

5.2、std::bind 绑定器

  1. std::bind

    • std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。
    • 通俗来讲,它主要有两大作用:
      • 将可调用对象与其参数一起绑定成一个仿函数
      • 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数。
  2. 绑定器函数使用语法格式如下

    • // 绑定非类成员函数/变量
      auto f = std::bind(可调用对象地址, 绑定的参数/占位符);
      // 绑定类成员函/变量
      auto f = std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符);
      
    • 例子

      • #include <iostream>
        #include <functional>
        using namespace std;
        
        class Test
        {
        public:
            void output(int x, int y)
            {
                cout << "x: " << x << ", y: " << y << endl;
            }
            int m_number = 100;
        };
        
        int func(string name, int age)
        {
            cout << name << "永远" << age << endl;
            return age;
        }
        
        int main(void)
        {
            // 绑定普通函数
            auto f = bind(func,"天天", placeholders::_1);
            cout << f(18) << endl;
        
            Test t;
            // 绑定类成员函数
            function<void(int, int)> f1 = bind(&Test::output, &t, placeholders::_1, placeholders::_2);
            // 绑定类成员变量(公共)
            function<int& (void)> f2 = bind(&Test::m_number, &t);
        
            // 调用
            f1(520, 1314);
            f2() = 2333;
            cout << "t.m_number: " << t.m_number << endl;
        
            return 0;
        }
        

6、可变参数列表

1. 概述

模板参数包(Template Parameter Pack)是 C++11 引入的一项重要特性,允许模板接受任意数量和类型的参数。模板参数包可以分为两种类型:类型模板参数包非类型模板参数包

2. 定义模板参数包

类型模板参数包:

template<typename... Args>
void print_args(Args... args);

非类型模板参数包:

template<int... Ns>
void print_values();
3. 使用模板参数包
3.1 函数模板

模板参数包可以在函数模板中使用,以处理任意数量的参数。

示例代码:

#include <iostream>

template<typename... Args>
void print_args(Args... args) {
    (std::cout << ... << args) << std::endl;
}

int main() {
    print_args(1, 2.5, "Hello");
    return 0;
}

输出:

12.5Hello
3.2 类模板

模板参数包也可以在类模板中使用,以处理任意数量的模板参数。

示例代码:

#include <iostream>
#include <tuple>

template<typename... Args>
class MyClass {
public:
    MyClass(Args... args) : data(std::make_tuple(args...)) {}

    void print() {
        std::apply([](const auto&... args) {
            ((std::cout << args << " "), ...);
            std::cout << std::endl;
        }, data);
    }

private:
    std::tuple<Args...> data;
};

int main() {
    MyClass<int, double, std::string> obj(1, 2.5, "Hello");
    obj.print();
    return 0;
}

输出:

1 2.5 Hello 
4. 折叠表达式(Fold Expressions)

C++17 引入了折叠表达式,进一步简化了处理模板参数包的方式。

4.1 一元右折叠

一元右折叠表达式将参数包展开并与操作符结合。

示例代码:

#include <iostream>

template<typename... Args>
void print_sum_right(Args... args) {
    std::cout << (args + ...) << std::endl; // 一元右折叠
}

int main() {
    print_sum_right(1, 2, 3, 4);
    return 0;
}

输出:

10
4.2 一元左折叠

一元左折叠表达式将参数包展开并与操作符结合。

示例代码:

#include <iostream>

template<typename... Args>
void print_sum_left(Args... args) {
    std::cout << (... + args) << std::endl; // 一元左折叠
}

int main() {
    print_sum_left(1, 2, 3, 4);
    return 0;
}

输出:

10
4.3 二元折叠

二元折叠表达式将参数包展开并与初始值和操作符结合。

示例代码:

#include <iostream>

template<typename... Args>
void print_sum_binary(Args... args) {
    std::cout << (0 + ... + args) << std::endl; // 二元折叠
}

int main() {
    print_sum_binary(1, 2, 3, 4);
    return 0;
}

输出:

10
5. 递归展开参数包

模板参数包可以通过递归的方式展开,这在某些复杂的情况下非常有用。

示例代码:

#include <iostream>

// 基础情况:没有参数
void print_args() {
    std::cout << std::endl;
}

// 递归情况:处理一个参数并递归处理其余参数
template<typename T, typename... Args>
void print_args(T first, Args... args) {
    std::cout << first << " ";
    print_args(args...);
}

int main() {
    print_args(1, 2.5, "Hello");
    return 0;
}

输出:

1 2.5 Hello 
6. 总结

模板参数包是 C++11 引入的一项强大特性,允许模板接受任意数量和类型的参数。通过折叠表达式和递归展开,可以灵活地处理参数包。模板参数包在函数模板和类模板中都有广泛的应用,能够极大地提高代码的灵活性和可复用性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值