MultiThread笔记

MultiThread是自己在B站学习C++多线程,跟老师写的代码
b站up主mkuangxiang
https://www.bilibili.com/video/BV1Yb411L7ak
而这个文件是对代码再次阅读时的笔记,削去多余的东西,方便对知识点的检索

  • windows_critical_section

参考:windows_critical_section1.cpp
函数调用函数,函数里也有std::recursive_mutex.lock
可使用:

std::lock_guard<std::recursive_mutex> my_Recursive_Mutex_Guard(m_recursiveMutex);
testfunc1();            //会加三次锁  
//而testfun1()中,连续调用两次的  
void A::testfunc1()
{
    std::lock_guard<std::recursive_mutex> myGuard(m_recursiveMutex);
    cout << "testfunc1() is do something" << endl;
    testfunc2();
}

void A::testfunc2()
{
    std::lock_guard<std::recursive_mutex> myGuard(m_recursiveMutex);
    cout << "testfunc2() is do something" << endl;
}  

windows_critical_section2.cpp中提出请求互斥锁,按时间的去try_lock,超时走掉
std::timed_mutex:超时功能的独占互斥锁
try_lock_for()超时没拿到锁就自己走了,不会阻塞
try_lock_unti()
std::recursive_timed_mutex:超时功能的递归独占互斥量
使用如下

std::chrono::milliseconds timeout(100);
if (m_recursive_Timed_Mutex.try_lock_for(timeout))
{
    m_msgRecvQueue.push_back(i);
    m_recursive_Timed_Mutex.unlock();
}
有前文铺垫  
std::recursive_timed_mutex m_recursive_Timed_Mutex;

设置超时的一种变种
std::chrono::steady_clock 为了表示稳定的时间间隔,后一次调用now()得到的时间总是比前一次的值大
(这句话的意思其实是,如果中途修改了系统时间,也不影响now()的结果),每次tick都保证过了稳定的时间间隔
铺垫知识:
steady_clock::time_point t1 = steady_clock::now();
设置超时时间的变种

if (m_recursive_Timed_Mutex.try_lock_until(chrono::steady_clock::now() + timeout))      //与上面一行等价,当前时间算起多加100ms
{
    m_msgRecvQueue.push_back(i);
    m_recursive_Timed_Mutex.unlock();
}
  • Thread_Vector

ThreadCommunicate2 演示共享数据的--以保护方式来访问
注意outMsgLULProc中unlock有两处,因为可能从两处路径返回
注意.empty()也是保护访问的方式

std::lock_guard<std::mutex>的方式会解决上面可能是多种路径返回的问题,不用担心提前return的时候没有解锁
Thread_std_lock.cpp中

std::lock(m_myMutex1, m_myMutex2);//等价于每个互斥量都.lock().这是部分的代码段
m_msgRecvQueue.push_back(i);
m_myMutex2.unlock();
m_myMutex1.unlock();  

std::lock与std::lock_guard配合使用,注意std::lock_guard第二参数
std::lock(m_myMutex2, m_myMutex1);          //等价于每个互斥量都.lock()
std:;lock_guard<std::mutex> stdGuard1(m_myMutex1, std::adopt_lock);     //前一语句已经把.lock()调用了,这里所以std::adopt_lock
std::lock_guard<std::mutex> stdGuard2(m_myMutex2, std::adopt_lock);     //注意是stdGuard2,易写成stdGuard1

Thread_unique_lock1.cpp中
能不能有种机制让outMsgLULProc()知道自己想拿的锁处在何种状态呢?占有锁的线程是在执行还是处于拿到锁没干活阻塞起的呢?

std::unique_lock<std::mutex> stdGuard2(m_myMutex1, std::try_to_lock);
if (stdGuard2.owns_lock())                      //成功拿到了锁
或者这么使用  
std::unique_lock<std::mutex> uniquelock(m_myMutex1, std::defer_lock);
bool getLock = uniquelock.try_lock();
if(getLock)                     //成功拿到了锁  
  • thread_test

简单,没重复看的必要

  • Thread_async_future_packaged_task_promise

Thread_async_future_packaged_task_promise.cpp中
比较总结性的说明了下std::async,std::future的关系,以及std::future的特点

Thread_async_future_packaged_task_promise3.cpp
注意,把打包对象按std::ref()传入线程对象
像这样

