中级C++11:function、std::bind、线程库

前言

  • 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() 时,可能会发生以下三种情况:

  1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
  2. 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
  3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(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:

lock_guard和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;
}

编者寄语

  • 对线程泛泛的认知…
  • 线程池都去任务队列里取,任务队列保证线程安全…
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值