c++游戏小技巧14:多线程(全网最全,c++)

1.前言

最近,被期末考试AK的zzb在回顾以前的代码时,无意看到一个问题:

请问:

大佬能解释一下怎么同时运行两个c++for循环吗?
就比如说游戏里你一边出招电脑也能出招这种的

 当时,zzb是用的kd来解决(详见小技巧2)

而现在,zzb要用一种新的方式:

多线程

2.正文

1.定义

摘自百度百科

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” 

所以,上述问题用c++就可以很好解决

2.thread

1.定义

thread是c++多线程中的一种

在c++11时,头文件

#include<thread>

就被加入

该头文件中定义了thread类,创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名,thread th1(proc1);如果传递进去的函数本身需要传递参数,实例化对象时将这些参数按序写到函数名后面,thread th1(proc1,a,b);只要创建了线程对象(传递“函数名/可调用对象”作为参数的情况下),线程就开始执行(std::thread 有一个无参构造函数重载的版本,不会创建底层的线程)。

看着很绕对不对?

用样例解释一下

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

void zzb(int a){cout<<"这是线程"<<a<<"
";}

int main()
{
    cout<<"主线程:"<<endl;
    //定义 线程名 函数名 参数 
    thread th2     (zzb,   9);
    cout<<"主线程中显示子线程id为"<<th2.get_id()<<endl;//获取线程id 
    th2.join();//暂停主线程,运行th2 
    return 0;
}

运行出来的效果可能

当然,也可能是

甚至种种

(大致是因为线程运行的时间不定吧)

在网上看到一个生动的例子:

你在做某件事情,中途你让老王帮你办一个任务(你办的时候他同时办)(创建线程1),又叫老李帮你办一件任务(创建线程2),现在你的这部分工作做完了,需要用到他们的结果,只需要等待老王和老李处理完(join(),阻塞主线程),等他们把任务做完(子线程运行结束),你又可以开始你手头的工作了(主线程不再阻塞)。

而代码里面也用到了一些成员函数:

2.成员函数

函数名 作用
get_id 获取线程 ID
joinable(bool) 检查线程是否可被 join,如果线程未被joindetach则返回true
join 阻塞(暂停)当前线程直到join的线程返回
detach 不阻塞当前线程,不等待该线程返回,相当于这是个守护线程。
swap 交换线程
native_handle e,点这里
hardware_concurrency e,检测硬件并发特性(后两个zzb都不会awa)

3.创建线程

1里面是直接创建,再来一遍加深记忆

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

void zzb(int a){cout<<"
这是线程"<<a<<"
";}

int main()
{
    cout<<"主线程:"<<endl;
    //定义 线程名 函数名 参数 
    thread th2     (zzb,   9);
    cout<<"主线程中显示子线程id为"<<th2.get_id()<<endl;//获取线程id 
    th2.join();//暂停主线程,运行th2 
    return 0;
} 

除此之外,匿名函数lambda也可以

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

int main()
{
	auto zzb=[](int a){cout<<"这是线程"<<a<<"
";};
	//定义 线程名 函数名 参数 
    thread th2     (zzb,   9);
	th2.join();
	return 0;
}

class也可以

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

class node
{
	public:
		void zzb(int a){cout<<"这是线程"<<a<<"
";}
}a;

int main()
{
    thread th2(&node::zzb,&a,9);
	th2.join();
	return 0;
}

4.互斥量

1里面出现了一个神奇的bug

而对于这个bug,互斥量就可以解决

比方:

这样比喻,单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock),那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。

互斥量在

#include<mutex>

 里面

用法:

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

mutex m;//实例化m对象,不要理解为定义变量

void p1(int a)
{
    m.lock();//许可
    cout<<"p1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
    m.unlock();//归还,一定要写,不然TLE
}

void p2(int a)
{
    m.lock();
    cout<<"p2函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+1<<endl;
    m.unlock();
}

void p3(int a)
{
    cout<<"p3函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
}

void p4(int a)
{
    cout<<"p4函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+1<<endl;
}

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    system("pause");
    thread pr3(p3,a);
    thread pr4(p4,a);
    pr3.join();
    pr4.join();
    return 0;
}

效果很明显:

前面很合理,后面很核理

再详细说一下互斥量函数

5.mutex

1.lock

调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

1.如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。

2.如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。

3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)(说白了TLE)。

2.unlock()

解锁,释放对互斥量的所有权,如果没有锁的所有权尝试解锁会导致程序异常。

3 try_lock()

尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:

1.如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
2.如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

4.lock_guard()

e,一个局部对象,对象内自动+-lock

例子:

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

mutex m;//实例化m对象,不要理解为定义变量

void p1(int a)
{
    lock_guard<mutex> g1(m);//用此语句替换了m.lock();
	//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout<<"p1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

void p2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout<<"p2函数正在改写a"<<endl;
        cout<<"原始a为"<<a<<endl;
        cout<<"现在a为"<<a+1<<endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout<<"作用域外的内容3"<<endl;
    cout<<"作用域外的内容4"<<endl;
    cout<<"作用域外的内容5"<<endl;
}

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    return 0;
}

