c++并发
《c++并发编程第二版》,一方面翻译的挺拗口,一方面内容有点难度,看的很吃力,尤其到后面内存模型,无锁编程部分,看不下去了,仅记录部分内容。
并发:采用任务切换的方式,一个任务运行一会在切换回另一个任务,在一个时间段内好像两个任务在同时进行
并行:在多个CPU或多核CPU中同一时间多个任务同时在进行(多核CPU中不仅有并行还有并发)
多进程并发:进程可以通过信号,套接字,文件,管道等传递信息
多线程并发:每个进程可以运行多个线程,线程独立运行,共享地址空间(除此之外,线程其实有自己独立的堆栈空间)
线程管理
启动线程
在对象std::thread创建时,线程启动。thread对象可以用可调用对象构造(可调用对象包括函数,函数指针和引用,lambda表达式,仿函数等,仿函数是类重载了()运算符,使得类的实例可以像函数一样被调用)
等待线程
join()等待线程完成后再继续执行,此时会清除线程存储部分。所以如果join之前有异常退出,不一定保证join()可以正常执行到,可以使用try-catch或使用一个类,在析构函数中join()
//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(可调用对象,参数),有一些特殊情况:
- 传递引用时
向线程入口函数传递参数时,一律采用拷贝的方式,不论接受的函数的形参是否为引用,如果要传递引用,需要使用std::ref
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();
}
- 使用智能指针移动构造
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();
}
- 使用类成员函数指针
对象指针存放对象地址,类函数对象指针存放函数代码首地址
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
std::thread::get_id()
std::this_thread::get_id()
使用线程的例子
#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,构造时上锁,析构时解锁
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)
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保护共享数据
用于共享数据的初始化保护
if(!c)
c=new C();
c.do()
当a线程开始创建C的实例一半时,b线程判断c实例已经存在,就会使用一个未初始化完成的实例进行c.do(),从而引发错误
可以使用std::once_flag和std::call_once
std::once_flag f;
void n(){
//初始化一个对象
}
std::call_once(f,n);//输入可以是任何可调用对象
使用读写锁保护共享数据
只读时可以共享,写时独占
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继续执行
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表示立即创建线程执行,如果不加这些参数可能立即执行,也可能不立即执行(因为可能因为系统资源紧张,创建失败)
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等待对象执行的返回值
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
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<>采用截断方式完成
时延支持时延间运算
基于时延的等待
//就绪转态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
#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;
}
阻塞与非阻塞
阻塞:使用互斥量,条件变量,期望可以同步阻塞算法和数据结构。阻塞时,阻塞库会挂起线程
非阻塞:不会调用阻塞库挂起线程。并不是无锁的,如自旋锁
自旋锁
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()内部使用一个循环,避免伪失败
// 如果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);
线程安全数据结构
基于锁的线程安全的哈希表
#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();
}
}
对于链表可以对每个节点上锁,通过手递手的方式上锁:先锁当前节点,获取下一节点并上锁后,释放上一节点的锁
无锁的线程安全的栈
无锁主要基于原子操作。线程可以并发访问数据结构,但不能进行相同操作。当其中一个线程被挂起,其它线程依然可以继续完成自己的工作。
无锁的优点:
- 提高并发,线程不必阻塞或等待
- 更健壮,线程在持有锁的同时死亡会破坏数据结构,无锁的线程死亡只会影响自己的线程
不会出现死锁问题(但会出现活锁问题(一个线程会让另一个线程重启,过独木桥),但活锁问题只稍微损耗性能,无等待数据结构可以解决活锁问题)
无锁的缺点:
- 无锁尽管使用了原子操作,但可能比不使用原子操作的慢
- 硬件需要通过原子变量同步线程
//还要检查内存序
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());
}
};