C++_thread & mutex

本文详细介绍了C++中的多线程概念,包括线程的创建与管理、互斥量mutex的使用,如std::lock_guard、std::mutex、std::recursive_mutex等,以及条件变量condition_variable的wait和notify操作。此外,还讨论了原子操作atomic和future、async的相关内容,深入探讨了如何在多线程环境中实现同步和数据安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.thread – 线程 包含在std空间< thread>头文件中

1.1 常用得成员函数
  • get_id() – 获取线程id
  • joinable() – 判断线程是否可以等待加入
  • join() – 等待线程执行完成之后才返回
  • detach() – 将本线程从调用线程中分离出去,允许本线程独立运行,但是当主线程结束得时候,本线程即使没有结束也会被强制杀死,调用了detach之后得线程调用join是没有用得
  • yield – 线程放弃当前执行,操作系统调用别得线程执行
1.2 一个简单得创建线程得过程 – 编译得时候加上选项 -std=c++11

ps:pthread_create 创建线程得函数是C++ 98 的创建接口 只支持linux 而thread是c11的新特性

#include<iostream>
#include<thread>
using namespace std;

void func1()
{
	cout<<"func1 foo" <<endl;
}
void func2(int a, int b)
{
cout <<"func2 foo" <<"a ==" <<a <<"  b==" <<b <<endl;
}

class A
{
	public:
	static void func3(int a)
	{
		cout << "static func3 foo" <<endl;
	}
	void func4(int a)
	{
		cout << "static fun4 foo "<<endl;
	}
};
int main()
{
	std::thread t1(func1);
	t1.join();
	int a=10,b=20;
	std::thread t2(func2,a,b);
	t2.join();
	std::thread t3(&A::func3,a);
	t3.join();
	//A Aa; -- 切记此种用法是错误得,不能绑定到非静态成员函数
	//std::thread t4(Aa.func4,a);
	//t4.join();	
}

2. 互斥量 - - mutex 包含在std空间,< mutex >头文件中 用来处理多线程之间对共享区域的原子操作

2.1 – std::lock_guard() RAII(资源分配时初始化)

std::lock_guard类是为了mutex RAII调用的一套类模板。类模板除了构造函数和析构函数外没有其它成员函数,该模板的工作原理是在构造mutex对象的时候 调用构造函数的时候(对象创建的时候)进行加锁,在析构函数的时候(对象生命周期结束的时候)进行解锁处理,这样可以简化对互斥量的加锁和解锁的处理

2.2 独占的互斥量 不能递归操作 std::mutex
  • 构造函数 std::mutex不允许拷贝构造 ,不允许move拷贝,刚定义的mutex变量处于unlocked状态
  • lock() 调用线程将锁住该互斥量 会有三种情况 1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住 直到调用unlock() 2.如果当前互斥量被其他线程锁住那么线程调用lock函数将会被 阻塞 3. 如果当前互斥量被当前线程锁住,则会产生死锁,相当于一个线程里面调用了2次lock
  • try_lock 调用线程将锁住该互斥量 会有三种情况 1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住 直到调用unlock() 2.如果当前互斥量被其他线程锁住那么线程调用lock函数,当前线程返回false, 不会阻塞 3. 如果当前互斥量被当前线程锁住,则会产生死锁,相当于一个线程里面调用了2次try_lock()
  • unlock() 解锁
2.3 带超时的独占互斥量,不能递归使用 std::time_mutex

*std::time_mutex比std::mutex 多了两个超时获取锁的接口 try_lock_for 和try_lock_until
try_lock_for 尝试在某个时间段内进行加锁

  1. 如果此时互斥量未被上锁,则获得改锁
  2. 如果此时互斥量已经被其他线程获取,则当前线程会阻塞,直到获得互斥量的锁,但是阻塞的时间不会超过传入的参数real_time,超时返回false
  3. 如果此时互斥量已经在当前线程获取了一次锁,则会产生死锁
    try_lock_until与try_lock_for类似只是不是时间段而是时间点
2.4 可递归使用的互斥量,不带超时功能 std::recursive_mutex

std::recursiev_mutex 允许同一线程获取多次该互斥锁,而不会产生死锁的问题

#include<iostream>
#include<thread>
#inlude<mutex>

class Complex
{
public:
	std::recursive_mutex mu;
	int i;
	Complex():i(0){}
	void mul(int x)
	{
		std::lock_guard<std::recursive_mutex> lock(mu);
		i*=x;
	}
	void div(int x)
	{
		std::lock_guard<std::recursive_mutex>lock(mu);
		i/=x;
	}
	void both(x, y)
	{
		std::lock_guard<std::recursive_mutex>lock(mu);
		mul(x);
		div(y);
	}

};
int main()
{
	Complex complex;
	complex.both(2,3);
	cout << "finish" <<endl;
	return 0;
}
2.5 可递归使用的互斥量,带超时功能 std::recursive_timed_mutex

结合了std::recursive_mutex和time_mutex两者的功能

2.6 lock_guard 和unique_lock的使用和区别

*unique_lock和lock_guard 都能实现自动的加锁和解锁处理,但是unique_lock更加的灵活,能实现更多的功能,因为unique_lock可以进行临时的加锁和解锁,不一定非等到析构函数
*

3. 条件变量 – 头文件< condition_variable >

使用条件变量一定要使用unique_lock,因为条件变量在wait的时候会先unlock然后进入休眠,但是lock_guard并无此接口

3.1 使用过程
  1. 拥有条件变量的线程获得互斥量
  2. 循环检测某个条件,如果条件满足则向下执行,如果不满足就则阻塞置条件满足的时候
  3. 某个线程满足条件执行完成之后使用notify_one 或notify_all唤醒一个或所有等待的线程
3.2 成员函数
1. wait
void wait (unique_lock<mutex>& lck);//只包含unique_lock对象
template <class Predicate>
//包含unique_lock对象和 Predicate条件变量
void wait (unique_lock<mutex>& lck, Predicate pred)

  • 工作原理

1.当前线程调用wait之后会进入休眠状态,并且在休眠之前会进行unlock互斥量(为什么使用unique_lock的原因),直到另外的线程调用notify_one 或者notify_all唤醒该线程,一旦线程获得notify则wait 函数会自动添加lock 同理不能使用lock_guard

2.如果wait没有第二个参数,则在第一次调用的时候默认条件不满足,释放互斥量并且阻塞到本行,直到某个线程nofity_one或notify_all为止 如果该线程在notify中被唤醒 无条件的执行下面的操作

3.如果wait有第二个参数 ,如果第二个参数的条件不满足,则线程会释放互斥量并进入阻塞状态,直到notify_ont和notify_all唤醒为止,被唤醒后,线程将尝试获取互斥量,如果得不到线程将会阻塞在这里,如果得到了,就判断第二个参数得条件是否满足 如果满足则继续执行下面得内容

ps:wait_for和wait_until 两者都可以加上一个时间(一个是时间段,一个是时间点,两者在线程获得唤醒之前和超时之前都会进入阻塞状态,如果被notify_one或则notify_all唤醒,或者到了超时时间了,也会进行唤醒),在wait的时候会释放锁并进入等待状态,在重新唤醒之后就会重新获取锁

2.notify_one和notify_all
  • notify_one 解锁正在等待当前条件的一个线程, 如果没有正在等待的线程则函数不执行操作,如果正在等待的线程有多个那么唤醒的线程是不确定的
  • notify_all 解锁正在等待当前条件的所有线程,如果没有则不执行任何操作
#include<thread>
#include<iostream>
#include<condition_variable>
#include<mutex>
#include<list>

template<typename T>
class SimpleSyncQueue
{
public:
SimpleSyncQueue(){};
void  put(const T&x)
{
	std::unique_lock<std::mutex>locker(_mutex);
	_queue.push_back(x);
	_notEmpty,notify_one(); //唤醒一个
}
void Take(T&x)
{
	std::unique_lock(std::mutex) locker(_mutex);
	_notEnpty.wait(locker,[this]{return !_queue.empty();});
	x = _queue.front();
	_queue.pop_front();
}

private:
std::list<T>_queue;
std::mutex _mutex;
std::condition_variable _notEmpty;
};

SimpleSyncQueue<int> syncQueue;
void PutDatas
{
	for(int i=0; i<20;i++)
	{
		syncQueue.put(888);	
	}
}

void TakeDatas()
{
	int x =0;
	for(int i=0; i< 20; i++)
	{
		syncQueue.Take(x);
		std::cout << "Take X==" << x <<endl;
	}
}

int main()
{
	std::thread t1(PutDatas);
	std::thread t2(TakeDatas);
	t1.join();
	t2.join();
	std::cout << "main finish" <<std::endl;
	return 0;
}

4.atomic – 原子操作 #include < atomic >

4.1 成员函数
1.定义的方式 std::atomic foo(0)
2. load 返回包含的值。 foo.load()
strore 用val 替换包含的值。 该操作是原子的,并遵循sync指定的内存顺序。foo.store(x, std::memory_order_relaxed)

memory_load:
memory_order_relaxed 不对执行的顺序作任何保证
memory_order_acquire 本线程中,所有后续的读操作必须在本条原子操作完成后执行
memory_order_release 本线程中,所有之前的写操作完成后才能执行本条原子操作
memory_order_acq_rel 同时包含memory_order_acquire和memory_order_release
memory_order_consume 在本线程中,所有后续的有关原子类型的操作,必须在本条原子操作完成之后执行
memory_order_seq_cst 全部存取都按顺序执行

#include<atomic>
#include<thread>
#include<iostream>
std::atomic<int> foo(0);
 void set_foo(int x)
 {
	foo.store(x, std::memory_order_relaxed); //设置值 atomic
}
void print_foo()
{
	int x;
	do
	{
		x = foo.load(std::memory_order_relaxed); //获取值atomic
	}while(x ==0)
	std::cout<<"x ==" <<x <<std::endl;
}

int main()
{
	std::thread t2(print_foo)
	std::thread t1(set_foo,10);
	t1.join();
	t2.join();
	return 0;
}

5. call_once和once_flag 用来解决多线程之间只要执行一次的情况

#include<iostream>
#include<thread>
#include<mutex>
std::once_flag flag1;

void simple_do_once()
{
	std::cout<<"simple do once\n";
	std::call_once(flag1, []()(std::cout<<"simple do once Call")) ;
}