当然,lock_guard传两个参数时,如果第个为adopt_lock标识时,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。(不然位置还是要乱)

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

mutex m;//实例化m对象,不要理解为定义变量

void p1(int a)
{
	m.lock(); 
    lock_guard<mutex> g1(m,adopt_lock);//用此语句替换了m.lock();
	//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout<<"p1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

void p2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout<<"p2函数正在改写a"<<endl;
        cout<<"原始a为"<<a<<endl;
        cout<<"现在a为"<<a+1<<endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout<<"作用域外的内容3"<<endl;
    cout<<"作用域外的内容4"<<endl;
    cout<<"作用域外的内容5"<<endl;
}

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    return 0;
}

当然,比起lock_guard,unique_lock更强

除了adopt_lock,它还支持try_to_lock/defer_lock

别的一样(所以谁还用lock_guard?)

try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
defer_lock: 始化了一个没有加锁的mutex;

例子:

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

mutex m;
void p1(int a)
{
    unique_lock<mutex> g1(m,defer_lock);//始化了一个**没有加锁**的mutex
    cout<<"关注一下吧"<<endl;
    g1.lock();//手动加锁;
	//注意,不是m.lock();
	//注意,不是m.lock();
	//注意,不是m.lock()
    cout<<"proc1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
    g1.unlock();//临时解锁
    cout<<"祝关注的同学"<<endl;
    g1.lock();
    cout<<"AKIOI,暴打集训队"<<endl;
}//自动解锁

void p2(int a)
{
    unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
    cout<<"proc2函数正在改写a" << endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+1<<endl;
}//自动解锁

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    return 0;
}

(还是会错位,可能因为临时解锁时可能正好try_to_lock try到了)

繁琐是很繁琐,但用的时候还是真香

recursive_mutex

允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权

放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同

其余=mutex

time_mutex

比mutex多了两个函数

try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

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

timed_mutex m;

void p1()
{
	//等lock: 每随机ms输出一次关注(还不关注) 
	while(!m.try_lock_for(std::chrono::milliseconds(rand()%200+1))) cout<<"关注";
	//得到lock,等1s后输出点赞(还不点赞)
	this_thread::sleep_for(std::chrono::milliseconds(1000));
	cout<<"点赞
";
	m.unlock();
}

int main()
{
	srand(time(NULL));
	std::thread t[5];//线程组
	for(int i=0;i<5;i++)t[i]=thread(p1);
	for(auto& th:t)th.join();
	return 0;
}

recursive_timed_mutex

e,你看一下recursive_mutex与mutex的区别,再类比一下(不会问老师什么是类比)

6.condition_variable

e,scratch中的广播用过吧?

比如:

你正在买东西,发现脏脏包卖完了,然后你联系服务员(join),如果你收到服务员的@(收到广播)再接着买

例子:        

#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<thread>
#include<mutex>
#include<bits/stdc++.h>
using namespace std;

int a=0;
mutex m;
condition_variable cond;

void th1()
{
	unique_lock<mutex> lock(m);//自动上锁,解放双手 
	cond.wait(lock,[]{return!(a%1000);});//回应 
	++a;
}

void th2()
{
	for(int i=0;i<100000;++i)
	{
		unique_lock<std::mutex> lock(m);
		if(!(a%1000)) cond.notify_one();//回应 
		++a;
	}
}

int main()
{
	thread t1(th1);
	thread t2(th2);
	t1.join();
	t2.join();
	cout<<a<<endl;
	return 0;
}

同样,相互呼应像不像:

(你在存钱,某条dog在取钱)

#include<iostream>
#include<deque>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<windows.h>
using namespace std;

