c++并发

《c++并发编程第二版》,一方面翻译的挺拗口,一方面内容有点难度,看的很吃力,尤其到后面内存模型,无锁编程部分,看不下去了,仅记录部分内容。

并发:采用任务切换的方式,一个任务运行一会在切换回另一个任务,在一个时间段内好像两个任务在同时进行
并行:在多个CPU或多核CPU中同一时间多个任务同时在进行(多核CPU中不仅有并行还有并发)

多进程并发:进程可以通过信号,套接字,文件,管道等传递信息
多线程并发:每个进程可以运行多个线程,线程独立运行,共享地址空间(除此之外,线程其实有自己独立的堆栈空间)

线程管理#

启动线程
在对象std::thread创建时,线程启动。thread对象可以用可调用对象构造(可调用对象包括函数,函数指针和引用,lambda表达式,仿函数等,仿函数是类重载了()运算符,使得类的实例可以像函数一样被调用)

等待线程
join()等待线程完成后再继续执行,此时会清除线程存储部分。所以如果join之前有异常退出,不一定保证join()可以正常执行到,可以使用try-catch或使用一个类,在析构函数中join()

Copy
//RAII(Resource Acquisition Is Initialization)“资源获取就是初始化”,C++中管理资源、避免泄漏的惯用法。使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源 class task; class threadGuard { private: std::thread& t; public: explicit threadGuard(std::thread & t):t(t){} ~threadGuard() { if (t.joinable()) { t.join(); } } //禁止对线程的直接拷贝和赋值 threadGuard(threadGuard const&) = delete; threadGuard& operator=(threadGuard const&) = delete; }; int main() { std::thread t(task{}); threadGuard gt(t); std::cout << "Hello World!\n"; }

分离线程(守护线程)
detach()让线程分离,不等待线程结束,线程结束后,自己会回收相关资源

向线程传参
std::thread t(可调用对象,参数),有一些特殊情况:

  1. 传递引用时
    向线程入口函数传递参数时,一律采用拷贝的方式,不论接受的函数的形参是否为引用,如果要传递引用,需要使用std::ref
