当前位置: 首页 > 工具软件 > Dark Exchange > 使用案例 >

Blue is white,dark is not blue(c++ 多线程)

苏宏峻
2023-12-01

C++并发编程

1 了解并发

1.1 何为并发

  • 对于生活中来说,并发就是多个活动在同时的进行

  • 对于计算机来说,并发就是多个任务同时在计算机上执行,可以分为进程并发和线程并发,进程并发就是指给每个任务分配独立的资源,而线程并发是指给每个任务共享一个进程资源,对每个线程方法共享资源进行控制

  • 对于多核处理器来说,可以称并发为并行,应为多个任务是真正意义上的同时执行,而不是时间片轮转执行

  • 一般来说,并发是指多线程并发,即多个线程同时在一个进程内执行

  • 计算机线程分为内核线程和应用线程,处理器只会对内核线程进行执行,所以应用线程会和内核线程有一定的映射关系,多对一,一对一和多对多等

1.2 为什么要并发

  • 保持任务的专一性,即每个线程只干同一性质的事情,比如Android的UI线程就只进行刷新UI

  • 提高计算机的利用率,和程序的执行效率,也就是别让CPU闲置,因为现在的计算机都能实现真正意义上的同时执行了

1.3 c++中的并发

  • c++11才支持多线程,基于boost库

1.4 简单例子

#include <iostream> 
#include <thread> //线程管理头文件 
void threadInitMethod() //线程初始函数 
{
	std::cout << "blue is white, dark is not blue1" << std::endl;
}
int main(int args,char *argv[])
{
	std::thread t(threadInitMethod); //创建线程 并启动 
	std::cout << "blue is white, dark is not blue2" << std::endl;
	t.join(); //阻塞主线程 
	std::cout << "blue is white, dark is not blue3" << std::endl;
	return 0;
}

2 线程管理

2.1 线程管理基础

  • 使用join函数可以让调用线程阻塞

  • 使用detach函数可以让线程成为后台线程,使线程对象和线程分离

  • 线程参数可以是函数,lambda表达式,重写调用运算符的类对象,可以再其后带上合适参数

2.2 向线程函数传递参数

  • 向线程传递参数,传递函数、lambda表达式等之外还可以传递类成员函数

#include <iostream> 
#include <thread> //线程管理头文件 
void normarMethod() 
{
	std::cout << "blue is white, dark is not blue1" << std::endl;
}
struct MyClass 
{
	void doSomething() 
	{
		std::cout << "blue is white, dark is not blue2" << std::endl;
	}
	
} ;
int main(int args,char *argv[])
{
	std::thread t1(normarMethod);
	t1.join();
	MyClass c;
	std::thread t2(MyClass::doSomething,c); //调用类成员函数,需要传递函数地址 和类对象 
	t2.join();
	return 0;
}
  • 向线程函数传递参数,都会进行拷贝,不管是不是引用类型,但是如果需要将一个对象的引用传递给线程函数需要调用std::ref进行包裹,因为thread的构造无视函数的参数类型,会先拷贝一份到线程内部,然后将拷贝的传递给线程函数,当做实参

  • 如独享指针指针可以使用std::move进行移动操作,对于不能拷贝的对象来说

  • thread实例对象可以互相转移,更好控制线程的所有权

2.3 转移线程所有权

  • 构造函数可以转移所有权

  • move函数可以转移所有权

  • 线程实例同时只能获得一个线程的所有权

2.4 线程数量

  • 可以通过std::thread::concurrency获取每个可进程同时并发的线程数,如果获取失败返回0

2.5 线程标识

  • std::thread::id,线程成员

  • std::thread的对象调用get_id, std::this_thread::get_id,可以获取线程id


3 线程间共享数据

