C++11 多线程学习

C++11学习

一、多线程

1、模板线程是以右值传递的

template <class Fn, class... Args> explicit thread(Fn&& fn, Args&&... args)

则需要使用到std::refstd::cref很好地解决了这个问题,std::ref 可以包装按引用传递的值。
std::cref 可以包装按const引用传递的值。

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
using namespace std;
template<class T> void changevalue(T &x, T val) {
	x = val;
}
int main() {
	thread th[100];
	int nums[100];
	for (int i = 0; i < 100; i++)
		th[i] = thread(changevalue<int>, ref(nums[i]), i+1);
	for (int i = 0; i < 100; i++) {
		th[i].join();
		cout << nums[i] << endl;
	}
	return 0;
}

2、线程一些注意事项

线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。
分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
没有执行join或detach的线程在程序结束时会引发异常

3、std::mutex互斥锁

三个方法

lock  上锁
unlock 解锁
bool try_lock()  尝试将mutex上锁。如果mutex未被上锁,则将其上锁并返回true;如果mutex已被锁则返回false。

4、std::atomic & atomic_flag

意思是该类型的变量是原子类型的操作。因为mutex的资源浪费较大,总是要上锁解锁的。引入atomic

std::atomic_int`只是`std::atomic<int>
atomic_int n = 0;  表示定义一个int类型的原子属性的变量n,他是线程安全的
atomic() noexcept = default 默认构造函数 构造一个atomic对象(未初始化,可通过atomic_init进行初始化)
constexpr atomic(T val) noexcept 初始化构造函数 构造一个atomic对象,用val的值来初始化

atomic_flag,原子布尔类型,不同于 std::atomic , std::atomic_flag 不提供加载或存储操作。

成员函数

(构造函数) 构造 atomic_flag (公开成员函数)
operator= 赋值运算符 (公开成员函数)
clear 原子地设置标志为 false (公开成员函数)
test_and_set 原子地设置标志为 true 并获得其先前值 (公开成员函数)
#include <thread>
#include <vector>
#include <iostream>
#include <atomic>
 
 //ATOMIC_FLAG_INIT 置为false
std::atomic_flag lock = ATOMIC_FLAG_INIT;
 
void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
    //std::memory_order_acquire 内存访问
        while (lock.test_and_set(std::memory_order_acquire))  // 获得锁
             ; // 自旋
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // 释放锁
    }
}
 
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f, n);
    }
    for (auto& t : v) {
        t.join();
    }
}

5、std::async&std::future&std::shared_future

std::async定义在future头文件中,新的线程类,执行效率更高于thread,并且可以选择同步异步执行,还有返回值可以选择调用时间。

重载版本 作用
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
async (Fn&& fn, Args&&… args)
异步或同步(根据操作系统而定)以args为参数执行fn
同样地,传递引用参数需要std::refstd::cref
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
async (launch policy, Fn&& fn, Args&&… args);
异步或同步(根据policy参数而定(见下文))以args为参数执行fn,引用参数同上。std::launch有2个枚举值和1个特殊值:枚举值:launch::async、launch::deferred、launch::async
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <future>
using namespace std;
int main() {
	async(launch::async, [](const char *message){
		cout << message << flush;
	}, "Hello, ");
	cout << "World!" << endl;
	return 0;
}

其async的返回值std::future,是一个模板对象。

其成员函数有

一般:T get()
当类型为引用:R& future<R&>::get()
当类型为void:void future::get()
阻塞等待线程结束并获取返回值。
若类型为void,则与future::wait()相同。
只能调用一次。
void wait() const 阻塞等待线程结束
template <class Rep, class Period>
future_status wait_for(const chrono::duration<Rep,Period>& rel_time) const;
阻塞等待rel_time(rel_time是一段时间),
若在这段时间内线程结束则返回future_status::ready
若没结束则返回future_status::timeout
若async是以launch::deferred启动的,则不会阻塞并立即返回future_status::deferred