std::packaged_task<int(int)> mypt([](int mypar) {       //包装lambda表达式
                                                cout << mypar << endl;
                                                cout << "myThread is running " << "thradid is:" << std::this_thread::get_id() << endl;
                                                std::chrono::milliseconds dura(5000);
                                                std::this_thread::sleep_for(dura);                  //不用window.h的Sleep();
                                                cout << "myThread is over " << "thradid is:" << std::this_thread::get_id() << endl;
                                                return 5; }
                                    );
std::thread t1(std::ref(mypt), 3);
t1.join();
std::future<int> result = mypt.get_future();
cout << result.get() << endl;  

思考下,之前的代码对打包对象是如何启动的?是否使用引用

Thread_async_future_packaged_task_promise4.cpp

mytask.push_back(std::move(mypt));                      //使用移动语义,mypt的所有权才为空
如果mytask.emplace(mypt);是否可以不显式地使用std::move()语义?    
注意这里有声明
vector<packaged_task<int(int)>> mytask;
以及销毁容器中的可调对象
temmypt = std::move(*iter);
mytask.erase(iter);                                     //后续将不能再使用iter这个迭代器
 
//Thread_async_future_packaged_task_promise5.cpp
演示了std::future<T> std::promise<T>作为参数传入子线程的用法,比较复杂    
可以理解成利用std::promise在两个线程之间通信的做法
  • Singleton pattern

Singleton pattern.cpp中
可以用模板自己实现个单例类,注意类里面嵌套了类型声明

class MyClass
{
public:
    ~MyClass() {};
    static MyClass* GetInstance();
    class Reclaim 
    {
    public:
        ~Reclaim()
        {
            if (MyClass::m_instance)                        //注意嵌套类内写代码,变量前加限定域::,容易搞忘
            {
                delete MyClass::m_instance;
                MyClass::m_instance = NULL;
            }
        }
    };
private:
    MyClass() {};
    static MyClass* m_instance;
};

MyClass* MyClass::m_instance = NULL;


MyClass* MyClass::GetInstance()
{
    //if(m_instance ==NULL)
    if (!m_instance)
    {
        m_instance = new MyClass();
        static Reclaim cl;                      //小技巧,声明一个静态成员,程序结束时它被销毁时,跟到调用Reclaim的析构函数
    }
    return m_instance;
}  

自己实现的模板形式的singleton.h

#pragma once

template<typename T>
class SingLeton
{
public:
    static T* get_instance()
    {
        static T* p = nullptr;
        if (nullptr == p)
        {
            p = new T();
            static ReClaim<T> reclaim(&p);
        }                   
        return p;
    } 

    template<typename T>
    class ReClaim 
    {
    public:
        ReClaim(T** _p) :p(_p) {}
        ~ReClaim() { delete *p; *p = nullptr; p = nullptr; }
    private:
        T** p;
    };

private:
    SingLeton();
};

Singleton pattern1.cpp对上面创建唯一的实例GetInstance()函数里做了创建的保护工作
作者做了个小技巧,提高互斥访问效率的方法,首先会先在外层判断,如果已经创建了唯一的实例(创建唯一实例只能有一遍的机会,而判断后放弃创建是比较常见的情况)
就不会继续下一步去拿锁std::unique_lock<std::mutex> myMutex(resourse_mutex);
代码段:

if (!m_instance)                                //对上面代码逻辑上小改进,提高效率.双重检查
{                                               //注意!在不加锁,即没下一行加锁的情况下,m_instance == NULL;并不能说明MyClass没被new过;因为多线程,且这部分(即这行周边的代码)的操作不是原子操作
    std::unique_lock<std::mutex> myMutex(resourse_mutex);   
    if (!m_instance)                            //根据上面的表述,所以才会把判断放在互斥保护这里来
    {
        m_instance = new MyClass();
        static Reclaim cl;                      //小技巧,声明一个静态成员,程序结束时它被销毁时,跟到调用Reclaim的析构函数
    }
}

Singleton pattern2.cpp
引入std::call_once()

#include <mutex>
std::once_flag g_flag;
std::call_once(g_flag, CreateInstance);         //两个线程同时执行到此时,自动作原子操作,上锁,其他没拿到锁的在此等待,等待CreateInstance()执行完毕,其他线程随后没法执行CreateInstance()

