现代C++多线程基础 - std中的线程

处理可调用对象

在C中已经有一个叫做pthread的东西来进行多线程编程,但是并不好用 (如果你认为句柄、回调式编程很实用,那请当我没说),所以c++11标准库中出现了一个叫作std::thread的东西。

C++中有多种可调用对象,他们可以作为参数传给std::bind(),std::thread(), std::async(),std::call_once()等。

编译选项 cmake/make

std::thread 的实现背后是基于 pthread 的。

cmake

CMake中使用pthread的坑与解决方案_cmake 链接pthread-CSDN博客

cmake_minimum_required(VERSION 3.10)

set(CMAKE_CXX_STANDARD 17)

project(cpptest LANGUAGES CXX)

add_executable(cpptest main.cpp)
# 重点是这一步
target_link_libraries(cpptest PUBLIC pthread)

makefile

-lpthread

excute=out
obj=test.cpp

$(excute): $(obj)
	gcc $(obj) -lpthread  -lstdc++ -o $(excute)

clean:
	rm $(excute)

std::thread

std::thread

概述

C++11 开始,为多线程提供了语言级别的支持。用 std::thread 这个类来表示线程。

执行与回收

何时执行与回收?

  • 执行

    • 线程是在thread对象被定义的时候开始执行的。

    • 当那个线程启动时,就会执行这个 lambda 里的内容。

  • 回收

    • join函数只是阻塞等待线程结束并回收资源
    • 线程不是在调用join函数时才执行的,
RAII

作为一个 C++ 类,std::thread 同样遵循 RAII 思想和三五法则。

因为管理着资源,所以

  • 自定义 析构函数
  • 删除 拷贝 构造/赋值函数,
  • 提供 移动 构造/赋值函数。

std::thread对象 所在的函数退出时,就会调用 std::thread 的解构函数,这会销毁线程。

可能导致 std::thread对象的函数还没开始执行,线程就被销毁了。

void myfunc() {
    std::thread t1([&] {
        download("hello.zip");
    });
}

void main() {
    myfunc();
}
构造函数

std::thread 构造函数的参数可以是任意 lambda 表达式。

// 默认构造函数
// 创建一个线程,什么也不做
thread() noexcept;

// 初始化构造函数	
// 创建一个线程,以args为参数,执行fn函数(注意,开始执行)
template <class Fn, class… Args>
explicit thread(Fn&& fn, Args&&… args);

// 移动构造函数
// 构造一个与x相同的对象,会破坏x对象
thread(thread&& x) noexcept;
// 如果对象是joinable的,那么会调用std::terminate()结果程序
thread& operator=(thread &&rhs);
    
// 析构函数	析构对象
~thread();
执行
阻塞等待 join

没有执行join或detach的线程在程序结束时会引发异常

调用join函数(阻塞)

  • 线程不是在调用join函数时才执行的
  • 只是阻塞等待线程结束并回收资源

分离的线程(执行过detach的线程)

  • 在调用它的线程结束或自己结束时释放资源。
    • 线程会在函数运行完毕后自动释放,
    • 不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。

让主线程不要急着退出,等子线程也结束了再退出

// 等待线程结束并清理资源(会阻塞)
void join();
// 返回线程是否可以执行join函数
bool joinable();


std::thread t1([&] {
    download("hello.zip");
});

t1.join();
分离 detach

detach

调用成员函数 detach() 分离该线程

  • 意味着线程的生命周期不再由当前 std::thread 对象管理,
  • 在线程退出以后自动销毁自己。

不过这样还是会在进程退出时候自动退出

// 将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join)
void detach();
void myfunc() {
    std::thread t1([&] {
        download("hello.zip");
    });
    t1.detach();
}

void main() {
    myfunc();
}
全局线程池

detach 的问题是进程退出时,进程不会等待所有子线程执行完毕。

另一种解法是把 t1 对象移动到一个全局变量去,再join它们,从而延长其生命周期到 myfunc 函数体外。

vector

从中取出thread,手动join

std::vector<std::thread> pool;
void myfunc() {
    std::thread t1([&] {
        download("hello.zip");
    });
    pool.push_back(std::move(t1)); // 移交thread的控制权(所有权)给全局变量,延长生命周期
}