std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <future>
using namespace std;
void count_big_number() {
	// C++14标准中,可以在数字中间加上单
	// 引号 ' 来分隔数字,使其可读性更强
	for (int i = 0; i <= 10'0000'0000; i++);
}
int main() {
	future<void> fut = async(launch::async, count_big_number);
	cout << "Please wait" << flush;
	// 每次等待1秒
	while (fut.wait_for(chrono::seconds(1)) != future_status::ready)
		cout << '.' << flush;
	cout << endl << "Finished!" << endl;
	return 0;
}

std::shared_future类模板 std::shared_future 提供访问异步操作结果的机制,类似 std::future ,除了允许多个线程等候同一共享状态。不同于仅可移动的 std::future (故只有一个实例能指代任何特定的异步结果),std::shared_future 可复制而且多个 shared_future 对象能指代同一共享状态。

若每个线程通过其自身的 shared_future 对象副本访问,则从多个线程访问同一共享状态是安全的。

#include <iostream>
#include <future>
#include <chrono>
 
int main()
{   
    std::promise<void> ready_promise, t1_ready_promise, t2_ready_promise;
    std::shared_future<void> ready_future(ready_promise.get_future());
 
    std::chrono::time_point<std::chrono::high_resolution_clock> start;
 
    auto fun1 = [&, ready_future]() -> std::chrono::duration<double, std::milli> 
    {
        t1_ready_promise.set_value();
        ready_future.wait(); // 等待来自 main() 的信号
        return std::chrono::high_resolution_clock::now() - start;
    };
 
 
    auto fun2 = [&, ready_future]() -> std::chrono::duration<double, std::milli> 
    {
        t2_ready_promise.set_value();
        ready_future.wait(); // 等待来自 main() 的信号
        return std::chrono::high_resolution_clock::now() - start;
    };
 
    auto result1 = std::async(std::launch::async, fun1);
    auto result2 = std::async(std::launch::async, fun2);
 
    // 等待线程变为就绪
    t1_ready_promise.get_future().wait();
    t2_ready_promise.get_future().wait();
 
    // 线程已就绪,开始时钟
    start = std::chrono::high_resolution_clock::now();
 
    // 向线程发信使之运行
    ready_promise.set_value();
 
    std::cout << "Thread 1 received the signal "
              << result1.get().count() << " ms after start\n"
              << "Thread 2 received the signal "
              << result2.get().count() << " ms after start\n";
}

6、std::promise

是实际上是std::future的一个包装,但是作用确实想实现线程函数的左值引用,将函数执行结果带回来。

一般:
void set_value (const T& val)
void set_value (T&& val)
当类型为引用:void promise<R&>::set_value (R& val)
当类型为void:void promise::set_value (void)
设置promise的值并将共享状态设为ready(将future_status设为ready)
void特化:只将共享状态设为ready
future get_future() 构造一个future对象,其值与promise相同,status也与promise相同
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <future> // 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;
}

6、std::this_thread中的一些静态方法

std::thread::id get_id() noexcept 获取当前线程id
template<class Rep, class Period>
void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration )
等待sleep_durationsleep_duration是一段时间)
void yield() noexcept 暂时放弃线程的执行,将主动权交给其他线程
(放心,主动权还会回来)
template <class Clock, class Duration> void sleep_until (const chrono::time_point<Clock,Duration>& abs_time); 等到到abs_time这个时间到来再执行
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic_bool ready = 0;  //原子类型变量 bool类型  atomic<bool>
// uintmax_t ==> unsigned long long
void sleep(uintmax_t ms) {
	this_thread::sleep_for(chrono::milliseconds(ms));
}
void count() {
	while (!ready) this_thread::yield();//暂时交出cpu执行,
	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;
}