Singleton pattern3.cpp
注意:要在for()循环内使用std::unique_lock<std::mutex>

for (int i = 0; i < 10000; i++)
{
    cout << "inMsgRecvQueue() is running,Insert " << i << endl;
    std::unique_lock<std::mutex> uniquelock(m_myMutex1);
    std::chrono::milliseconds dura(1000);                               //即将睡1s
    std::this_thread::sleep_for(dura);

    m_msgRecvQueue.push_back(i);
}

Singleton pattern4.cpp引入std::condition_variable

class A
{
    in_message();
    out_message();
private:
    std::list<int> my_list;
    std::mutex my_mutex;
    std::condition_variable my_cond;
};  

void A::in_message()
{
    std::unique_lock<std::mutex> my_lock(my_mutex);
    my_list.emplace_back(); 
    my_cond.notify_one();
}

void A::out_message()
{
    std::unique_lock<std::mutex> my_lock(my_mutex);
    my_cond.wait(my_lock, [this](){if(my_list.empty()) return false; return true;})     //第二参数防虚假唤醒
    my_list.pop_front();
    my_lock.unlock();       //可以提前unlock(),非必须的操作,主要是为之后的操作考虑,之后的操作不涉及共享数据则可以提前unlock()
}  

从别人博客处扩展
std::condition_variable_any与 std::condition_variable 类似,只不过 std::condition_variable_any 的 wait 函数可以接受任何 lockable 参数
而 std::condition_variable 只能接受 std::unique_lock<std::mutex> 类型的参数
除此以外,和 std::condition_variable 几乎完全一样。
condition_variable参考博客https://www.cnblogs.com/haippy/p/3252041.html

  • Shared_Ptr_Test

这里有两个shared_ptr和unique_ptr的小示例
shared_ptr里有解决返回自身共享指针造成引用计数还是为1的办法
两种办法:

    1. 类修饰成enable_shared_from_this<Person>
      调用return shared_from_this();
    1. 封装个shared_ptr<>返回
      return shared_ptr<Person>(this);

注意移动语义的典型例子
Shared_Ptr.cpp中
为类型增加个成员函数,并在合适的情境下调用它

struct Person :enable_shared_from_this<Person>
//struct Person                             //二选一,只能开启一个    `                                       
{
    void show() {
        cout << str << "\n";
    }
    shared_ptr<Person> getShared() {
        //return shared_ptr<Person>(this);  //配合到上面结构体的声明
        return shared_from_this();
    }
    string str;
};  

调用:  
shared_ptr<Person>  ptr1;
{
    shared_ptr<Person> ptr2(new Person);    
    ptr1 = ptr2->getShared();           //两个智能指针的引用计数增加
    ptr1 = std::move(ptr2);             //移动语义将原对象也转移ptr1引用计数为1,ptr2的引用计数为0
}

Shared_Ptr_Test.cpp中
注意:
之前一直记得unique_ptr是没有make_shared<>()方法的,但它有配套的make_unique<>()

//确实可以
std::unique_ptr<int> np_int;
np_int = std::make_unique<int>(10);

这个回顾下CPP_Primer中创建unique_ptr
auto node = make_unique<Node<T>>();
注意,这里有std::move()更典型的用法,它根本就不能用operator=(),引用计数只能0或1
而且,cpp文件实现了一个头插法的链表,简洁易懂
前插缺乏:
1.声明都不插的情况下
Link成员Node<T> head; //初始化缺乏
2.前插需要检查Node<T> head成员是否为nullptr(也即有上面一行的铺垫),若head为空,则新插进的节点不必指向下一节点
因为除了头节点(它不保存数据,只保存指向第一个实节点的指针),和才插入的实节点,没有其他节点了(实节点后面没有节点了)

  • shared_future_atomic

async的第一参数决定是否以多线程,异步的方式启动新线程
enum class launch { // names for launch options passed to async
async = 0x1,
deferred = 0x2
};
上面是future原文

记到,std::async(std::launch::async, )是为了创建std::future<T>对象
std::future<int> result = std::async(std::launch::deferred,myThread); //这里不会阻塞,异步线程即使不调用join(),在主函数返回前也会等待它执行完std::future<>的wait_for()方法
get()方法启动线程,并等待线程执行完
//shared_future_atomic.cpp中使用

std::future_status status = result.wait_for(std::chrono::seconds(6));       //会等待线程1秒钟,并检查线程状态
if (std::future_status::timeout == status)                                  //它有三种状态:ready;timeout;deferred;
{
    cout << "经过result.wait_for()状态检查得出myThread()超时" << endl;
}
else if(std::future_status::ready == status)
{
    cout << "经过result.wait_for()状态检查得出myThread()执行完毕" << endl;
}
else if (std::future_status::deferred == status)                            //async的第一个参数被置为std::launch::deferred,则本条件成立
{                                                                           //注意:如果延迟执行,async中创建的线程myThread不会主动执行,还必须result.get(),注意它不开启子线程,而在主线程中执行
    cout << "线程被延迟了,但现在还未执行,下面调用result.get()" << endl;
    result.get();
}  

注意,这个方法很智能,能检查异步运行的子线程的状态,比单单的std::thread()启动一个线程要更高级

稍微复杂点,对上面的辨析,std::thread启动的线程也能检测运行状态,即获取std::future<T>,但前提条件是启动的是包装对象
将函数封装成包装对象
//shared_future_atomic1.cpp中
注意,线程对象std::thread能用join(),能100%使线程运行并阻塞(线程若未执行完,就block,直到运行完)
而std::future<T>对象的get()方法,如果,线程是由std::async()启动,则get()实际会起到启动线程,并等待子线程结束并接受子线程返回值的作用
std::future<T>对象的方法
扩展下,摘录别人的博客
一个有效(valid)的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联。Provider 可以是函数或者类,其实我们前面都已经提到了,他们分别是:

  1. std::async 函数,本文后面会介绍 std::async() 函数。
  2. std::promise::get_future,get_future 为 promise 类的成员函数,详见 C++11 并发指南四(<future> 详解一 std::promise 介绍)。
  3. std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数,详见C++11 并发指南四(<future> 详解二 std::packaged_task 介绍)。

一个 std::future 对象只有在有效(valid)的情况下才有用(useful),由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被 move 赋值另一个有效的 future 对象)。

在一个有效的 future对象上调用get()会阻塞当前的调用者,直到Provider设置了共享状态的值或异常(此时共享状态的标志变为 ready),std::future::get将返回异步任务的值或异常(如果发生了异常)。

铺垫知识:
std::future::valid()
检查当前的 std::future 对象是否有效,即释放与某个共享状态相关联。一个有效的 std::future 对象只能通过 std::async(), std::future::get_future 或者 std::packaged_task::get_future 来初始化。

继续扩展,关键:
std::future::share()
返回一个 std::shared_future 对象(本文后续内容将介绍 std::shared_future ),调用该函数之后,该 std::future 对象本身已经不和任何共享状态相关联,因此该 std::future 的状态不再是 valid 的了。
所以新接收的std::shared_future<T>可以多次get(),像下面这样

std::shared_future<int> result_s(result.share());       //因为这里将std::future<int>转译成std::shared_future<int>类型所以给下面的std::thread t2(myThread1,)第二参数应是result_s,不要忘了函数定义中参数类型也需要改造
auto threadResult  = result_s.get();
auto threadResult1 = result_s.get();      

下面跟到了解std::shared_future<T>对象
std::shared_future 与 std::future 类似,但是 std::shared_future 可以拷贝、多个 std::shared_future 可以共享某个共享状态的最终结果(即共享状态的某个值或者异常)。
shared_future 可以通过某个 std::future 对象隐式转换(参见 std::shared_future 的构造函数),或者通过 std::future::share() 显式转换,无论哪种转换,被转换的那个 std::future 对象都会变为 not-valid.

现在回过头来讨论,std::thread对象可以用join()来控制子线程"必须"执行
std::future::get()方法是针对std::future<T>对象的

小结:
std::thread()在系统资源紧张时,创建线程可能会失败,这行语句可能会导致程序崩溃
std::async()异步任务与std::thread()最大不同是std::async()有时不创建线程
为std::async()传入第一参数:std::launch::deferred延时执行,那么该async其返回值std::future<>类型对象必须调用.wait()或.get()方法
传std::launch::deferred参数可能就是主线程执行那种(不会创建新线程)
纠正:如果不向其传参,则其默认值是std::launch::async | std::launch::deferred即可能创建新线程以异步方式运行,也可能以主线程中以函数调用的方式运行
std::thread创建的线程如果有返回值,拿到这个返回值不容易;但std::async相比这点有优势
系统资源紧张的话不加额外参数的std::async不会创建新线程
一个程序线程数量不宜超过100-200

