C++多线程(一)

简介: C++多线程

在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下有各自的接口,这使得代码的可移植性较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念


一、线程库thread

1.1 线程对象的构造

调用无参构造函数


thread提供了无参构造函数,调用无参构造函数创建出来的线程对象没有关联任何线程函数,即没有启动任何线程

thread t;

由于thread提供了移动赋值函数,因此当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象

#include <iostream>
#include <thread>
using namespace std;
void func(int num)
{
  for (int i = 0; i < num; ++i) {
    cout << i << endl;
  }
}
int main()
{
  thread t;
  //...
  t = thread(func, 100);
  t.join();
  return 0;
}


调用带参构造函数

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
  • fn:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等
  • args...:调用可调用对象fn时所需要的若干参数。


#include <iostream>
#include <thread>
using namespace std;
void func(int num)
{
  for (int i = 0; i < num; i++) {
    cout << i << endl;
  }
}
int main()
{
  thread t(func, 10);
  t.join();
  return 0;
}

调用移动构造函数


thread提供了移动构造函数,能够用一个右值线程对象来构造一个线程对象

#include <iostream>
#include <thread>
using namespace std;
void func(int num)
{
  for (int i = 0; i < num; ++i) {
    cout << i << endl;
  }
}
int main()
{
  thread t = thread(func, 10);
  t.join();
  return 0;
}


注意:


线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态

若创建线程对象时没有提供线程函数,那么该线程对象实际没有对应任何线程。

若创建线程对象时提供了线程函数,那么就会启动一个线程来执行这个线程函数,该线程与主线程一起运行

thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行

1.2 thread类的成员函数

thread类中常用的成员函数如下:

724ca3ae54c14b859c03fc3552850218.png



joinable()函数还可以用于判定线程是否是有效的,若是以下任意情况,则线程无效:


采用无参构造函数构造的线程对象(该线程对象没有关联任何线程)

线程对象的状态已经转移给其他线程对象(已经将线程交给其他线程对象管理)

线程已经调用join或detach结束(线程已经结束)

启动一个线程后,当这个线程退出时,需要对该线程所使用的资源进行回收,否则可能会导致内存泄露等问题。thread库提供了两种回收线程资源的方式:


join方式


主线程创建新线程后,可以调用join()函数等待新线程终止,当新线程终止时join()函数就会自动清理线程相关的资源。join()函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join(),否则程序会崩溃

#include <iostream>
#include <thread>
using namespace std;
void func(int n)
{
  for (int i = 0; i < n; i++) {
    cout << i << endl;
  }
}
int main()
{
  thread t(func, 10);
  t.join();
  t.join(); //程序崩溃
  return 0;
}


但如果一个线程对象join后,又调用移动赋值函数,将一个右值线程对象的关联线程的状态转移过来了,那么这个线程对象又可调用一次join

#include <iostream>
#include <thread>
using namespace std;
void func(int n)
{
  for (int i = 0; i < n; i++) {
    cout << i << endl;
  }
}
int main()
{
  thread t(func, 10);
  t.join();
  t = thread(func, 10);
  t.join();
  return 0;
}

但采用join的方式结束线程,在某些场景下也可能会出现问题。如在该线程被join前,若中途因为某些原因导致程序不再执行后续代码,这时这个线程将不会被join

#include <iostream>
#include <thread>
#include <windows.h>
using namespace std;
void func(int num)
{
  for (int i = 0; i < num; i++) {
    cout << i << endl;
  }
}
bool DoSomething(){ return false; }
int main()
{
  thread t(func, 10);
  Sleep(3);
  if (!DoSomething()) return -1;
  t.join(); //不会被执行
  return 0;
}


因此采用join方式结束线程时,join()函数的调用位置非常关键,为了避免上述问题,可以采用RAII的方式对线程对象进行封装,即利用对象的生命周期来控制线程资源的释放