// this_thread::sleep_for example
#include <iostream> // std::cout
#include <iomanip> // std::put_time
#include <thread> // std::this_thread::sleep_until
#include <chrono> // std::chrono::system_clock
#include <ctime> // std::time_t, std::tm, std::localtime, std::mktime
int main()
{
using std::chrono::system_clock;
std::time_t tt = system_clock::to_time_t (system_clock::now());
struct std::tm * ptm = std::localtime(&tt);
std::cout << "Current time: " << std::put_time(ptm,"%X") << '\n';
std::cout << "Waiting for the next minute to begin...\n";
++ptm->tm_min; ptm->tm_sec=0;
std::this_thread::sleep_until (system_clock::from_time_t (mktime(ptm)));
std::cout << std::put_time(ptm,"%X") << " reached!\n";
return 0;
}

7、std::lock_guard & std::unique_lock

成员函数

(构造函数) 构造 lock_guard ,可选地锁定给定的互斥 (公开成员函数)
(析构函数) 析构 lock_guard 对象,解锁底层互斥 (公开成员函数)

lock_guard( mutex_type& m, std::adopt_lock_t t );的构造方法的第二个参数

std::defer_lock_t, std::try_to_lock_t, std::adopt_lock_t 三个值。是用于为 std::lock_guard 、 std::scoped_lock 、 std::unique_lock 和 std::shared_lock 指定锁定策略的空类标签类型。

defer_lock_t不获得互斥的所有权try_to_lock_t尝试获得互斥的所有权而不阻塞adopt_lock_t假设调用方线程已拥有互斥的所有权

构造互斥锁的写法,就是会在lock_guard构造函数里加锁,在析构函数里解锁,达到自动加锁,解锁的目的。因此lock_guard的作用域就是锁的范围。

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
 
mutex mt;
void thread_task()
{
    for (int i = 0; i < 10; i++)
    {
        lock_guard<mutex> guard(mt);
        cout << "print thread: " << i << endl;
    }
}
 
int main()
{
    thread t(thread_task);
    for (int i = 0; i > -10; i--)
    {
        lock_guard<mutex> guard(mt);
        cout << "print main: " << i << endl;
    }
    t.join();
    return 0;
}

虽然lock_guard挺好用的,但是有个很大的缺陷,在定义lock_guard的地方会调用构造函数加锁,在离开定义域的话lock_guard就会被销毁,调用析构函数解锁。这就产生了一个问题,如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。所以为了解决lock_guard锁的粒度过大的原因,unique_lock就出现了。

unique_lock<mutex> unique(mt);

这个会在构造函数加锁,然后可以利用unique.unlock()来解锁,所以当你觉得锁的粒度太多的时候,可以利用这个来解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候一定会解锁,也没有中途解锁的功能。方便肯定是有代价的,unique_lock内部会维护一个锁的状态,所以在效率上肯定会比lock_guard慢。

(构造函数) 构造 unique_lock ,可选地锁定提供的互斥 (公开成员函数)
(析构函数) 若占有关联互斥,则解锁之 (公开成员函数)
operator= 若占有则解锁互斥,并取得另一者的所有权 (公开成员函数)
锁定
lock 锁定关联互斥 (公开成员函数)
try_lock 尝试锁定关联互斥,若互斥不可用则返回 (公开成员函数)
try_lock_for 试图锁定关联的可定时锁定 (TimedLockable) 互斥,若互斥在给定时长中不可用则返回 (公开成员函数)
try_lock_until 尝试锁定关联可定时锁定 (TimedLockable) 互斥,若抵达指定时间点互斥仍不可用则返回 (公开成员函数)
unlock 解锁关联互斥 (公开成员函数)
修改器
swap 与另一 std::unique_lock 交换状态 (公开成员函数)
release 将关联互斥解关联而不解锁它 (公开成员函数)
观察器
mutex 返回指向关联互斥的指针 (公开成员函数)
owns_lock 测试锁是否占有其关联互斥 (公开成员函数)
operator bool 测试锁是否占有其关联互斥
#include <mutex>
#include <thread>
#include <chrono>
 
struct Box {
    explicit Box(int num) : num_things{num} {}
 
    int num_things;
    std::mutex m;
};
 