这部分的知识平时用的比较少,这篇博客介绍的比较好
https://www.cnblogs.com/haippy/p/3280643.html

  • phonywake_atomic

防虚假唤醒,简单

  • packaged_task

packaged_task.cpp

std::packaged_task 与 std::function 类似,只不过 std::packaged_task 将其包装的可调用对象的执行结果传递给一个 std::future 对象
打包对象,实际生成可调对象,核心操作:
packaged_task<int()> task( [] {return 123; } );         //<>内是类型,task()小括号内是匿名函数
future<int> result = task.get_future();                 //打包任务对象中取得std::future<int>对象       
task();                                                 //直接运行可调对象
cout << result.get() << '\n';                           //从std::future<int>对象获取返回值  

这里用到了比较偏的知识点,同时detach()和std::future<T>::get()方法:
packaged_task<int(array<int, 5>::const_iterator, array<int, 5>::const_iterator)> task(acc);     //把lambda这个可调对象再生成一个可调对象
auto result = task.get_future();
thread t(move(task), numbers.cbegin(), numbers.cend());
t.detach();
cout << result.get() << '\n';

从shared_future_atomic了解了够多的std::future<T>用法
std::future<T>对象基本都可够用了,能从异步子线程中通信,获得子线程执行后的返回值
现在进一步了解std::promise<T>
#include <future>
和std::packaged_task一样使用get_future()来返回一个std::future<T>
两者不同的是std::packaged_task<T>是可调对象,而std::promise<T>似乎并不是,它更像一种比较特殊的参数,特殊在共享(不同的线程可访问),以及异步(可以在其它时刻去获取结果)
promise.cpp中
注意std::future<T>::wait()方法阻塞在std::promise<T>来set_value()
注意向lambda函数传入的std::promise<T>使用std::move语义

array<int, 5> numbers{ 5,6,7,8,9 };
promise<int> accResult;                                                 //先声明一种数据类型,下面备用,存储一个值,异步获取
auto acc = [](const array<int, 5>& numbers, promise<int> result)        //匿名函数,小括号内是参数类型
            {
                int sum = accumulate(numbers.cbegin(), numbers.cend(), 100);
                result.set_value(sum);
            };

auto result = accResult.get_future();                           //返回future对象
//引用传递时第一个参数不宜直接numbers,宜用ref();
thread(acc, ref(numbers), move(accResult)).detach();            //一来就detach();小括号内(匿名函数,参数引用,第二个参数)
result.wait();                                                  //等待阻塞结果可用
cout << "result = " << result.get() << '\n';                        //拿到结果值  

这个用法比较复杂

  • C++_Async

Async_Array.cpp中

#include <future>
auto res = async(&accumulate<Itetator, int>, numbers.cbegin(), numbers.cend(), 0);
//函数作用:求和.参数(,迭代器开始,迭代器结束,求和基数).偏通化模板函数
//async()做了三件事:
//1.创建子线程
//2.子线程向主线程传参 ?
//3.协调子线程与主线程同步关系
//而get()才是将子线程启动起来
cout << res.get() << '\n';  
//亦可使用类型推断  
auto res = async(&accumulate<decltype(std::cbegin(a)), int>, std::cbegin(a), std::cend(a), 20);
cout << res.get() << '\n';  

//async生成的对象原型  
future<void>  ret = async( [] { cout << "This Thread Run In:" << this_thread::get_id() <<"Use Windows Api:"<< ::GetCurrentThreadId() <<"           "<< endl; });
上一部分future应该是future<int> res
  • 学习线程间同步,老余的四件套

#include <thread>
#include <mutex>
#include <condition_variable>


class A
{

private:
void fun();  
void fun1();  

std::thread t;
std::thread t1;
std::mutex mutex;
std::atomic_bool flag_ready;
std::condition_variable cnd_var;
};

A::fun()
{
  {
    std::unique_lock<std::mutex> unlock(mutex);
    condition_variable .wait(unlock, [this](){ return flag;});
    flag_ready = false;
  }
  
}
A::fun1()
{
  {
    std::lock_guard<std::mutex> lock(mutex);
    flag_ready = true;
  }
   condition_variable .notify_one()
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容