void main() {
    myfunc();
    for (auto &t: pool) t.join(); // 手动join
}
线程池

在 main 里面手动 join 全部线程还是有点麻烦,

我们可以自定义一个类 ThreadPool,并用他创建一个全局变量,其解构函数会在 main 退出后自动调用。

class ThreadPool {
    std::vector<std::thread> m_pool;

public:
    void push_back(std::thread thr) {
        m_pool.push_back(std::move(thr));
    }

    ~ThreadPool() {    // 即将离开作用域(进程要结束时),自动调用
        for (auto &t: m_pool) t.join(); // 等待线程池中的线程全部结束
    }
};

ThreadPool tpool;

void myfunc() {
    std::thread t1([&] {
        download("hello.zip");
    });
    tpool.push_back(std::move(t1));  // 移交thread的控制权(所有权)给全局变量,延长生命周期
}

int main() {
    myfunc();
    return 0;
}

this_thread

线程控制自己的方法,在头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,它可以很方便地让线程对自己进行控制。

std::this_thread是个命名空间,所以你可以使用using namespace std::this_thread;这样的语句来展开这个命名空间,不过我不建议这么做。

常用方法
// 获取当前线程id
std::thread::id get_id() noexcept	
    
// 等待sleep_duration(sleep_duration是一段时间)
template<class Rep, class Period>
void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration )	

// 暂时放弃线程的执行,将主动权交给其他线程(主动权还会回来)
void yield() noexcept	

get_id
//获取线程id
std::thread::id get_id();
yield
demo
#include 
#include 
#include 
using namespace std;
atomic_bool ready = 0;
// uintmax_t ==> unsigned long long
void sleep(uintmax_t ms) {
	this_thread::sleep_for(chrono::milliseconds(ms));
}
void count() {
	while (!ready) this_thread::yield();
	for (int i = 0; i <= 20'0000'0000; i++);
	cout << "Thread " << this_thread::get_id() << " finished!" << endl;
	return;
}
int main() {
	thread th[10];
	for (int i = 0; i < 10; i++)
		th[i] = thread(::count);
	sleep(5000);
	ready = true;
	cout << "Start!" << endl;
	for (int i = 0; i < 10; i++)
		th[i].join();
	return 0;
}

promise - thread函数 返回引用

使用场景

std::promise实际上是std::future的一个包装,一般情况下:

  • 使用std::future获取async创建线程的返回值。

  • 使用std::promise获取thread创建线程的返回值。

    如果不想让 std::async 帮你自动创建线程,想要手动创建线程,可以直接用 std::promise

原理

如果使用thread以引用传递返回值的话,就必须要改变future的值,std::future的值不能改变,那么如何利用引用传递返回值?

答:改不了 我新建一个

  • 可以通过promise来创建一个拥有特定值的future

    返回的是新创建的future对象,没有改变任何已有future的值

  • 在外面看起来就是,future的值不能改变,promise的值可以改变。

过程

如何获得thread的返回值?过程是什么样的呢?

  1. 在主线程

    • 声明一个 std::promise 变量。

    • 意味着这个值,虽然现在还没有,但是以后会有的,会有子线程把它填上的。

  2. 在子线程中,

    • 设置在上一步承诺过会被填进去的值。

      调用 std::promiseset_value()方法

  3. 在主线程里,

    • 用 get_future() 获取其 std::future 对象,

    • 进一步 std::future::get() 可以等待并获取线程返回值。

如果线程没有执行完,这里就阻塞。对应的场景就是,哎,你不是说过以后会有返回值的吗?你不能烂尾,就阻塞在这里,弄完了返回值再走。

常用函数
// 默认构造函数 构造一个空的promise对象
promise() 
// 带参构造函数
// 与默认构造函数相同,但使用特定的内存分配器alloc构造对象
template <class Alloc> 
promise(allocator_arg_t aa, const Alloc& alloc)
promise (const promise&) = delete // 复制构造函数	(已删除)
promise (promise&& x) noexcept	// 移动构造函数 构造一个与x相同的对象并破坏x
~promise()	// 析构函数	析构对象
set_value

