文章

c++(2·多线程)

多线程

基础概念

  • 进程:一个正在运行的应用程序被称为一个进程
  • 线程:是进程中的实际运行单位,每个进程可以拥有至少一个的线程
  • 同步:一个任务完成后另一个任务才能开始
  • 异步:一个任务的开始和完成不会直接影响另一个任务的开始和完成
  • 并发:有多个任务在单核CPU被处理,任务被交替执行
  • 并行:有多个任务在多核CPU被处理,任务可以被同时执行

多线程

多线程是并发/并行的技术实现,使得程序能够在同一时间执行多于一个线程,进而提升整体处理性能,但是其实它并非一定会提升性能

  • 单核处理CPU任务:多线程会时间切片,交替执行任务,这是同步且并发的,CPU任务不能被异步处理,且任务调度需要时间,因此不会提高效率
  • 单核处理CPU + GPU任务:由于GPU是高度并行的,因此可以在执行CPU任务时同时处理多个GPU任务,CPU和GPU两者之间是异步且并行的,会提高效率
  • 多核处理CPU任务:多个任务被异步处理,这是异步且并行的,会提高效率

std::thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <thread>

thread a(
    []{
        cout << "Hello, " << flush;
    }
)//创建线程a并执行函数
a.join();//线程结束后清理

void printId(int id) {
    std::cout << id << std::endl;
}
thread th(print, 1);//有参线程
th.join();

展开

  • thread()创建一个线程
  • thread(Fn&& fn, Args&&… args)创建线程并执行函数(可调用对象),以args为参数
  • thread(thread&& x) 移动构造(注意没有拷贝构造,否则会有严重的混乱问题)
  • join()阻塞主线程后续的指令,线程结束清理资源,它能确保线程执行的完整性
  • detach()将线程与调用其的线程分离,彼此独立执行,会自动清理资源(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join)
  • 注:如果没有调用join 或 detach,程序的行为是未定义的,可能会内存泄漏等问题
  • joinable()返回线程是否可以执行join函数

引用args

1
2
3
4
5
6
7
8
template<typename T> 
void changevalue(T &x, T val) {
	x = val;
}
int x = 0;
thread th(changevalue, x, 5);//❌:不能将左值传给右值
thread th(changevalue, ref(x), 5);//✅
th.join();

展开

如果bind关联的函数的参数是有引用的,我们应该在thread创建时以引用形式传递变量

但thread 参数是万能引用,会发生拷贝,利用std::ref和std::cref可以以引用的方式传递,防止拷贝

std::atomic和std::mutex

多个线程由于并行异步的关系,线程间无序执行,操作共享变量时会出错,会发生数据竞争:指多个线程同时访问同一个共享变量,并且至少有一个访问是写操作,其他为读操作

例如一个线程正在读取变量的值,而另一个线程同时在修改这个值,那么读取线程可能会得到一个不一致或错误的值,

为了应对这种情况,使用:

1
2
3
4
mutex mtx;
mtx.lock();
mtx.unlock();
atomic_int n = 0;

展开

  • mutex互斥量:通过mutex将共享变量定义为互斥量,当一个线程将mutex lock()锁住时,其它的线程就不能操作mutex,其他线程会被阻塞,直到这个线程将mutex unlock()解锁,可以用try_lock()查看mutex是否被锁住,mutex在每次被修改都要上锁、解锁,因此效率很低
  • atomic原子:通过atomic_数据类型 定义一个原子,表明它不能被并行执行,需要被同步操作,因此免去了mutex每次上锁、解锁的时间消耗

std::async

  • async (Fn&& fn, Args&&… args)开始执行fn函数,以args为参数,返回std::future
  • async (launch policy, Fn&& fn, Args&&… args);其中launch是枚举,表示启动策略
  • launch::async异步启动
  • launch::deferred同步启动

async vs. thread

  • async是函数而非类,返回结果为std::future未来
  • async可以选择启动策略
  • async会自动管理线程的生命周期,也就是不需要join() / detach()释放资源
  • thread适用于对线程进行精细控制的场景,async适用于简单的线程任务

std::future

1
2
3
4
5
6
template<class ... Args> 
decltype(auto) sum(Args&&... args) {
	return (0 + ... + args);
}
future<int> val = async(launch::async, sum<int, int, int>, 1, 10, 100);
cout << val.get() << endl;

展开

  • 创建一个future<对应函数返回类型>类型的变量等于async函数的结果,还可以用来检测线程是否已结束
  • get()等待线程结束并获取返回值
  • 检查线程是否结束wait_for()线程结束则返回future_status::ready,若没结束则返回future_status::timeout

std::promise

thread通过引用获取返回值

由于thread类并没有提供直接获取返回值的函数,想要获取thread线程的返回值,可以通过引用传递,参数为引用会直接修改被绑定的那些参数

thread通过promise获取返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class ... Args> decltype(auto) sum(Args&&... args) {
	return (0 + ... + args);
}
template<class ... Args> void sum_thread(promise<long long> &val, Args&&... args) {
	val.set_value(sum(args...));//设置值为sum()
}
int main() {
	promise<long long> sum_value;
	thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100);//将promise以引用的方式传入函数
	cout << sum_value.get_future().get() << endl;//构造future对象,它的值和primise相同,get获取返回值
	get_sum.join();
	return 0;
}

展开

promise承诺 实际上是std::future的一个包装,作用是线程的通讯,例如在一个线程t1中设置值(任何类型),可供相绑定的std::future对象在另一线程t2中获取,

  • promise()
  • promise(allocator_arg_t aa, const Alloc& alloc)使用特定的内存分配器alloc构造对象
  • promise (promise&& x) 移动构造
  • get_future()构造一个future对象,其值与promise相同
  • set_value (const T& val / T&& val)设置值

std::this_thread

this_thread是个命名空间

  • get_id() 获取线程id
  • sleep睡眠( const std::chrono::duration< Rep, Period>& sleep_duration )线程等待sleep_duration(5000表示5秒)
  • yield()让步 暂时放弃线程的执行,将主动权 / 时间片 交给其他线程

注意:主线程指main线程

本文由作者按照 CC BY 4.0 进行授权
本页总访问量