#include <iostream>
#include <thread>
#include <windows.h>
using namespace std;
class Thread
{
public:
  Thread(thread& t):_thread(t) {}
  ~Thread() {
    if (_thread.joinable()) _thread.join();
  }
private:
  //防拷贝
  Thread(const Thread&) = delete;
  Thread& operator=(const Thread&) = delete;
private:
  thread& _thread;
};
void func(int num)
{
  for (int i = 0; i < num; i++) {
    cout << i << endl;
  }
}
bool DoSomething(){ return false; }
int main()
{
  thread t(func, 10);
  Thread T(t);
  Sleep(3);
  if (!DoSomething()) return 1;
  return 0;
}


每当创建一个线程对象后,就用Thread类对其进行封装产生一个Thread对象。

当Thread对象生命周期结束时就会调用析构函数,在析构中会通过joinable()判断这个线程是否需要被join,若需要那么就会调用join对其该线程进行等待

detach方式


主线程创建新线程后,也可以调用detach函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收


使用detach的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach函数。

否则线程对象可能会因为某些原因,在后续调用detach函数分离线程之前被销毁掉,这时就会导致程序崩溃。

因为当线程对象被销毁时会调用thread的析构函数,而在thread的析构函数中会通过joinable判断这个线程是否需要被join,如果需要那么就会调用terminate终止当前程序(程序崩溃)

1.3 this_thread类

函数名 功能
get_id 获取当前线程ID
yeild 当前线程出让时间片,CPU调度其他时间片
sleep_until 使调用线程休眠到一个固定时间(绝对时间)
sleep_for 使调用线程休眠一个时间段(相对时间)

当想获取线程ID时,可以通过线程对象的get_id()接口,但想在与线程关联的线程函数中获取线程ID,这个办法就行不通了,可以调用this_thread类中的接口get_id()

#include <iostream>
#include <thread>
using namespace std;
void func()
{
  cout << this_thread::get_id() << endl;
}
int main()
{
  thread t(func);
  t.join();
  return 0;
}

1.4 线程函数的参数问题

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参


若要通过线程函数的形参改变外部的实参,可以参考以下三种方法:


方法一:借助std::ref()


当线程函数的参数类型为引用类型时,若要想线程函数形参引用的是外部传入的实参,而不是线程栈空间中的拷贝,那么在传入实参时需要借助ref()函数保持对实参的引用


#include <iostream>
#include <thread>
using namespace std;
void add(int& num) {
  num++;
}
int main()
{
  int num = 0;
  thread t(add, ref(num));
  t.join();
  cout << num << endl;
  return 0;
}

方法二:指针地址


将线程函数的参数类型改为指针类型,将实参的地址传入线程函数,此时在线程函数中可以通过修改该地址处的变量,进而影响到外部实参


#include <iostream>
#include <thread>
using namespace std;
void add(int* num) {
  ++(*num);
}
int main()
{
  int num = 0;
  thread t(add, &num);
  t.join();
  cout << num << endl;
  return 0;
}

方法三:lambda表达式


将lambda表达式作为线程函数,利用lambda函数的捕捉列表,以引用的方式对外部实参进行捕捉,此时在lambda表达式中对形参的修改也能影响到外部实参


#include <iostream>
#include <thread>
using namespace std;
int main()
{
  int num = 0;
  thread t([&num](){ ++num; });
  t.join();
  cout << num << endl;
  return 0;
}
目录
相关文章
|
16天前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
30 7
|
16天前
|
消息中间件 存储 安全
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
22天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
45 6
|
1月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
76 5
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
23 0
C++ 多线程之线程管理函数
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
29 0
Linux C/C++之线程基础
|
3月前
|
Java 调度
基于C++11的线程池
基于C++11的线程池
|
4月前
|
算法 编译器 C++
开发与运维线程问题之在C++的原子操作中memory_order如何解决
开发与运维线程问题之在C++的原子操作中memory_order如何解决
43 2