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;
}
目录
相关文章
|
26天前
|
存储 Java 程序员
C++多线程编程基础
C++多线程编程基础
29 2
|
1月前
|
安全 前端开发 程序员
|
4天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
2天前
|
API C++
c++进阶篇——初窥多线程(三)cpp中的线程类
C++11引入了`std::thread`,提供对并发编程的支持,简化多线程创建并增强可移植性。`std::thread`的构造函数包括默认构造、移动构造及模板构造(支持函数、lambda和对象)。`thread::get_id()`获取线程ID,`join()`确保线程执行完成,`detach()`使线程独立,`joinable()`检查线程状态,`operator=`仅支持移动赋值。`thread::hardware_concurrency()`返回CPU核心数,可用于高效线程分配。
|
2天前
|
安全 API C++
逆向学习Windows篇:C++中多线程的使用和回调函数的实现
逆向学习Windows篇:C++中多线程的使用和回调函数的实现
5 0
|
7天前
|
存储 安全 程序员
c++理论篇——初窥多线程(一) 计算机内存视角下的多线程编程
c++理论篇——初窥多线程(一) 计算机内存视角下的多线程编程
|
1月前
|
安全 Go 对象存储
C++多线程编程:并发与同步的实战应用
本文介绍了C++中的多线程编程,包括基础知识和实战应用。C++借助`&lt;thread&gt;`库支持多线程,通过`std::thread`创建线程执行任务。文章探讨了并发与同步的概念,如互斥锁(Mutex)用于保护共享资源,条件变量(Condition Variable)协调线程等待与通知,以及原子操作(Atomic Operations)保证线程安全。实战部分展示了如何使用多线程进行并发计算,利用`std::async`实现异步任务并获取结果。多线程编程能提高效率,但也需注意数据竞争和同步问题,以确保程序的正确性。
|
28天前
|
安全 Linux 编译器
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(下)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
24 0
|
28天前
|
安全 C语言 C++
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(中)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
26 0
|
28天前
|
Linux 调度 C语言
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(上)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
27 0