3.1 互斥量

  • 可以使用mutex结合lock_guard进行同步控制,lock_guard在构造时对mutex上锁,在析构时对mutex解锁

  • 不要将保护对象以指针或者引用的形式传递到互斥量的作用范围之外

  • 可以使用std::lock同时获取两个或以上的锁 向lock_guard传递std::adopt_lock参数可以获取锁对象,将mutex交给lock_guard管理

  • std::try_lock尝试获取多个锁对象,但是不会阻塞当前线程

  • recursive_mutex互斥量同一个线程可以多次进入

  • unique_lock可以更加方便的控制互斥量,但是开销叫lock_guard大
    ,他的对象拥有和mutex类似的成员函数,可以提前释放锁,而不是析构时,如果没有提前调用,最后会在析构释放

  • adot_lock假设mutex已经上锁,获取锁的所有权,defer_lock不对锁进行上锁在构造时,如果没有传递该参数表示在构造上锁,如果传递try_lock表示在构造尝试上锁

  • unique_lock可以进行移动


4 同步并发操作

4.1 等待一个事件或其他条件

  • std::this_thread::sleep_for(std::chrono::milliseconds(1000)) 可以进行线程等待

#include <iostream> 
#include <thread> //线程管理头文件 
#include <mutex>
#include <queue>
#include <condition_variable> //条件变量 
std::queue<int> myQueue;
std::mutex mut;
std::condition_variable condition;

void pFunc()
{
	while(true) 
	{
		int data = 0;
		std::cin >> data ;
		std::cout << std::this_thread::get_id() << ":"<<data << std::endl;
		std::lock_guard<std::mutex> lg(mut); //获取锁 
		myQueue.push(data); //安全入队 
		condition.notify_one(); //唤醒一个等待锁的线程 
	}
}

void cFunc()
{
	while(true) 
	{
			std::unique_lock<std::mutex> uL(mut); //获取锁 
			condition.wait(uL,[]()->bool{return !myQueue.empty();}); //释放锁 阻塞当前线程 直到有人唤醒和满足条件 该线程获得锁 
			int data = myQueue.front(); //安全出队 
			myQueue.pop();
			uL.unlock(); 
			std::cout << std::this_thread::get_id() << ":"<<data << std::endl;
	}
}

 
int main(int args,char *argv[])
{
	std::thread c(cFunc);
 	c.detach();
	std::thread p(pFunc);
	p.join(); 	
	return 0;
}

4.2 使用期望

  • std::async 异步任务,可以传入可调用对象,返回future对象,对future调用get可以阻塞当前线程,得到异步任务的返回结果

  • std::packaged_task 可以对可调用对象进行包裹,然后调用get_future可以得到future对象,然后将task交给thread处理,另外packaged_task的泛型参数是可调用对象的函数签名,对future对象调用get可以阻塞当前线程,获取返回值

  • std::promise 对象可以通过get_future获取绑定的future对象,future调用get时会阻塞,此时可以调用promise对象的set_value,作为get的返回值

  • std::future能够存储异常,promise可以set_exception(std::current_exception())

  • std::shared_future可以进行拷贝,和指针指针类似

4.3 等待时间

  • steady_clock稳定时钟 system_clock不稳定时钟

  • std::chrono::duration表示时间跨度,泛型参数第一个表示计数类型,第二个表示单位,以秒作为基准

  • std::chrono::time_point泛型第一个参数为时钟类型,第二个是单位


5 原子类型

5.1 原子类型

  • 原子类型不允许拷贝和赋值,因为会产生两个对象,所以不能保证原子性

  • 原子类型 atomic_flag支持clear和test_and_set操作,需要初始化

  • atomic_flag的test_and_set操作返回是否已经设置,如果设置了返回true,否则为false,可以用它实现自选

  • 对于任意泛型原子类型支持 load 返回一个相对应的非原子类型值 store 替换值 exchange 用参数值替换值,返回原来的值 compare_exchange_strong 比较和替换 如果包含值和期望值相同,则用替换值替换包含值 返回true,如果不相同,则用包含值替换期望值 返回false ,compare_exchange_weak和他唯一的区别是相同也可能返回false,建议循环用weak,非循环用strong

  • 指针支持原子类型操作 自增 复合增加都是原子操作

 类似资料: