C++拔尖——多线程-线程维护

C++拔尖——多线程-线程维护

### 线程的维护主要是线程的调度和线程对资源的保护
我们经常会遇到多个线程访问同一个资源的情况,因为线程执行任务的时候,可能会是并行的,
如果有多个线程都在执行写的操作,则会导致公共资源数据混乱不同步的情况,为了解决这种
情况,我们通常都会去做线程资源的保护措施,也叫线程同步。

## 线程同步
线程同步有非常多方式,用的比较多的就是互斥锁,读写锁,条件变量,信号量。
互斥锁的原理相对简单一些。定义一个锁的变量,在需要保护的数据前加上上锁操作,
操作完之后做开锁操作,在上锁的时候会检查当前锁是否被占用,如果被占用则等待资源
开放再上锁,每次操作的时候都需要上锁和开锁。

## 最基本的锁 互斥锁 ——std::mutex
互斥锁只有2种状态,开,关,这种状态是互斥的,当锁开的时候,才可以去申请锁的使用权,进而\
        访问锁保护的区域,如果锁不可用,则进入阻塞状态 

// 方法
lock()      // 锁定互斥,若互斥不可用则阻塞
try_lock()  // 尝试锁定互斥,若互斥不可用则返回
unlock()      // 解锁互斥

// 例如
int g_i = 0;
std::mutex g_mutex;

void test1()
{
    g_mutex.lock(); // 上锁

    g_i++; // 保护区域

    g_mutex.unlock(); // 解锁
}

void test2()
{
    g_mutex.lock(); // 上锁

    g_i *= 10; // 保护区域

    g_mutex.unlock(); // 解锁
}

// 上面是锁的经典用法,这样就可以保证每次g_i的++操作和*10操作都是独立完成的
// 如果不做锁定,则可以设想一下情景 g_i = 0; g_i++; 与此同时,g_i *= 10;结果先后出来 \
// 后,g_i++ = 1; g_i*10 = 0; 可能会出现永远等于0的情况
// 如果加了锁,则会先后出结果,即每次都是 +1 *10,不会出一线永远等于0的情况

// 思考一个问题,有没有可能线程会陷入无线等待与至于结束不了的情况?
// 当然会,lock()和unlock()是成双成对存在的,如果漏了lock(),可能会导致属于访问错误,
// 要是漏了unlock(),则另一个lock()调用的时候,则可能出现死锁的情况?钥匙断在了锁里。
// 还有一种情况,被意外析构了。例如一个线程被意外结束了,但是它只调用了lock(),还没有\
// 来得及解锁,则也会出现这种情况。是不是无语了,所以,非常不赞成使用std::mutex


// 那么就不得不推出一下c++11的安全机制了
## 给锁提供安全的机制 std::lock_guard
安全锁并不是独立的锁,而是给上面这种不靠谱的锁提供一种安全机制。
std::lock_guard需要用std::mutex进行初始化,当被使用的时候会自动上锁,被析构的时候会自动开锁,它并没有\
        什么复杂的用法,只有一个初始化

// 举个例子
void test1()
{
    std::lock_guard<std::mutex> lock(g_mutex);
    g_i++;
}

// 没错,这就完事了,只需要这样就可以解决锁的资源问题
// 有没有想过一个问题,这里只有上锁,解锁需要析构后才能解? 万一后面还有很多逻辑没处理,另一个线程又要急着用怎么办?
// 当然没问题,我们这里就要用到编程的一种重要思想了,低耦合,高聚合,把对公共资源的操作拆成最小的单独的函数,与主流程\
// 分离出来就可以了

//-------------------------------------------------
// 在讲独占指针之前,再插入一个工具引用包装器 std::ref  std::cref
// 这两个工具在可以将类的引用进行包装 std::ref包装成 类& std::cref包装成 const 类&
// 这个工具在线程和异步中应用的非常广泛,因为异步和线程中,局部变量的数据具有时效性,异步和线程
// 维护着自己的栈,释放掉之后,对栈内的数据不进行保存,即使加了引用,也无法改变局部变量的值。为了线程
// 可以将修改后的值原样的还给局部,可以使用引用包装器,延长线程中引用的生命周期。
// 可能还比较模糊,举个例子
#include <functional>
#include <iostream>

// 三个变量,前2个是引用第三个是const引用
void f(int& n1, int& n2, const int& n3)
{
    std::cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    ++n1; // 增加存储于函数对象的 n1 副本
    ++n2; // 增加 main() 的 n2
    // ++n3; // 编译错误
}

int main()
{
    // 初试
    int n1 = 1, n2 = 2, n3 = 3;
    // 三种用法
    std::function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    // 用std::bind定义一个新的函数指针,std::function和std::bind后面讲一下,
    // 这里大概可以知道这个std::bind是创建函数变量使用的。
    // 这里相当于bound_f = f(n1,n2,n3) 此时为f(1,2,3)

    // 改变变量 此时在函数f()调用的时候,变成了f(1,11,12);//因为n2和n3被绑定了
    n1 = 10;
    n2 = 11;
    n3 = 12;
 
    std::cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << std::endl;
    // 打印 10,11,12

    // 调用f(1,11,12) 此时 n1没有绑定,n2和n3被引用包装器包装着,此时n2 = 11 n3 = 12
    bound_f();
    // 函数里面打印1,11,12

    std::cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << std::endl;
    // f(1,11,12)之后 n1= 10  n2 = 12 n3 = 12
    return 0;
}

