C++——线程维护1
### 线程的维护主要是线程的调度和线程对资源的保护
我们经常会遇到多个线程访问同一个资源的情况,因为线程执行任务的时候,可能会是并行的,
如果有多个线程都在执行写的操作,则会导致公共资源数据混乱不同步的情况,为了解决这种
情况,我们通常都会去做线程资源的保护措施,也叫线程同步。
## 线程同步
线程同步有非常多方式,用的比较多的就是互斥锁,读写锁,条件变量,信号量。
互斥锁的原理相对简单一些。定义一个锁的变量,在需要保护的数据前加上上锁操作,
操作完之后做开锁操作,在上锁的时候会检查当前锁是否被占用,如果被占用则等待资源
开放再上锁,每次操作的时候都需要上锁和开锁。
## 最基本的锁 互斥锁 ——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让锁不再是死锁状态,关于独占锁的理解就差不不多这样了`在这里插入代码片`
C++——线程维护1
最新推荐文章于 2023-09-06 09:52:40 发布