文章目录
前言
- C++:万物皆可类,万物皆可重载
std::function
- C++的历史遗留,如果想实现一个函数功能,存在的问题:
- 函数指针类型太复杂,不方便使用和理解
- 仿函数类型是一个类名,没有指定调用参数和返回值,得去看operator()的实现才能看出来
- ambda表达式在语法层,看不到类型,只能在底层看到其类型,基本都是lambda_uuid
- 函数包装器function,函数类模板,对可调用类型的封装;生成一个新的对象,实现对数据用原处理方式的使用。
- 可调用类型:
- 函数指针,
- 类成员函数、
- 仿函数、
- lambda表达式,
- 一个可被转换为函数指针的类对象
function的使用
#include <functional>
function < 原可调用的返回值 ( 参数列表.. ) > f1 = ....
包装函数指针:
int half(int x)
{
return x/2;
}
std::function<int(int)> fn1 = half;
std::cout << "fn1(60): " << fn1(60) << '\n';
- 包装仿函数
struct third_t
{
int operator()(int x) {return x/3;}
};
std::function<int(int)> fn3 = third_t();
std::cout << "fn3(60): " << fn3(60) << '\n';
- 包装类的成员函数:
- 第一个参数为类类型,
- 调用时第一个参数为类对象,
- 对于 类 的非静态成员函数要取地址。
struct MyValue
{
int value;
int fifth() {return value/5;}
};
std::function<int(MyValue&)> value = &MyValue::value; // pointer to data member
std::function<int(MyValue&)> fifth = &MyValue::fifth; // pointer to member function
MyValue sixty {60};
std::cout << "value(sixty): " << value(sixty) << '\n';
std::cout << "fifth(sixty): " << fifth(sixty) << '\n';
- 包装lambda表达式
std::function<int(int)> fn4 = [](int x){return x/4;};
std::cout << "fn4(60): " << fn4(60) << '\n';
std::bind
- 可以看作一个通用的函数适配器,生成一个 基于可调用类型新的可调用对象,对数据进行处理。
- 如果
functional
看作是pair
,则std::bind
的使用,约等于make_pair(...).
#include <functional>
auto newCallable = bind(callable,arg_list);
arg_list:placeholders::_1, placeholders::_2....
- arg_list中的参数可能包含形如
placeholders::_n
的名字,其中n是一个整数,这些参数是“占位符”,表示原函数第一个参数用的是 newCallable的 第几个参数;通过改变占位符的位置,可以得到基于原可调用类型相同处理方式的不同结果。 - 生成对象的类型可用
auto
来推导。
详情见下列使用方式:
- 如果 newCallable 不想传参,则参数列表直接使用值;
- 如果使用 newCallable 传参,则参数列表 全使用占位符;或某一个参数位置使用占位符,某一个位置给 值。
占位符 也可以在使用的域里展开
using namespace std::placeholders;
std::bind的使用
- 适配函数指针:
double my_divide (double x, double y)
{
return x/y;
}
int main()
{
using namespace std::placeholders;
auto fn_five = std::bind (my_divide,10,2); // returns 10/2
std::cout << fn_five() << '\n'; // 5
auto fn_half = std::bind (my_divide,_1,2); // returns x/2
std::cout << fn_half(10) << '\n'; // 5
auto fn_invert = std::bind (my_divide,_2,_1); // returns y/x
std::cout << fn_invert(10,2) << '\n'; // 0.2
}
- 适配类的成员函数
- 第一个参数,类的成员函数地址,
- 第二个,类的实例对象,可以是匿名对象。
- 参数列表不写原类的实例化对象,则参数列表需要使用占位符,生成对象传参时,传对象,等于是 原类的对象调用了该成员函数。
- 成员函数无参,则先实例化一个对象。
struct MyPair
{
double a,b;
double multiply() {return a*b;}
};
MyPair ten_two {10,2};
auto bound_member_fn = std::bind (&MyPair::multiply,_1); // returns x.multiply()
std::cout << bound_member_fn(ten_two) << '\n'; // 20
//不使用占位符:
auto bound_member_fn1 = std::bind(&MyPair::multiply, ten_two); //不使用占位符
std::cout << bound_member_fn1() << '\n';
auto bound_member_data = std::bind (&MyPair::a,ten_two); // returns ten_two.a
std::cout << bound_member_data() << '\n'; // 10
成员函数有参,利用匿名对象
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
std::function<int(int, int)> func4 = std::bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << func4(1, 3) << endl; //return -2
std::function<int(int, int)> func5 = std::bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << func5(1, 3) << endl; //return 3 - 1 2
线程库
- 在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。使用标准库中的线程,包
< thread >
头文件。
thread类
- 构造线程对象:
default (1) thread() noexcept; //构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
//构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的参数
initialization (2) template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
copy [deleted] (3) thread (const thread&) = delete;
move (4) thread (thread&& x) noexcept;
get_id() 获取线程id namespace<thread>std::this_thread
jion() 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
jionable() 线程是否还在执行,joinable代表的是一个正在执行中的线程。
detach() 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关
注意
- thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值;即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。
- 可以通过 jionable() 函数判断线程是否是有效的,如果是以下任意情况,则线程无效:
- 采用无参构造函数构造的线程对象
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用jion或者detach结束
线程对象的使用
- 线程对象关联线程函数,该线程就被启动,与主线程一起运行。
- 空线程:
#include <thread>
int main()
{
std::thread t1;
cout << t1.get_id() << endl;
return 0;
}
- 线程对象关联函数指针、lambda表达式、仿函数对象:
#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
cout << "Thread1" << a << endl;
}
class TF
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([]{cout << "Thread2" << endl; });
// 线程函数为函数对象
TF tf;
thread t3(tf);
t1.join();
t2.join();
t3.join();
cout << "Main thread!" << endl;
return 0;
}
- 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。
原子性操作库:atomic
- 所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
- 在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
- 使用atomic类模板,定义出需要的任意原子内置类型。
atmoic<T> t; // 声明一个类型为T的原子类型变量t
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
- 举个例子:
#include <iostream>
using namespace std;
#include <thread>
#include <atomic>
atomic_long sum{ 0 };
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum ++; // 原子操作
}
int main()
{
cout << "Before joining, sum = " << sum << std::endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();
cout << "After joining, sum = " << sum << std::endl;
return 0;
}
ios::operator bool
Mutex的种类
- 在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。
std::mutex
- C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
lock() 上锁:锁住互斥量
unlock() 解锁:释放对互斥量的所有权
try_lock() 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞
线程函数调用 lock() 时,可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用 try_lock() 时,可能会发生以下三种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
std::recursive_mutex
- 其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和std::mutex 大致相同。
std::timed_mutex
- 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
- try_lock_for()
- 接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
- ry_lock_until()
- 接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
第四种锁:std::recursive_timed_mutex
lock_guard 与 unique_lock
- RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;是C++语言的一种管理资源、避免泄漏的机制。
lock_guard:
- std::lock_gurad 是 C++11 中定义的模板类,自动上锁解锁。
- 调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。{ }里面就是一个作用域。
- lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx还没有被上锁
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{}
~lock_guard() _NOEXCEPT
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
unique_lock:
条件变量:condition_variable
- 当前线程获取锁进来时,不满足条件会被阻塞,阻塞之前,会自动解锁,让其他线程使用共享资源。
while (!pred()) wait(lck);
两个线程交替打印奇数偶数
- 本质是通过flag 和 条件变量配合控制互斥。
int main()
{
mutex mtx;
condition_variable cv;
bool flag = true;
int i = 1;
// 打印偶数
thread t2([&i, &mtx, &cv, &flag](){
while (i < 100)
{
std::unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag](){return !flag; });
cout << this_thread::get_id() << ":" << i << endl;
i++;
flag = true;
cv.notify_one();
}
});
// 打印奇数
thread t1([&i, &mtx, &cv, &flag](){
while (i < 100)
{
std::unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag](){return flag; });
cout << this_thread::get_id() << ":" << i << endl;
i++;
flag = false;
cv.notify_one();
}
});
// 极端场景下:假设主线程执行到这里时间片用完了,进入休眠排队
// sleep模拟一下这个场景
// std::this_thread::sleep_for(std::chrono::milliseconds(1));
t1.join();
t2.join();
return 0;
}
编者寄语
- 对线程泛泛的认知…
- 线程池都去任务队列里取,任务队列保证线程安全…