std::promise 的 set_value() 不接受参数,仅仅作为同步用,不传递任何实际的值。

// set_value函数
// - 设置promise的值
// - 将共享状态设为ready(将future_status设为ready)
// void特化:只将共享状态设为ready

// 一般
void set_value (const T& val)
void set_value (T&& val)
// 当类型为引用
void promise<R&>::set_value (R& val)
// 当类型为void
void promise::set_value (void)
get_future
// 构造一个future对象,
// 其值与promise相同,status也与promise相同
future get_future()
demo

声明保存 int 的promise

void download(std::string file, promise<int> &pret) {
    std::this_thread::sleep_for(std::chrono::milliseconds(400));
    pret.set_value(404);  // 向promise中写入值,创建future
}

int main() {
	//声明一个std::promise对象pret,其保存的值类型为int  
    std::promise<int> pret;
    std::thread t1([&] {
        auto ret = download("hello.zip", pret);
    });
    std::future<int> fret = pret.get_future(); // 从promise中取出future
    int ret = fret.get(); // 从future中取出返回值
    std::cout << "Download result: " << ret << std::endl;

    t1.join();
    return 0;
}

声明保存 string 的promise

void download(std::string file, promise<string> &pret) {
    std::this_thread::sleep_for(std::chrono::milliseconds(400));
    pret.set_value("for promise");  // 向promise中写入值,创建future
    // return 1;
}

int main() {
	//声明一个std::promise对象pret,其保存的值类型为int  
    std::promise<string> pret;
    std::thread t1([&] {
        download("hello.zip", pret);
    });
    std::future<string> fret = pret.get_future(); // 从promise中取出future
    string ret = fret.get(); // 从future中取出返回值
    std::cout << "Download result: " << ret << std::endl;

    t1.join();
    return 0;
}
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include 
#include 
#include  // std::promise std::future
using namespace std;

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...));
}

int main() {
	promise<long long> sum_value;
	thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100);
	cout << sum_value.get_future().get() << endl;
	get_sum.join();
	return 0;
}

std::async

std::async

概念

不同于thread,async是一个模板函数,所以没有成员函数。

出现原因:

  • 同步执行还是异步执行是可以选择的(手动指定 或 交给操作系统选择)
  • 返回值操作比thread更加方便。

std::async 有两种构造函数

// 异步/同步交给操作系统选择
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type> 
    async (Fn&& fn, Args&&… args);
  • 接受入参:带返回值的 lambda、参数
  • 返回值: std::future 对象。
// 异步/同步手动指定选择
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
    async (launch policy, Fn&& fn, Args&&… args);
  • 接受入参:手动选择的policy、带返回值的 lambda、参数
    • 同步执行 launch::deferred
    • 异步执行,创建新线程 launch::async
  • 返回值: std::future 对象。
不指定 policy

视操作系统而定。

// 不显式指定,交给操作系统
std::future<int> fret = std::async([&] {
    return download("hello.zip"); 
});
int ret = fret.get();

// 不指定,显式的交给操作系统
std::future<int> fret = std::async(std::launch::async | std::launch::defereed, [&] {
    return download("hello.zip"); 
});

int ret = fret.get();
指定 policy

异步或同步,根据policy参数而定

std::launch有2个枚举值和1个特殊值:

template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
    async (launch policy, Fn&& fn, Args&&… args);
同步 deferred

std::launch::deferred也就是 0x2

  • 同步启动

    • 创建 std::async 时,不会创建一个线程来执行,

    • 在调用future::getfuture::wait时,才启动函数体

      推迟 lambda 函数体内的运算 到 future 的 get()或 wait() 被调用时

  • 线程的执行仍在主线程中,

    这只是函数式编程范式意义上的异步,而不涉及到真正的多线程。

  • 可以用这个实现惰性求值(lazy evaluation)之类。

std::future<int> fret = std::async(std::launch::deferred, [&] {
    return download("hello.zip"); 
});
// 此时才开始启动
int ret = fret.get();
异步 async

std::launch::async也就是 0x1

  • 异步启动
  • 创建async时,创建一个线程来执行,
// 此时就已经启动
std::future<int> fret = std::async(std::launch::async, [&] {
    return download("hello.zip"); 
});

