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(可调用对象,参数),有一些特殊情况:

  1. 传递引用时
    向线程入口函数传递参数时,一律采用拷贝的方式,不论接受的函数的形参是否为引用,如果要传递引用,需要使用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();
}
  1. 使用智能指针移动构造
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. 使用类成员函数指针
    对象指针存放对象地址,类函数对象指针存放函数代码首地址
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());
    }
};
posted @ 2023-07-31 20:32  启林O_o  阅读(21)  评论(0编辑  收藏  举报