deque<int> q;
mutex m;
condition_variable cond;
int c=0;//缓冲区的钱个数 

void p()
{ 
	int d;
	while(1)
	{
		//通过外层循环,能保证生成用不停止
		if(c<3)
		{//限流
			{
				d=rand();
				unique_lock<mutex> locker(m);//锁
				q.push_front(d);
				cout<<"存了"<<d<<"元"<<endl;
				cond.notify_one();//通知取
				++c;
			}
			Sleep(500);
		}
	}
}

void co()
{
	int d;
	while(1)
	{
		{
			unique_lock<mutex> locker(m);
			while(q.empty()) cond.wait(locker); //wati()阻塞前先会解锁,解锁后生产者才能获得锁来放产品到缓冲区;生产者notify后,将不再阻塞,且自动又获得了锁。
			d=q.back();//取的第一步
			q.pop_back();//取的第二步
			cout<<"取了"<<d<<"元"<<endl;
			--c;
		}
		Sleep(1500);
	}
}

int main()
{
	thread t1(p);
	thread t2(co);
	t1.join();
	t2.join();
	return 0;
}

然后你就可以发现:

其实不存钱最好

7.原子类型

终于到了最后一个了

先问一个问题:
Q:mutex是上锁对吧?那么能递归不断上锁解锁的是什么?

A:recursive_mutex

Q:time_mutex多了那些函数?

 try_lock_for与try_lock_until

OK,来看最后亿个:

原子类型

首先,了解一下:

原子类型=简单数据结构(int,bool,char)+(保护无限+耐久无限)下界合金甲

用人话说:就是它打不断(不能强制停止),切不了(不能swap)

而且,它不需要上锁

可惜的是,它只能++、--、+=、-=

#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<thread>
#include<atomic>//原子
using namespace std;

atomic<int> a;
//int a=0;//你可以试一下,与期望答案不一样oh 

void th1()
{
	for(int i=0;i<100000;i++) ++a;
}

void th2()
{
	for(int i=0;i<100000;i++) ++a;
}

int main()
{
	thread t1(th1);
	thread t2(th2);
	t1.join();
	t2.join();
	cout<<a<<endl;
	return 0;
}

所以说,如果变量在线程里要改变的话,atomic YYDS!!!

3.async

1.async

async是thread的高级封装

封装了thread promise与packaged_task

基本可以替代thread

只是它是异步任务

所以thread有什么用???

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

int th()
{
	this_thread::sleep_for(chrono::milliseconds(1000));
	cout<<"thread_task"<<endl;
	return 0;
}

int main()
{
	async(th);
	return 0;
}

2.future

async异步任务可以返回一个future对象,可用来保存子线程入口函数的返回值。
async、std::packaged_task 或 promise都能提供一个future对象给该异步操作的创建者。

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

int thread_task()
{
	this_thread::sleep_for(chrono::milliseconds(1000));
	cout<<"thread_task"<<endl;
	return 0;
}

int main()
{
	future<int> res=async(thread_task);
	cout<<res.get()<<"
";
//	while(1) res.get();//不要多次执行get,否则你可以逝世 
	return 0;
}

3.packaged_task

没看懂网上的解释

大致是把自己的调用对象的运行成果传给future

在网上抄了一段:

std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果,从包装可调用对象意义上来讲,std::packaged_task 与 std::function 类似,只不过 std::packaged_task 将其包装的可调用对象的执行结果传递给一个 std::future 对象(该对象通常在另外一个线程中获取 std::packaged_task 任务的执行结果)。
std::packaged_task 对象内部包含了两个最基本元素:一、被包装的任务(stored task),任务(task)是一个可调用的对象,如函数指针、成员函数指针或者函数对象;二、共享状态(shared state),用于保存任务的返回值,可以通过 std::future 对象来达到异步访问共享状态的效果。

代码例子也是C的

#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread
#include <mutex>    // std::mutex
#include <future>	// std::future

int thread_task(int i)
{
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "thread_task" << i << std::endl;
	return 0;
}

int main()
{
	std::packaged_task<int(int)> pack(thread_task);
	std::thread mythread(std::ref(pack), 5);
	mythread.join();
	std::future<int> result = pack.get_future();
	std::cout << result.get() << std::endl;
	return 0;
}

4.promise

在一个线程里对某个对象赋值,然后在别的进程里提出来

#include<bits/stdc++.h>
using namespace std;

int th(promise<int>&p, int i)
{
	this_thread::sleep_for(chrono::milliseconds(1000));
	cout<<"线程:"<<i<<endl;
	p.set_value(i);//只能用一次,不然参考while(1)get() 
	return 0;
}

int main()
{
	promise<int> p;
	thread t(th,ref(p),5);
	t.join();
	future<int> r=p.get_future();
	cout<<r.get()<<endl;//好像说过了,也只能用一次,不然参考while(1)set_value 
	return 0;
}

OK,关于线程的主要部分就讲完了!!!

4.拓展:线程池

不采用线程池时:

创建线程 -> 由该线程执行任务 -> 任务执行完毕后销毁线程。即使需要使用到大量线程,每个线程都要按照这个流程来创建、执行与销毁。

虽然创建与销毁线程消耗的时间 远小于 线程执行的时间,但是对于需要频繁创建大量线程的任务,创建与销毁线程 所占用的时间与CPU资源也会有很大占比。

为了减少创建与销毁线程所带来的时间消耗与资源消耗,因此采用线程池的策略:

程序启动后,预先创建一定数量的线程放入空闲队列中,这些线程都是处于阻塞状态,基本不消耗CPU,只占用较小的内存空间。

接收到任务后,线程池选择一个空闲线程来执行此任务。

任务执行完毕后,不销毁线程,线程继续保持在池中等待下一次的任务。

线程池所解决的问题:

(1) 需要频繁创建与销毁大量线程的情况下,减少了创建与销毁线程带来的时间开销和CPU资源占用。(省时省力)

(2) 实时性要求较高的情况下,由于大量线程预先就创建好了,接到任务就能马上从线程池中调用线程来处理任务,略过了创建线程这一步骤,提高了实时性。(实时)

代码:

不会awa

5.后文

zzb考完了,最近会不断更新文章

挖的坑也会不上,别急

最后,送颓废OIer一点心意

记得点赞关注

#include<bits/stdc++.h>
#include<windows.h>
#include<conio.h>
#define kd(VK_NONAME) GetAsyncKeyState(VK_NONAME)&0x8000
using namespace std;

bool D=false;
void F()// 防御模式函数
{
    while (1)
    {
        if(kd(VK_ESCAPE))//如果按下Esc可改括号内的VK键值)
        {
            D=1;
            system("cls"); // 清屏
            clock_t startTime=clock();
            int n,m,a[100005],maxn; // 伪装:一道输入形如n,m,a数组的题目
            cin>>n>>m;
            for(int i=1;i<=n;i++)
            {
                cin>>a[i];
                maxn=max(maxn,a[i]);//随机答案合理性
            }
            cout<<rand()%maxn*2<<"
"; // 随机输出答案
            clock_t endTime=clock();
            puts("--------------------------------"); // 伪装运行结束界面
            printf("Process exited after %.3lf seconds with return value 0
", (double)(endTime-startTime)/CLOCKS_PER_SEC);
            system("pause");
            HWND hWnd=GetForegroundWindow();
            ShowWindow(hWnd, SW_HIDE); //伪装关闭窗口,实则最小化到后台
            while(1)                  //持续获取按键
            {
                if(kd(VK_MENU))
                {
                    if(kd(VK_CONTROL))// 如果按下Alt+CTRL(可改括号内的VK键值),唤醒窗口
                    {
                        system("cls");
                        ShowWindow(hWnd,SW_SHOW);
                        D=0;
                        // 在这里可以做一些恢复的事情
                        break;
                    }
                }
            }
        }
        Sleep(20); // 防止CPU占用率过高
    }
}

int main()
{
    std::thread f(F);// 防御模式的线程
    f.detach();// 分离式加入,防止阻塞主线程
    while(1)// 主线程,实现游戏本体功能
    {
        if(D)//如果进入防御模式
        {
            Sleep(10); // 不执行任何操作
            continue;
        }
        puts("点赞关注");
        Sleep(20);
    }
    return 0;
}

用法:制作成头文件,加入进去,在第56行和第40行按照游戏代码稍作改动

当teacher来时,按ESC进入防御状态

输入n,m和n个数后按回车进入隐藏后台

按alt+ctrl唤起隐藏后的窗口

have a good 颓废 time!

上一篇:c++游戏小技巧13:中文输出(编码转汉字)_c++输出汉字-CSDN博客

下一篇:未完待续······