Copy
void hello(int &v) { std::cout << v<< '\n'; } int main() { int v = 1; std::thread t(hello,std::ref(v));//std::thread t(hello,v)thread构造函数会得到变量拷贝的引用 t.join(); }
  1. 使用智能指针移动构造
Copy
void hello(std::unique_ptr<int> v) { std::cout <<*v << '\n'; } int main() { std::unique_ptr<int> v(new int(1)); std::thread t(hello, std::move(v)); t.join(); }
  1. 使用类成员函数指针
    对象指针存放对象地址,类函数对象指针存放函数代码首地址
Copy
class Hello { public: void hello(int v) { std::cout << v << '\n'; } }; int main() { Hello h; int v = 1; std::thread t(&Hello::hello,&h,v); t.join(); }

获取线程id与获取当前线程id

Copy
std::thread::get_id() std::this_thread::get_id()

使用线程的例子

Copy
#include <iostream> #include<thread> #include<vector> #include<functional> #include <numeric> template<typename Iterator,typename T> struct acculate_block { void operator()(Iterator first, Iterator last, T& result) { //accumulate累加范围内的值 result = std::accumulate(first, last, result); } }; template<typename Iterator,typename T> T parallel_accumulate(Iterator first, Iterator last, T init) { //distance返回他们之间包含的元素个数 unsigned long const length = std::distance(first, last); if (!length) { return init; } unsigned long const min_per_thread = 25;//每线程最少计算几个值 unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;//想要启动线程的最大个数 //std::thread::hardware_concurrency()得到计算机CPU最大支持线程数,与CPU核数及当前运行环境有关 unsigned long const hardware_threads = std::thread::hardware_concurrency(); unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);//启动线程数量与CPU支持线程数与想要启动线程的最大个数最小的有关 unsigned long const block_szie = length / num_threads; std::vector<T> results(num_threads); std::vector<std::thread> threads(num_threads - 1); Iterator block_start = first; for (unsigned long i = 0; i < (num_threads - 1); ++i) { Iterator block_end = block_start; //advance将迭代器前进或者后退几个位置 std::advance(block_end, block_szie); threads[i] = std::thread(acculate_block<Iterator, T>(), block_start, block_end, std::ref(results[i])); block_start = block_end; } acculate_block<Iterator, T>()(block_start, last, results[num_threads - 1]);//计算最后不够block_size的部分 //for_each对范围内的对象执行操作,最后的参数可以使任意可调用对象 //mem_fn将指向对象成员函数的指针包装为对象,定义在include<functional> std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); //std::accumulate定义在include <numeric> return std::accumulate(results.begin(), results.end(), init); } int main() { std::vector<long> a(10000000); for (long i = 0; i < 10000000; ++i) { a[i] = i; } long long result=0; std::cout<<parallel_accumulate(a.begin(), a.end(), result); }

线程间数据共享#

条件竞争:当多个线程同时访问改变共享数据,数据最终结果取决于线程执行的顺序
恶性条件竞争发生于对多个数据修改时,避免恶性竞争方法:

  • 无锁编程
  • 使用事务
  • 使用互斥量(即上锁,但是会造成死锁)

使用互斥量保护共享数据#

std::mutex创建锁实例,lock()对互斥量上锁,unlock()解锁,c++标准库提供了RAII语法的模板类std::lock_guard,构造时上锁,析构时解锁

Copy
std::mutex m; std::lock_guard<std::mutex> l(m); //因为17中可以进行模板类参数推导所以也可以写成 std::lock_guard l(m); //17还定义了更严格的std::scope_lock //std::lock()可以一次性锁住多个互斥量 std::lock(a,b); std::lock_guard<std::mutex> locka(a,std::adopt_lock); std::lock_guard<std::mutex> lockb(b,std::adopt_lock); //std::adopt_lock表示将锁给lock_guard管理。不用他自己创建了 //作为之上的替代17引入 std::scoped_lock<std::mutex,std::mutex> lockab(a,b);

更灵活的锁std::unique_lock
具有lock_guard相同的功能,并且还能随时加锁解锁,创建时可以不锁定(通过指定参数为std::defer_lock)

Copy
std::unique_lock<std::mutex> locka(a,std::defer_lock); std::unique_lock<std::mutex> lockb(b,std::defer_lock); std::lock(a,b);

死锁
当一个操作需要两个及以上的互斥量时可能发生:每个线程都持有一个互斥量,相互等待对方释放互斥量

避免死锁的注意:

  • 避免嵌套锁
  • 避免在持有锁时调用用户代码
  • 使用固定顺序获取锁
  • 使用层次锁:只有低层锁没有上锁时,高层锁才能上锁

使用stad::once_flag和std::call_once保护共享数据#

用于共享数据的初始化保护

Copy
if(!c) c=new C(); c.do()

当a线程开始创建C的实例一半时,b线程判断c实例已经存在,就会使用一个未初始化完成的实例进行c.do(),从而引发错误
可以使用std::once_flag和std::call_once

Copy
std::once_flag f; void n(){ //初始化一个对象 } std::call_once(f,n);//输入可以是任何可调用对象

使用读写锁保护共享数据#

只读时可以共享,写时独占

Copy
std::shared_mutex entry_mutex;//还有std::shared_timed_mutex void read(){ std::shared_lock<std::shared_mutex> l(entry_mutex); //读 } void write(){ std::lock_guard<std::shared_mutex> l(entry_mutex); //写 }

线程同步#

方式:

  • 轮询:不断检查某个互斥量是否可以访问(当等待的线程完成时,该互斥量可以被访问)
  • 等待:线程完成后通知正在等待的线程(c++标准库使用这种机制,称为条件变量)

条件变量#

std::condition_variable与std::condition_variable_any,需要与互斥量配合,condition_variable只能和std::mutex配合
PerpareData线程通过notify_one()通知正在等待的线程
ProcessData线程通过wait()等待被唤醒,第二个参数为可调用对象,作为这个线程是否被唤醒的条件,wait会检查其返回,如果为false就解锁继续等待,如果为true继续执行

Copy
std::mutex mut; std::queue<int> data; std::condition_variable condition;//条件变量 void PrepareData() { int i = 0; while (i!=100) { std::cout << "准备数据\n"; data.push(i); condition.notify_one(); i++; } } void ProcessData() { while (true) { std::unique_lock l(mut); condition.wait(l, [] {return !data.empty(); }); std::cout << data.front() << '\n'; data.pop(); l.unlock(); } } int main() { std::thread tPrepaer(PrepareData); std::thread tProcess(ProcessData); tProcess.join(); tPrepaer.join(); }

期望#

一个条件变量不是“等待一次之后,就不再等待”这样场景的最好选择,所以进一步有future期望值
future期望(std::future一个线程等待一个事件与std::shared_futured多个线程等待一个事件)
std::async异步调用会返回future对象(相比std::thread,它就不提供直接返回值的机制),std::launch::deferred表示等调用get()或wait()时候再创建线程执行std::launch::async表示立即创建线程执行,如果不加这些参数可能立即执行,也可能不立即执行(因为可能因为系统资源紧张,创建失败)

Copy
int fun(int v) { std::this_thread::sleep_for(std::chrono::seconds(v)); std::cout << v << "号完成\n"; return v; } int main() { std::future<int> back = std::async(std::launch::deferred, fun, 1);//等调用get()或wait()时候再执行 //std::future<int> back = std::async(std::launch::async,fun, 1);//另开一个在新线程上执行 std::cout << "我有更重要的是要做\n"; std::this_thread::sleep_for(std::chrono::seconds(3)); std::cout << "我做完了你呢?"<< back.get() << '\n'; std::cout << "我还有更重要的是要做\n"; }

std::packaged_task
可以通过std::packaged_task将要处理的函数封装成这样一个可调用对象,通过get_future等待对象执行的返回值

Copy
int fun(int v) { std::this_thread::sleep_for(std::chrono::seconds(v)); std::cout << v << "号完成\n"; return v; } int main() { std::packaged_task<int(int)> task(fun); std::future<int> res=task.get_future(); task(5); }

std::promise
与future可以组合成一种线程同步的手段
通过get_future()与promise关联,通过set_value设置promise的值,通过get得到promise的值,通过set_exception将异常传入promise

Copy
void source(std::promise<int>& p) { std::cout << "t1执行\n"; std::this_thread::sleep_for(std::chrono::seconds(3)); p.set_value(3); std::cout << "t1设置了给main的值" <<3<<'\n'; } void tail( std::promise<int>& p) { std::cout << "t2执行\n"; std::this_thread::sleep_for(std::chrono::seconds(4)); std::future<int> res = p.get_future(); std::cout<<"t2得到了main设置的值" << res.get() << '\n'; } int main() { std::promise<int> p1; std::future<int> res = p1.get_future(); std::thread t1(source, std::ref(p1)); t1.detach(); int v = res.get(); std::cout << "main得到了t1设置的值"<<v<<'\n'; std::promise<int> p2; std::thread t2(tail, std::ref(p2)); t2.detach(); p2.set_value(v+1); std::cout << "main设置了给t2的值" << v+1 << '\n'; std::this_thread::sleep_for(std::chrono::seconds(10)); std::cout << "全部完成\n"; }

时钟#

std::chrono::system_clock代表系统时钟,now()返回系统时钟当前时间
标准库提供稳定时钟std::chrono::steady_clock

指定超时有两种方式:时延与时间点
时延
时延是过一段时间超时,使用std::chrono::duration<参数1,参数2>指定
参数1指定保存的类型,如int等
参数2指定时延单位,如std::ratio<60,1>表示单位为1分钟,如std::chrono::duration < int, std::ratio<60, 1>>a(10);表示10分钟
也有预定义的单位如秒std::chrono::seconds a(10),微秒等
时延支持大单位向小单位隐式转换,显示转换通过std::chrono::duration_cast<>采用截断方式完成
时延支持时延间运算
基于时延的等待

Copy
//就绪转态std::future_status::ready和超时状态std::future_status::timeout及任务延迟std::future_status::defered std::future<int> f=std::async(); if(f.wait_for(std::chrono::milliseconds(35)==std::future_status::ready)) { f.get(); }

时间点
std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds> a(std::chrono::seconds(1578466970))
也可以std::chrono:;steady_clock::now()+std::chrono::milliseconds(500)设置时间点
使用wait_until等待超时

内存模型#

每个变量是一个对象
每个对象至少占一个内存位置
基本类型都有确定的内存位置
相邻位域是相同内存的一部分
c++6种内存序
enum memory_order {
memory_order_relaxed,//宽松内存序
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst//顺序一致性模型 保证写必须在读之前发生
};

原子操作与类型#

原子类型std::atomic
其中T是trivially copyable type满足:要么全部定义了拷贝/移动/赋值函数,要么全部没定义;没有虚成员;基类或其它任何非static成员都是trivally copyable

Copy
#include<atomic> #include<iostream> int main() { std::atomic<int> a; std::atomic<int> c; //赋值 a.store(3); c.store(a); //对于指针的整数支持加和或操作 a.fetch_add(2); //转化为对应类型 int b=a.load(); std::cout << b; //交换 a.exchange(c); int d = c.load(); std::cout << d; }

阻塞与非阻塞
阻塞:使用互斥量,条件变量,期望可以同步阻塞算法和数据结构。阻塞时,阻塞库会挂起线程
非阻塞:不会调用阻塞库挂起线程。并不是无锁的,如自旋锁

自旋锁

Copy
class spinlock_mutex { std::atomic_flag flag; public: spinlock_mutex():flag(ATOMIC_FLAG_INIT){} void lock() { while(flag.test_and_set(std::memory_order_acquire)); } void unlock() { flag.clear(std::memeory_order_release); } }

原子操作的内存顺序

比较交换操作CAS——原子编程的基础
参考:https://zhuanlan.zhihu.com/p/412680378

compare_exchange_weak()与compare_exchange_strong()
compare_exchange_weak()可能由于时间片到了,发生伪失败,所以一般使用循环
compare_exchange_strong()内部使用一个循环,避免伪失败

Copy
// 如果a与except相同,则使用new_value更新a的值,并且返回true // 如果a与except不同,则返回fasle std::atomic<bool> a(true); bool except=false; vool new_value = false; std::cout<<a.compare_exchange_weak(except, new_value);

线程安全数据结构#

基于锁的线程安全的哈希表

Copy
#include <iostream> #include<list> #include <functional> #include<mutex> #include<shared_mutex> #include<algorithm> #include <memory> #include<thread> template<typename Key, typename Value, typename Hash = std::hash<Key>> class ThreadsafeTable { public: class Bucket { private: typedef std::pair<Key, Value> bucket_value; typedef std::list<bucket_value> bucket_data; typedef typename bucket_data::iterator bucket_iterator; bucket_data data; mutable std::shared_mutex m;//这里的mutable //书中这里加了const限定后缀,但是报错,去掉后可以运行 bucket_iterator find_entry_for(Key const& key) { return std::find_if(data.begin(), data.end(), [&](bucket_value const& item) {return item.first == key; }); } public: Value Find(Key const& key, Value const& default_value) const { std::shared_lock<std::shared_mutex> lock(m); bucket_iterator const found_entry = find_entry_for(key); return (found_entry == data.end()) ? default_value : found_entry->second; } void Insert(Key const& key, Value const& value) { std::unique_lock<std::shared_mutex> lock(m); auto found_entry = find_entry_for(key); if (found_entry == data.end()) { data.push_back(bucket_value(key, value)); } else { found_entry->second = value; } } void Remove(Key const& key) { std::unique_lock<std::shared_mutex> lock(m); bucket_iterator const found_entry = find_entry_for(key); if (found_entry != data.end()) { data.erase(found_entry); } } }; std::vector <std::unique_ptr<Bucket>> buckets; Hash hasher; Bucket& GetBucket(Key const& key) const { std::size_t const bucket_index = hasher(key) % buckets.size(); return *buckets[bucket_index]; } public: ThreadsafeTable(unsigned num_buckets = 19, Hash const& hasher = Hash()) :buckets(num_buckets), hasher(hasher) { for (unsigned i = 0; i < num_buckets; ++i) { buckets[i].reset(new Bucket); } } ThreadsafeTable(ThreadsafeTable const& other) = delete; ThreadsafeTable& operator=(ThreadsafeTable const& other) = delete; Value Find(Key const& key, Value const& default_value = Value()) const { return GetBucket(key).Find(key, default_value); } void Insert(Key const& key, Value const& value) { GetBucket(key).Insert(key, value); } void Remove(Key const& key) { GetBucket(key).Remove(key); } }; int main() { ThreadsafeTable<int, std::string, std::hash<int>> table; std::vector<std::thread> threads; for (int i = 0; i < 50; ++i) { threads.emplace_back([i, &table]() {table.Insert(i, std::to_string(i)); }); } for (int i = 0; i < 50; ++i) { threads[i].join(); } }

对于链表可以对每个节点上锁,通过手递手的方式上锁:先锁当前节点,获取下一节点并上锁后,释放上一节点的锁

无锁的线程安全的栈
无锁主要基于原子操作。线程可以并发访问数据结构,但不能进行相同操作。当其中一个线程被挂起,其它线程依然可以继续完成自己的工作。

无锁的优点:

  • 提高并发,线程不必阻塞或等待
  • 更健壮,线程在持有锁的同时死亡会破坏数据结构,无锁的线程死亡只会影响自己的线程
    不会出现死锁问题(但会出现活锁问题(一个线程会让另一个线程重启,过独木桥),但活锁问题只稍微损耗性能,无等待数据结构可以解决活锁问题)

无锁的缺点:

  • 无锁尽管使用了原子操作,但可能比不使用原子操作的慢
  • 硬件需要通过原子变量同步线程
Copy
//还要检查内存序 template<typename T> class lock_free_stack { private: struct node { std::shared_ptr<T> data; std::experimental::atomic_shared_ptr<node> next; node(std::shared_ptr<T> data_):data(data_){} }; std::experimental::atomic_shared_ptr<node> head;// 或者在之后使用atomic_compare_exchange_weak() public: void push(T const& data) { std::shared_ptr<node> const new_node=std::make_shared<node>(data); new_node->next=head.load(); while(!head.compare_exchange_weak(new_node->next,new_node)); } std::shared_ptr<T> pop() { std::shared_ptr<node> old_head=head.load(); while(old_head&& !head.compare_exchange_weak(old_head,old_head->next.load())); if(old_head) { old_head->next=std::shared_ptr<node>(); return old_head->data; } return std::shared_ptr<T>(); } ~lock_free_stack() { while (pop()); } };
posted @   启林O_o  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示
CONTENTS