int ret = fret.get();

future - 返回值

作用

std::future的作用

  • 检测 async 线程是否已结束

  • 获取 async 返回值

    std::async 里 lambda 的返回类型可以为 void, 这时 future 对象的类型为 std::future

  • 作为句柄,让async 阻塞等待/限时等待

    所以对于返回值是void的线程来说,future也同样重要。

int main() {
	// 注:这里不能只写函数名sum,必须带模板参数
	future<int> val = async(launch::async, sum<int, int, int>, 1, 10, 100);
	// future::get() 阻塞等待线程结束并获得返回值
	cout << val.get() << endl;
	return 0;
}
future() noexcept // 默认构造函数
// 	构造一个空的、无效的future对象,但可以移动分配到另一个future对象
future(const future&) = delete // 复制构造函数	(已删除)
future (future&& x) noexcept  // 移动构造函数	构造一个与x相同的对象并破坏x
~future()	// 析构函数
常用方法

需要调用 future 的方法,等待线程执行完毕。

get()

调用次数:只能调用一次

行为:

  • 阻塞等待线程结束并获取返回值

    • 如果还没完成,阻塞等待,

      只要线程没有执行完,会无限等下去。

    • 获取返回值。

  • 若future类型为void,则future::get()future::wait()相同

  • 如果是同步launch::deferred的async,启动的asycn函数

T get() // 一般情况
R& future<R&>::get() // 类型为引用
void:void future::get() // 当类型为void,则与future::wait()相同
std::future<int> fret = std::async([&] {
    return download("hello.zip"); 
});
// 等待线程执行结束
int ret = fret.get();
wait()

调用次数:只能调用一次

行为:

  • 阻塞等待线程结束

    如果还没完成,阻塞等待,只要线程没有执行完,会无限等下去。

  • 不会获取返回值。

    但是可以在future::wait()结束后,再次通过 future::get()获取返回值

  • 如果是同步launch::deferred的async,启动的asycn函数

std::future<int> fret = std::async([&] {
    return download("hello.zip"); 
});
// 等待线程执行结束,不获取返回值
fret.wait();
// 虽然已经结束,但还是可以获取返回值
int ret = fret.get();
wait_for()

调用次数:无限制

行为:

  • 阻塞等待 线程结束/限定时间到

    如果还没完成,阻塞等待

    • 可以用 chrono 里的类表示单位,指定一个最长等待时间。
    • 线程结束/限定时间到 结束阻塞
  • 返回值

    类型: std::future_status 表示等待是否成功。

    • 返回 std::future_status::timeout

      线程在指定的时间内没有执行完毕,放弃等待

    • 返回 std::future_status::ready

      线程在指定的时间内执行完毕,等待成功

  • 如果是同步launch::deferred的async,启动的asycn函数

template <class Rep, class Period>
  future_status wait_for(const chrono::duration<Rep,Period>& rel_time) const;
std::future<int> fret = std::async([&] {
    return download("hello.zip"); 
});

while (true) {
    // 循环多次等待
    auto stat = fret.wait_for(std::chrono::milliseconds(1000));
    if (stat == std::future_status::ready) {
        std::cout << "Future is ready!!" << std::endl;
        break;
    } else {
        std::cout << "Future not ready!!" << std::endl;
    }
}
// 虽然已经结束,但还是可以获取返回值
int ret = fret.get();
wait_until()

wait_until()

  • 同理 wait_for()
  • 其参数是一个时间点。
std::future<int> fret = std::async([&] {
    return download("hello.zip"); 
});

while (true) {
    // 循环多次等待
    auto stat = fret.wait_for(std::chrono::milliseconds(1000));
    if (stat == std::future_status::ready) {
        std::cout << "Future is ready!!" << std::endl;
        break;
    } else {
        std::cout << "Future not ready!!" << std::endl;
    }
}
// 虽然已经结束,但还是可以获取返回值
int ret = fret.get();
std::shared_future

future 为了三五法则,删除了拷贝构造/赋值函数。如果需要浅拷贝,实现共享同一个 future 对象,可以用 std::shared_future。