void transfer(Box &from, Box &to, int num)
{
    // 仍未实际取锁
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
 
    // 锁两个 unique_lock 而不死锁
    std::lock(lock1, lock2);
 
    from.num_things -= num;
    to.num_things += num;
 
    // 'from.m' 与 'to.m' 互斥解锁于 'unique_lock' 析构函数
}
 
int main()
{
    Box acc1(100);
    Box acc2(50);
 
    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
 
    t1.join();
    t2.join();
}

8、condition_variable 条件变量

condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable

-------------有意修改变量的线程必须
获得 std::mutex (常通过 std::lock_guard )
在保有锁时进行修改
在 std::condition_variable 上执行 notify_one 或 notify_all (不需要为通知保有锁)
即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。
-----------任何有意在 std::condition_variable 上等待的线程必须
在与用于保护共享变量者相同的互斥上获得 std::unique_lock<std::mutex>
执行下列之一:
检查条件,是否为已更新或提醒它的情况
执行 wait 、 wait_for 或 wait_until ,等待操作自动释放互斥,并悬挂线程的执行。
condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。
或者
使用 wait 、 wait_for 及 wait_until 的有谓词重载,它们包揽以上三个步骤

std::condition_variable 只可与 std::unique_lock<std::mutex> 一同使用文章来源地址https://uudwc.com/A/rZ6W6

成员函数

(构造函数) 构造对象 (公开成员函数)
(析构函数) 析构对象 (公开成员函数)
operator=[被删除] 不可复制赋值 (公开成员函数)
通知
notify_one 通知一个等待的线程 (公开成员函数)
notify_all 通知所有等待的线程 (公开成员函数)
等待
wait 阻塞当前线程,直到条件变量被唤醒 (公开成员函数)
wait_for 阻塞当前线程,直到条件变量被唤醒,或到指定时限时长后 (公开成员函数)
wait_until 阻塞当前线程,直到条件变量被唤醒,或直到抵达指定时间点 (公开成员函数)
原生句柄
native_handle 返回原生句柄
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    // 等待直至 main() 发送数据
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});//阻塞当前线程,直到条件变量被唤醒 (公开成员函数)
 
    // 等待后,我们占有锁。
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // 发送数据回 main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
    lk.unlock();
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    // 发送数据到 worker 线程
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    // 等候 worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

总结:C++多线程,涉及5个头文件

thread.h
	thread  类 (成员方法)
		thread 构造函数
		join 等待结束
		detach 分离线程,则无需等待结束
	this_thread 命名空间(静态方法)
		get_id  获取当前线程id
		yield 暂时交出cpu执行权
		sleep_for 睡眠等待函数
		sleep_until 等待到下一个具体的时间
mutex.h
	mutex 互斥锁
	lock_guard  配合互斥锁,达到互斥锁作用域范围内的自动上锁解锁
	unique_lock 在lock_guard基础上加了可以手动解锁的接口unlock
	
atomic.h
	atomic  原子化变量
	atomic_flag 原子布尔类型
	
condition_variable.h
	condition_variable
		wait  阻塞当前线程,直到条件变量被唤醒 (公开成员函数)
		wait_for  阻塞当前线程,直到条件变量被唤醒,或到指定时限时长后 (公开成员函数)
		wait_until  阻塞当前线程,直到条件变量被唤醒,或直到抵达指定时间点 (公开成员函数)
		notify_ont  通知一个等待的线程 (公开成员函数)
		notify_all  通知所有等待的线程 (公开成员函数)
	condition_variable_any   std::condition_variable 的泛化
		wait
		wait_for
		wait_until
		notify_ont
		notify_all		
future.h
	future   类似一个针对线程的类模板对象,可以对线程进行管理的。
	shared_future  不同于仅可移动的 std::future (故只有一个实例能指代任何特定的异步结果),std::shared_future 可复制而且多个 shared_future 对象能指代同一共享状态。
	promise  类似future
	packaged_task

原文地址:https://blog.csdn.net/zw1996/article/details/133253455

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

h
上一篇 2023年09月25日 09:13
下一篇 2023年09月25日 09:13