int main()
{
	std::thread t1(simple_do_once);
	std::thread t2(simple_do_once);
	std::thread t3(simple_do_once);
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

6.std::future 和std::async future代表的是一次性事件。— #include < future >

std::future期待一个返回,从一个异步调用的角度来说,future更像是执行函数的返回值,C++标准库使用std::future为一次性事件建模,如果一个事件需要等待特定的一次性事件,那么这线程可以获取一个future对象来代表这个事件。异步调用往往不知道何时返回,但是如果异步调用的过程需要同步,或者说后一个异步调用需要使用前一个异步调用的结果。这个时候就要用到future。
线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务,一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。

6.1 在库的头文件中声明了两种future,唯一future(std::future)和共享future(std::shared_future)这两个是参照std::unique_ptr和std::shared_ptr设立的,前者的实例是仅有的一个指向其关联事件的实例,而后者可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪
6.2 future的使用 - - 需要使用std::async用来线程之间进行同步 std::async 返回一个future对象,当你需要这个值的时候就可以使用future的get函数获取,如果没有ready,线程就阻塞
#include<iostream>
#include<future>
#include<thread>
using namesapce std;

int get_result1()
{
	return 1+1;
}
ing get_result2(int a, int b)
{
	return a+b;
}
void do_other_thing()
{
	cout <<"do other thing now" <<endl;
}

int main()
{
	std::future<decltype(get_result1())> result = std::async(get_result1);
	do_other_thing();
	std::cout <<"result==" <<result.get() <<std::endl;
	// std::future<decltype (find_result_to_add2(int, int))> result2 =
//std::async(find_result_to_add2, 10, 20); //错误
// 使用decltype推导类型的时候 
std::future<decltype (get_result2(0, 0))> result2 =
std::async(get_result2, 10, 20);
std::cout << "result2: " << result2.get() << std::endl; // 延迟是否有影响?
std::cout << "main finish" << endl;
return 0;
}

ps:默认情况下,std::async是否启动一个新线程,或者在等待future时,任务是否同步运行都取决于你给的
参数。这个参数为std::launch类型
std::launch::defered表明该函数会被延迟调用,直到在future上调用get()或者wait()为止
std::launch::async,表明函数会在自己创建的线程上运行
std::launch::any = std::launch::defered | std::launch::async
std::launch::sync = std::launch::defered
缺省参数是std::launch::any

6.3 std::packaged_task – #include< future>

std::async 和std::future 是分开的,std::packaged_task是将任务和future绑定到一起的模板, 需要使用函数签名

#include<iostream>
#include<future>
using namespace std;
int add(int a,int b)
{
	return a+b;
}

void do_some_thing()
{
	cout << "do something" <<endl;
}

int main()
{
	std::packaged_task<int(int,int)>task(add);//需要使用int(int,int)的函数签名
	do_some_thing();
	std::future<int> result = task.get_future();
	task(1,1);//必须要让task运行起来不然的话get值得时候会一直阻塞
	std::cout<<"result.get==" << result.get()<<std::endl;
	return 0;
}

7.function 和bind – #include< functional >

C11中提供了两种方法function和bind来对可回调对象进行封装,C11中可回调对象包括,函数,lambda表达式 bind创建的对象,以及重载了函数运算符的类

7.1function的用法
#include<iostream>
#include<functional>
using namespace std;

void printA(int a)
{
	std::cout<<"A=" <<a <<std::endl;
}

std::function <void(int)> func;
func = printA;
func(1); // print 1;

//对lambda表达式的调用
std::function<void()> func1 = [](){std::cout<<"hello world" <<std::endl;};
func1();//hello world

//对类成员函数的
class Foo
{
public:
	Foo(int num):num(num){};
	void print_add(int i)const{cout << num+i <<endl};
	int num;
}
//print_add的函数是有const修饰的this指针的,所以函数签名的时候需要加上
std::function<void(const&Foo, int)>f_add = &Foo::print_add;
Foo foo(2);
f_add(foo,3);//print 5

7.2 bind

可以看作是一个通用的适配器它接受一个可调用对象,然后生成一个新的可调用对象来适应原对象的参数列表,调用的一般形式是 auto newCallable=bind(callable,arglist), arglist是用逗号分开的参数列表 ,arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:1为newCallable的第一个参数,_2为第二个参数,以此类推

#include<iostream>
#include<functional>
using namespace std;
class A
{
public:
void fun_3(int k,int m)
{
	cout<<"print: k="<<k<<",m="<<m<<endl;
}
};
void fun_1(int x,int y,int z)
{
	cout<<"print: x=" <<x<<",y="<< y << ",z=" <<z<<endl;
}
void fun_2(int &a,int &b)
{
	a++;
	b++;
	cout<<"print: a=" <<a<<",b="<<b<<endl;
}
int main()
{
	//func_1的函数签名是void(int,int,int)
	auto f1 = std::bind(func_1,1,2,3);
	f1() ;//x =1,y=2,z=3
	auto f2=std::bind(func_1,std::placeholders::_1,std::placeholders::_2,3);
	//表示绑定函数func_1,其中第一个参数_1和第二个参数_2是占位符,有f2调用的时候指定,第3个参数是3
	f2(3,4);//x=3,y=4,z=3
	auto f3 = std::bind(func_1, std::placeholders::_2, std::placeholders::_1,3);
	//绑定函数func_1,其中第一个参数(_2)由调用时候第二个参数决定,第2个参数(_1),由调用的时候第一个参数决定
	f3(1,2);//x=2,y=1,z=3
	int m=2,n=3;
	auto f4  = std::bind(func_2,std::placeholders::_1,n);
	//表示绑定的func_2的第一个参数由调用的时候传参决定,第二个参数是n
	f4(m);//m=3,n=4;
	cout<<"m=" << m <<endl; //m=3
	cout<<"n =" << n <<endl;//n=3
	//从上面的打印结果来看,在传人m做参数的时候,m的值会改变,所以是引用传递,而n是在band时候指定的,只存储当时的值,所以采用的是值传递
   A a;
   auto f5=  std::bind(&A::func_3, a,std::placeholders::_1, std::placeholders::_2);
   f5(10,20);//调用a.func3 k=10, m=20
   std::function<void(int ,int)>f6 = std::bind(&A::func_3,a,std::placeholders::_1, std::placeholders::_2);
   f6(10,20);//k =10,m=20
   //bind返回的对象就是一个function对象
   return 0;

8.可变模板参数

8.1 对参数进行高度泛化。它能表示0到任意个参数
8.2 可变模板参数的展开
template<class ...T>
void f(T...args);

上面的可变模板参数的定义中,省略号的作用又两个
1.声明一个参数包T…args,这个参数包中包含0到n个参数
2. 在模板定义的右边,可以将参数包展开成一个个独立的参数

8.3 可变模板参数函数
#include<iostream>
using namespace std;
template<class ...T>
void f(T...args)
{
	cout<<sizeof...(args) <<endl;
}
int main()
{
	f();//0
	f(1,2,3);//3
	f(1,2.5,"");//3
	return 0;
}

展开可变模版参数函数的方法一般有两种:1. 通过递归函数来展开参数包,2. 是通过逗号表达式来展开参数包。

8.4 通过递归函数来展开参数包
#include<iostream>
using namespace std;
void print()
{
	cout << "empty" <<endl;
}
template<class T, class ...Args>
void print(T head, Args args)
{
	cout << head <<endl;
	print();
}
int main()
{
	print(1,2,3,4);
	return 0;
}

上例中会将1,2,3,4依次输出,需要用到的函数又两个,一个是递归函数,一个是递归终止函数,在可变模板参数的调用过程中,通过不断的调用自身来进行递归调用,直到最后为空的时候调用非递归的终止函数,这种可变模板参数的展开方式需要一个同名的非模板参数作为递归终止函数才能够实现

8.5 通过逗号表达式来展开参数包
#include<iostream>
using namespace std;
template<class T>
void pringarg(T t)
{
	cout << t <<endl;
}
template<class ...Args>
void expand(Args ... args)
{
	int arr[] = {(printarg(args),0)...};
}
int main()
{
	expand(1,2,3,4);
	return 0;
}

1.关于逗号表达式我们都知道它会先执行逗号前的结果,然后执行逗号后的,并且返回逗号后的执行结果
2. 上例中可变模板函数expand中不断调用printargs(args),但是printarg只接受一个模板参数,所以这里会展开调用,并依次打印结果,而逗号表达式每次都取得是后面得结果也就是0 这里会得到一个全部为0得数组
3.我们也可以使用lambda的方式来调用

#include<iostream>
using namespace std;
template<class F, class ...Args>
void expand(F&f, Args&&...args)
{
	initializer_list<int>{(f(std::forward<Args>(args,0)))...};
}
int main()
{
	expand([](int i){cout << i<<endl;},2,3,4);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值