void download(std::string file) {
    std::this_thread::sleep_for(std::chrono::milliseconds(400));
    std::cout << "Download complete: " << file << std::endl;
}

int main() {
    std::shared_future<void> fret = std::async([&] {
        download("hello.zip"); 
    });
    auto fret2 = fret;
    auto fret3 = fret;

    fret3.wait();
    std::cout << "Download completed" << std::endl;
    return 0;
}

misc

packaged_task 函数返回值

std::packaged_task,绑定一个可调用对象,在未来期望的时候执行。

  • 更方便的,天然异步的函数对象。

  • 将可调对象传递给关联的std::future对象

    • 包装std::promise>中的可调对象T= std::function

    • 可调用对象的执行、返回值获取 需要透过 future

  • std::packaged_task 的返回类型是void

int download02() {
    cout << "start download " << endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(400));
    cout << "set promise value " << endl;
    return 2;
}

int main()
{
    // 给packaged_task绑定函数对象
    std::packaged_task<int()> t(download02);
    // 指定函数对象的future返回值 
    std::future<int> fu2 = t.get_future();
    // 执行packaged_task绑定的函数对象
    t();
    // 先执行之后,才可以获取返回值
    int result = fu2.get();
    std::cout << result << std::endl;
    return 0;
}

考虑这样一个场景:多个线程共享一个任务队列

  • 一个线程负责生产任务
    • 将任务放到任务队列中
    • 在这个任务执行后获取它的返回值.
  • 多个子线程从任务队列中取出任务并执行

这里简化一下这个场景。主线程产生任务,一个子线程t1执行。

std::invoke_result_t

std::packaged_task

std::jthread 自动join

C++20 引入了 std::jthread 类,

和 std::thread 不同在于:

  • 他的解构函数里会自动调用 join() 函数,
  • 保证了解构时会自动等待线程执行完毕。

native_handler

jthread 自动join

std::call_once 执行一次

在多线程的环境下, 某些可执行对象只需要执行一次。std::call_once 应运而生。

  • 封装了加锁和修改flag的过程。简化代码(不再需要判空/添加flag),增强可读性。
  • 本身不具有类似 thread.join() 的功能。需要子线程join 或 主线程进行等待

原理:判断全局变量标识符once_flag。如果这个once_flag

  • “未执行过”
    • 加锁,执行,结束后释放锁mutex
    • 修改标识符为**“执行过”**
  • “执行过” 被标记过。不再执行。
template< class Function, class... Args >
void call_once (std::once_flag& flag, Function&& f, Args&& args... );
// 参数解析:
// std::once_flag flag  判断是个需要执行。若执行,执行后关闭。
// f 和 args... 需要被调用的函数以及函数f的传参
//  lambda 常用 [this]
// 抛出异常 std::system_error if any condition prevents calls to call_once from executing as specified any exception thrown by f
// 初始化资源(节省数组判空)
// 单例模式
#include 
#include 
#include 
#include 
 
std::once_flag flag1;

class PySingleton {
public:
static PySingleton* mInstance;
static PySingleton* get_instance() {
    std::call_once(flag1, [] (int id) {
        mInstance = new PySingleton(id);
    }, 1);
    return mInstance;
}

private:
PySingleton(int id) {
    std::cout << " do init, id = " << id << " \n";
}
};

PySingleton* PySingleton::mInstance = nullptr; // 必须在这里 为static mInstance,编译期执行  


void test() {
    PySingleton* py = PySingleton::get_instance();
}

int main()
{
    std::thread mThread(test);
    mThread.join();
    return 0;
}
#include 
#include 
#include 
#include 
 
std::once_flag flag1;

void do_print1() {
    std::cout << "do_print1 example: called once\n"; 
}
void do_print2() {
    std::cout << "do_print2 example: called once\n"; 
}

void simple_do_once()
{
    std::function<void()> mFunc1 = do_print1;
    std::function<void()> mFunc2 = do_print2;
    std::call_once(flag1, mFunc1); // 仅仅执行了一次这个
    std::call_once(flag1, mFunc2);
}
 
int main()
{
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once); // 什么都没做
    st1.join();
    st2.join();
    return 0;
}

你可能感兴趣的:(#,现代C++多线程,开发语言,c++)