// 总结引用包装器只能在做引用的时候才能应用,它打破了线程和异步只维护自己栈内数据的局限
// 使线程也可以操作自己栈之外的 “局部变量”.


//-------------------------------------------------------------------------------------
// 再次插播一个知识点 函数封装器 std::function 和 std::bind表达式
// std::bind表达式和lambda表达式是一个意思。都是函数的表达式。
// std::bind表达式是将函数用函数指针,参数列表的形式进行表达
// lambda表达式则是将冗长的函数用[](){}等符号进行表达
// 而着几种表达式最终的结果可以理解成一个函数类型的变量
// 而这个变量就是函数封装器std::function<>

## 函数封装器 

## 基本用法
头文件 #include<functional>
定义 std::function<函数类型 (参数类型)> 变量名 = 函数表达式
调用 变量名();

例如:c++手册的例子
#include <functional>
#include <iostream>

struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_ + i << '\n'; }
    int num_;
};

void print_num(int i)
{
    std::cout << i << '\n';
}

struct PrintNum {
    void operator()(int i) const
    {
        std::cout << i << '\n';
    }
};

int main()
{
    // 普通函数表达式
    std::function<void(int)> f_display = print_num;
    f_display(-9);
    /// 打印 -9


    // 存储 lambda表达式
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();
    // 打印42

    // 存储到 std::bind表达式
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();
    // 打印31337
    // std::bind表达式的用法较为简单 std::bind(函数表达式,参数1,参数2......);


    // 存储到成员函数的调用
    // 需要关心的是它的函数值类型
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    // 打印 314160
    f_add_display(314159, 1);
    // 打印 314160 相当于调用f_add_display(foo(314159,1))

    // 存储到数据成员访问器的调用 这个用法比较骚一些,用的地方没这么多
    // 同样这里也需要关心它的函数类型
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';

    // 存储到成员函数及对象的调用
    using std::placeholders::_1;
    // 这个是占位符参数,后面讲一下,在多线程中用的非常多,没有特殊的用法
    // 用于bind表达式这种需要绑定实参的地方,主要是std::bind使用,不用太关心实现的细节
    // 经常会有(func,_1,_2,_3,_4)这种用法
    // 这里用bind绑定之后,函数类型发生了变化
    // 使用std::bind表达式的函数,如果想在调用的时候才使用参数,则一定需要std::placeholders占位符
    std::function<void(int)> f_add_display2 = std::bind( &Foo::print_add, foo, _1 );
    f_add_display2(2);

    // 存储到成员函数和对象指针的调用
    std::function<void(int)> f_add_display3 = std::bind( &Foo::print_add, &foo, _1 );
    f_add_display3(3);

    // 存储到函数对象的调用
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);

    return 0;
}

// 打印
-9
42
31337
314160
314160
num_: 314159
314161
314162
18

// 总结
使用异步或者线程的时候,总是绕不开std::function std::bind,函数的封装和调用是异步和线程的关键 \
        异步实现回调的时候,可以用std::bind表达式将主线程的函数封装成std::function<>,将包装好\
        的函数传给线程或者异步,异步完成任务之后再通过std::function返回到主流程。可以很好的实现\
        异步和线程的功能。



std::ref()和std::cref()也是最重要的工具之一,它把线程维护的栈和主线程维护的栈留一个通道,线程不再\
        只服务于全局变量.

## 还有一种常用的锁,独占锁 std::unique_lock
// 这是一种功能强大的锁,析构的时候,同样会做解锁操作,它满足所有std::guard_lock功能。它主要的功能,\
// 体现在它构造函数的第二个参数,它允许延期加锁,尝试加锁,立刻锁定

// std::unique_lock( mutex_type& m, std::defer_lock_t t );   //延迟加锁
// std::unique_lock( mutex_type& m, std::try_to_lock_t t ); //尝试加锁
// std::unique_lock( mutex_type& m, std::adopt_lock_t t );   //马上加锁

举几个例子说一下
 
// 延期枷锁 std::defer_lock
这个参数表示暂时先不lock,之后手动去lock,但是使用之前也不允许lock

std::mutex mlock;
void work(int& s) {
    // std::defer_lock的意思是我用独占锁上锁,但是我先不锁定,上锁和解锁都通过lock()和unlock解锁
    std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
    munique.lock(); // 这里才会被锁定
    s += 100;
    munique.unlock(); // 这里解锁
    // 当然,不解锁也行,函数结束后自动解锁
}

// 尝试枷锁 std::try_to_lock
这个参数表示,如果可以获得使用权,则锁定,没有获得使用权,我就去执行别的函数

void work1(int& s) {
    // 常识性加锁,如果获得了使用权,则锁定
    std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
    // 判断使用权
    if (munique.owns_lock() == true) {
        s += 100;
    } else {
        // 执行没有获得使用权的代码
        std::cout << s << std::endl;
    }
}

// 立刻锁定 std::adapt_lock
使用的前提是已经获得了锁的所有权,再去操作它,于std::defer_lock功能类似,但是它的前提是已经获得所有权

void work(int& s) {
    munique.lock(); // 如果被锁定则在这里阻塞 
    std::unique_lock<std::mutex> munique(mlock, std::adapt_lock); // 获得所有权 
    s += 100;
    munique.unlock(); // 这里解锁
    // 同样,这里也可也不用解锁,因为函数结束后就自动释放了
}

// 上面三个参数是它的主要用法,其中std::defer_lock 和std::adapt_lock作用相同,区别就是是否需要提前获得使用权
// std::try_to_lock让锁不再是死锁状态,关于独占锁的理解就差不不多这样了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小卷同學

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值