当前位置: 首页 > news >正文

C++线程池手写实现

1.Thread类的封装

封装Thread类,使其可以直接在外部调用对象的start,detach,join和cancel等方法来实现对线程的操作

1.1代码

//Thread.h//
// Created by crab on 2024/10/20.
//#ifndef THREAD_H
#define THREAD_H#include <pthread.h>class Thread {
public:virtual ~Thread();bool start(void *arg);bool detach();bool join();bool cancel();[[nodiscard]] pthread_t getThreadId() const;protected:Thread();//在派生类中实现virtual void run(void *arg) = 0;private:static void *threadRun(void *);private:void *mArg;bool mIsStart;bool mIsDetach;pthread_t mThreadId{};
};
#endif //THREAD_H
//Thread.cpp
//
// Created by crab on 2024/10/20.
//#include "Thread.h"Thread::Thread() :mArg(NULL),mIsStart(false),mIsDetach(false)
{/*初始化类的成员变量线程函数的参数为NULL, 线程启动状态和分离状态均设置为false*/
}Thread::~Thread() {//检查线程是否已经启动且也没有分离,如果是,调用detach来分离线程,避免对象销毁后产生资源泄露if(mIsStart == true && mIsDetach == false)detach();
}bool Thread::start(void *arg) {//用于启动线程mArg = arg;//创建一个新线程,线程的入口函数为静态成员函数threadRun,并将当前Thread对象作为参数传递给线程,如果失败,返回falseif(pthread_create(&mThreadId, NULL, threadRun, this))return false;//标记线程启动mIsStart = true;return true;
}bool Thread::detach() {//如果线程没有启动,返回falseif(mIsStart != true)return false;//如果线程已经分离,返回trueif(mIsDetach == true)return true;//调用pthread库函数将线程设置为分离,成功则标记IsDetach为true,失败返回falseif(pthread_detach(mThreadId))return false;mIsDetach = true;return true;}bool Thread::join() {//等待线程结束//如果没有启动或者已经分离,则返回falseif(mIsStart != true || mIsDetach == true)return false;//如果join失败,返回false,否则返回trueif(pthread_join(mThreadId, NULL))return false;return true;
}bool Thread::cancel() {//用于线程取消//如果线程没启动,返回falseif(mIsStart != true)return false;//取消成功mIsStart设置为false,返回true, 失败则返回falseif(pthread_cancel(mThreadId))return false;mIsStart = false;return true;
}pthread_t Thread::getThreadId() const {//用于获取线程的描述符return mThreadId;
}void *Thread::threadRun(void *arg) {//作为线程的入口点 arg时指向对象Thread的指针,调用派生类中实现的run()方法,来执行具体的线程任务Thread* thread = (Thread*)arg;thread->run(thread->mArg);return NULL;}

1.2代码解释

1.2.1类参数
void *mArg;

用于获取参数列表

bool mIsStart;
bool mIsDetach;

布尔变量,用于判断线程目前的状态

pthread_t mThreadId{};

线程ID列表,用来操作对应id的线程

1.2.2类方法
Thread();
virtual ~Thread();Thread::Thread() :mArg(NULL),mIsStart(false),mIsDetach(false)
{/*初始化类的成员变量线程函数的参数为NULL, 线程启动状态和分离状态均设置为false*/
}Thread::~Thread() {//检查线程是否已经启动且也没有分离,如果是,调用detach来分离线程,避免对象销毁后产生资源泄露if(mIsStart == true && mIsDetach == false)detach();
}

构造函数和析构函数,其中析构函数是虚函数,可以在派生类中重写析构函数。构造函数中将mArg,mIsStart,mIsDetach三个变量初始化。析构函数中检查线程状态,如果已经开始但未分离的线程先将其分离,避免对象销毁后产生资源泄露。

bool start(void *arg);
bool Thread::start(void *arg) {//用于启动线程mArg = arg;//创建一个新线程,线程的入口函数为静态成员函数threadRun,并将当前Thread对象作为参数传递给线程,如果失败,返回falseif(pthread_create(&mThreadId, NULL, threadRun, this))return false;//标记线程启动mIsStart = true;return true;
}

start函数用于启动线程,利用pthread_create创建一个线程,失败返回false,成功则将启动标识符mIsStart置为true,并返回true

bool detach();
bool Thread::detach() {//如果线程没有启动,返回falseif(mIsStart != true)return false;//如果线程已经分离,返回trueif(mIsDetach == true)return true;//调用pthread库函数将线程设置为分离,成功则标记IsDetach为true,失败返回falseif(pthread_detach(mThreadId))return false;mIsDetach = true;return true;
}

detach操作函数,进行两次条件检查,如果线程没有启动或已经分离(mIsStart == true或者mIsDetach == False),则不进行操作,通过检查后,利用phread_detach来分离线程,并且将mIsDetach置为true。

bool join();
bool Thread::join() {//等待线程结束//如果没有启动或者已经分离,则返回falseif(mIsStart != true || mIsDetach == true)return false;//如果join失败,返回false,否则返回trueif(pthread_join(mThreadId, NULL))return false;return true;
}

join操作,用于等待线程运行结束,检查线程是否尚未启动或已经分离,如果是则返回false,否则join对应的线程。

bool cancel();
bool Thread::cancel() {//用于线程取消//如果线程没启动,返回falseif(mIsStart != true)return false;//取消成功mIsStart设置为false,返回true, 失败则返回falseif(pthread_cancel(mThreadId))return false;mIsStart = false;return true;
}

cancel函数,用于线程取消,逻辑与上面类似

[[nodiscard]] pthread_t getThreadId() const;
pthread_t Thread::getThreadId() const {//用于获取线程的描述符return mThreadId;
}

用于获取线程的描述符,必须接收返回值

void *Thread::threadRun(void *arg) {//作为线程的入口点 arg时指向对象Thread的指针,调用派生类中实现的run()方法,来执行具体的线程任务Thread* thread = (Thread*)arg;thread->run(thread->mArg);return NULL;
}

线程的入口点,通过调用派生类中的run方法来实现利用线程完成任务

virtual void run(void *arg) = 0;

纯虚函数,在派生类中实现,作为线程的任务

1.3测试

//测试代码和main函数
class MyThread : public Thread {
public:MyThread(Mutex &mutex, int &sharedResource): mMutex(mutex), mSharedResource(sharedResource) {}protected:void run(void *arg) override {// 使用 Mutex 进行同步pthread_mutex_lock(mMutex.get());std::cout << "Thread " << getThreadId() << " started." << std::endl;// 增加共享资源mSharedResource++;std::cout << "Shared resource value: " << mSharedResource << std::endl;std::cout << "Thread " << getThreadId() << " finished." << std::endl;pthread_mutex_unlock(mMutex.get());}private:Mutex &mMutex;int &mSharedResource;
};int main() {Mutex mutex;int sharedResource = 0;// 创建并启动多个线程MyThread *threads[30];for (int i = 0; i < 30; ++i) {threads[i] = new MyThread(mutex, sharedResource);threads[i]->start(nullptr);}// 等待所有线程完成for (int i = 0; i < 30; ++i) {threads[i]->join();}for (int i = 0; i < 30; ++i) {delete threads[i];}std::cout << "Final shared resource value: " << sharedResource << std::endl;return 0;
}

运行结果
tmp/tmp.NkOftkIsac/cmake-build-debug-remote-host2/ServerStudy
Thread 140583459776256 started.
Shared resource value: 1
Thread 140583459776256 finished.
Thread 140583434598144 started.
Shared resource value: 2
Thread 140583434598144 finished.
Thread 140583442990848 started.
Shared resource value: 3
Thread 140583442990848 finished.
Thread 140583451383552 started.
Shared resource value: 4
Thread 140583451383552 finished.
Thread 140583426205440 started.
Shared resource value: 5
Thread 140583426205440 finished.

2.ThreadRegister

因为在编程中,一般任务也就是我们要运行的函数,这里实现一个ThreadRegister,来激活一个线程并且绑定对应的任务。

2.1 代码

//ThreadRegister.h
//
// Created by crab on 2024/10/22.
//#ifndef THREADREGISTER_H
#define THREADREGISTER_H
#include "Thread.h"#include <functional>
#include <tuple>
#include <utility>class ThreadRegister : public Thread {
public:// 使用std::function和std::tuple表示任务和其参数template<typename Func, typename... Args>explicit ThreadRegister(Func&& func, Args&&... args);protected:void run(void *arg) override;private:std::function<void()> mTask; // 存储任务
};template<typename Func, typename... Args>
ThreadRegister::ThreadRegister(Func&& func, Args&&... args) {// 使用std::bind和std::forward将任务和参数绑定,这样mTask被调用时会自动调用func并传递参数argsmTask = std::bind(std::forward<Func>(func), std::forward<Args>(args)...);
}inline void ThreadRegister::run(void *arg) {if (mTask) {mTask(); // 执行绑定的任务}
}#endif //THREADREGISTER_H

在ThreadRegister类中,主要实现两个函数,一个是利用bind和模板来将任务和参数绑定为一个可调用对象,这里的func即为要绑定的任务,args为任务需要的参数列表,第二个是重写基类Thread中的run函数,来执行绑定的任务。

3.Mutex类

Mutex类中封装了pthread_mutex,对外提供lock,tryLock,unlock和get三种方法

3.1 代码

//Mutex.h
//
// Created by crab on 2024/10/21.
//#ifndef MUTEX_H
#define MUTEX_H
#include "Thread.h"
#include <mutex>class Mutex {
public:Mutex();~Mutex();Mutex(const Mutex&) = delete;Mutex& operator=(const Mutex&) = delete;//加锁void lock();//尝试加锁,非阻塞bool tryLock();//解锁void unlock();//获取pthread_mutex_t指针pthread_mutex_t* get() {return &mMutex;}private:pthread_mutex_t mMutex{};
};#endif //MUTEX_H
//Mutex.cpp
//
// Created by crab on 2024/10/21.
//#include "Mutex.h"#include <pthread.h>Mutex::Mutex() {pthread_mutex_init(&mMutex, nullptr);
}Mutex::~Mutex() {pthread_mutex_destroy(&mMutex);
}void Mutex::lock() {pthread_mutex_lock(&mMutex);
}bool Mutex::tryLock() {return pthread_mutex_trylock(&mMutex) == 0;
}void Mutex::unlock() {pthread_mutex_unlock(&mMutex);
}

3.2 类的实现

这里代码比较简单,就不单独解释了。
在类定义中,Mutex封装类主要包括四个方法,加锁,尝试加锁,解锁以及获取对象的pthread_mutex_t指针,在前三种方法中,只需要封装其在pthread_mutex中的对应函数方法即可。

3.ThreadPool

3.1前言

上面的几个类是用来实现线程池的封装类,让我们能够更简单的调用需要的方法来实现目标,这里终于写到了怎么实现线程池,大概分几个部分,线程池原理,线程池实现代码,线程池代码讲解,线程池的测试这几个。

3.2线程池原理

线程池,顾名思义就是一个用来调用线程的池子,我们往池子内投入任务,池子自动调用线程然后帮助我们完成任务,之后销毁线程,因此,其实可以把线程池看作一个简单的线程管理工具,而这个管理工具对外封闭,仅仅向用户开放一个“投入任务”的接口。
在这里插入图片描述
借用C/C++手撕线程池(线程池的封装和实现)的图来表现线程池的任务方法

基本上线程池就可以分为以下几个部分

从数据结构上来说,需要两个队列:
1.工作队列:用来存储线程池中的工作线程

std::vector<ThreadRegister*> mWorkers; //工作线程队列

2.任务队列:用来储存需要完成的任务,也就是绑定的函数和参数列表

std::queue<std::function<void()>> mTasks; //任务队列

以及几个用来控制的变量

Mutex mQueueMutex; //保护任务队列的互斥锁
std::condition_variable mCondition; //条件变量
std::atomic<bool> mStop; //停止标志
size_t mMaxTasks; //最大任务数

从类方法上来说,需要构造,析构,添加任务以及完成任务这四个函数,其中添加任务以及构造函数为对外暴露的函数,提供给用户来操作。

ThreadPool(size_t threadCount, size_t maxTasks);
~ThreadPool();template<typename Func, typename... Args>
bool addTask(Func& func, Args&&... args);void workerTask();

3.3 实现

//
// Created by crab on 2024/10/22.
////ThreadPool.h#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <atomic>
#include <condition_variable>
#include <iostream>#include "ThreadRegister.h"
#include "Mutex.h"#include <vector>
#include <queue>class ThreadPool {
public:ThreadPool(size_t threadCount, size_t maxTasks);~ThreadPool();//添加任务template<typename Func, typename... Args>bool addTask(Func& func, Args&&... args);//停止线程池void stop();//调整线程数void resizeThread(size_t newThreadCount);private://工作线程函数void workerTask();std::vector<ThreadRegister*> mWorkers; //工作线程队列std::queue<std::function<void()>> mTasks; //任务队列Mutex mQueueMutex; //保护任务队列的互斥锁std::condition_variable mCondition; //条件变量,用来实现线程同步std::atomic<bool> mStop; //停止标志size_t mMaxTasks; //最大任务数};template<typename Func, typename... Args>
bool ThreadPool::addTask(Func &func, Args &&... args) {{mQueueMutex.lock();if (mTasks.size() >= mMaxTasks) {std::cerr << "Task queue is full. Cannot add more tasks." << std::endl;mQueueMutex.unlock();return false;}// 添加任务mTasks.emplace(std::bind(std::forward<Func>(func), std::forward<Args>(args)...));mQueueMutex.unlock();}mCondition.notify_one(); // 通知一个空闲线程return true;
}#endif //THREADPOOL_H
//
// Created by crab on 2024/10/22.
////ThreadPool.cpp#include "ThreadPool.h"
#include <iostream>
#include <thread>ThreadPool::ThreadPool(size_t threadCount, size_t maxTasks): mStop(false), mMaxTasks(maxTasks)
{if (threadCount < 1 || maxTasks < 1) {std::cerr <<"workers num error"<<std::endl;return;}for(size_t i = 0; i < threadCount; i++) {//使用ThreadRegister创建线程实例,将workerTask作为任务auto* worker = new ThreadRegister([this]{this->workerTask();});//启动对应的线程实例worker -> start(nullptr);//讲线程添加至mWorkers中mWorkers.push_back(worker);}
}ThreadPool::~ThreadPool() {{// 加锁mQueueMutex.lock();mStop = true;mQueueMutex.unlock();}mCondition.notify_all();// 等待所有工作线程完成for (ThreadRegister* worker : mWorkers) {worker->join();delete worker; // 释放动态分配的 ThreadRegister}
}void ThreadPool::workerTask() {while (true) {std::function<void()> task;{// 手动加锁mQueueMutex.lock();// 检查停止标志和任务队列if (mStop && mTasks.empty()) {mQueueMutex.unlock(); // 手动解锁return; // 线程退出}// 如果有任务,则获取任务if (!mTasks.empty()) {task = std::move(mTasks.front());mTasks.pop();}mQueueMutex.unlock(); // 手动解锁}if (task) {task();}else {//避免忙等待std::this_thread::sleep_for(std::chrono::milliseconds(10));}}
}void ThreadPool::stop() {{mQueueMutex.lock();mStop = true;mQueueMutex.unlock();}mCondition.notify_all(); // 通知所有线程退出// 等待所有工作线程完成for (ThreadRegister* worker : mWorkers) {worker->join();delete worker;}mWorkers.clear(); // 清空线程池
}void ThreadPool::resizeThread(size_t newThreadCount) {if (newThreadCount == mWorkers.size()) return;// 增加新线程if (newThreadCount > mWorkers.size()) {size_t threadsToAdd = newThreadCount - mWorkers.size();for (size_t i = 0; i < threadsToAdd; ++i) {auto* worker = new ThreadRegister([this]{ this->workerTask(); });worker->start(nullptr);mWorkers.push_back(worker);}}// 减少线程else {size_t threadsToRemove = mWorkers.size() - newThreadCount;for (size_t i = 0; i < threadsToRemove; ++i) {mQueueMutex.lock();mStop = true;mQueueMutex.unlock();mCondition.notify_one(); // 通知一个线程退出mWorkers.back()->join();delete mWorkers.back();mWorkers.pop_back();}}
}
3.3.1 构造函数
ThreadPool::ThreadPool(size_t threadCount, size_t maxTasks): mStop(false), mMaxTasks(maxTasks)
{if (threadCount < 1 || maxTasks < 1) {std::cerr <<"workers num error"<<std::endl;return;}for(size_t i = 0; i < threadCount; i++) {//使用ThreadRegister创建线程实例,将workerTask作为任务auto* worker = new ThreadRegister([this]{this->workerTask();});//启动对应的线程实例worker -> start(nullptr);//讲线程添加至mWorkers中mWorkers.push_back(worker);}
}

首先判断构造是否合规,然后根据工作线程数threadCount来使用ThreadRegister注册封装好的工作线程,并且将线程添加到vector容器中,构造工作队列。

3.3.2析构函数
ThreadPool::~ThreadPool() {{// 加锁mQueueMutex.lock();mStop = true;mQueueMutex.unlock();}mCondition.notify_all();// 等待所有工作线程完成for (ThreadRegister* worker : mWorkers) {worker->join();delete worker; // 释放动态分配的 ThreadRegister}
}

首先将mStop置为true,然后等待所有工作线程完成,释放动态分配的内存

3.3.3 addTask
template<typename Func, typename... Args>
bool ThreadPool::addTask(Func &func, Args &&... args) {{mQueueMutex.lock();if (mTasks.size() >= mMaxTasks) {std::cerr << "Task queue is full. Cannot add more tasks." << std::endl;mQueueMutex.unlock();return false;}// 添加任务mTasks.emplace(std::bind(std::forward<Func>(func), std::forward<Args>(args)...));mQueueMutex.unlock();}mCondition.notify_one(); // 通知一个空闲线程return true;
}

首先加锁,判断添加任务是否合规(是否大于最大可接受任务数),然年后在mTasks尾构造新的任务,并且通知一个空闲线程。

注意:因为这个函数是模板函数,他的实例化是在编译阶段,因此需要在.h文件中定义可见

3.3.4 workerTask
void ThreadPool::workerTask() {while (true) {std::function<void()> task;{// 手动加锁mQueueMutex.lock();// 检查停止标志和任务队列if (mStop && mTasks.empty()) {mQueueMutex.unlock(); // 手动解锁return; // 线程退出}// 如果有任务,则获取任务if (!mTasks.empty()) {task = std::move(mTasks.front());mTasks.pop();}mQueueMutex.unlock(); // 手动解锁}if (task) {task();}else {//避免忙等待std::this_thread::sleep_for(std::chrono::milliseconds(10));}}
}

加锁,然后检查停止标志以任务队列是否为空,如果任务队列不为空,则获取队首任务,并且判断该任务是否为空,如果不为空则执行,否则线程sleep10毫秒来避免忙等待。

3.3.5 stop和resizeThread

两个功能性函数,分别是强制停止线程以及改变线程池内的工作线程数。

void ThreadPool::stop() {{mQueueMutex.lock();mStop = true;mQueueMutex.unlock();}mCondition.notify_all(); // 通知所有线程退出// 等待所有工作线程完成for (ThreadRegister* worker : mWorkers) {worker->join();delete worker;}mWorkers.clear(); // 清空线程池
}void ThreadPool::resizeThread(size_t newThreadCount) {if (newThreadCount == mWorkers.size()) return;// 增加新线程if (newThreadCount > mWorkers.size()) {size_t threadsToAdd = newThreadCount - mWorkers.size();for (size_t i = 0; i < threadsToAdd; ++i) {auto* worker = new ThreadRegister([this]{ this->workerTask(); });worker->start(nullptr);mWorkers.push_back(worker);}}// 减少线程else {size_t threadsToRemove = mWorkers.size() - newThreadCount;for (size_t i = 0; i < threadsToRemove; ++i) {mQueueMutex.lock();mStop = true;mQueueMutex.unlock();mCondition.notify_one(); // 通知一个线程退出mWorkers.back()->join();delete mWorkers.back();mWorkers.pop_back();}}
}

3.4 测试


void task(int taskid) {std::cout<<"now thread task "<< taskid <<" is runing....."<<std::endl;}int main() {ThreadPool pool(10, 100);unsigned int cores = std::thread::hardware_concurrency();std::cout << "Number of CPU cores: " << cores << std::endl;for (int i=0; i<100; i++) {bool flag = pool.addTask(task, i);}return 0;
}

/tmp/tmp.NkOftkIsac/cmake-build-debug-remote-host2/ServerStudy
Number of CPU cores: 2
now thread task 0 is runing…
now thread task 1 is runing…
now thread task 2 is runing…
now thread task 3 is runing…
now thread task 4 is runing…
now thread task 5 is runing…
now thread task 6 is runing…
now thread task 7 is runing…
now thread task 8 is runing…
now thread task 9 is runing…
now thread task 10 is runing…
now thread task 11 is runing…
now thread task 12 is runing…
now thread task 13 is runing…
now thread task 14 is runing…
now thread task 15 is runing…
now thread task 16 is runing…
now thread task 17 is runing…
now thread task 18 is runing…
now thread task 19 is runing…
now thread task 20 is runing…
now thread task 21 is runing…
now thread task 22 is runing…
now thread task 23 is runing…
now thread task 24 is runing…
now thread task 25 is runing…
now thread task 26 is runing…
now thread task 27 is runing…
now thread task 28 is runing…
now thread task 29 is runing…
now thread task 30 is runing…
now thread task 31 is runing…
now thread task 32 is runing…
now thread task 33 is runing…
now thread task 34 is runing…
now thread task 35 is runing…
now thread task 36 is runing…
now thread task 37 is runing…
now thread task 38 is runing…
now thread task 39 is runing…
now thread task 40 is runing…
now thread task 41 is runing…
now thread task 42 is runing…
now thread task 43 is runing…
now thread task 44 is runing…
now thread task 45 is runing…
now thread task 46 is runing…
now thread task 47 is runing…
now thread task 48 is runing…
now thread task 49 is runing…
now thread task 50 is runing…
now thread task 51 is runing…
now thread task 52 is runing…
now thread task 53 is runing…
now thread task 54 is runing…
now thread task 55 is runing…
now thread task 56 is runing…
now thread task 57 is runing…
now thread task 58 is runing…
now thread task 59 is runing…
now thread task 60 is runing…
now thread task 61 is runing…
now thread task 62 is runing…
now thread task 63 is runing…
now thread task 64 is runing…
now thread task 65 is runing…
now thread task 66 is runing…
now thread task 67 is runing…
now thread task 68 is runing…
now thread task 69 is runing…
now thread task 70 is runing…
now thread task 71 is runing…
now thread task 72 is runing…
now thread task 73 is runing…
now thread task 74 is runing…
now thread task 75 is runing…
now thread task 76 is runing…
now thread task 77 is runing…
now thread task 78 is runing…
now thread task 79 is runing…
now thread task 80 is runing…
now thread task 81 is runing…
now thread task 82 is runing…
now thread task 83 is runing…
now thread task 84 is runing…
now thread task 85 is runing…
now thread task 86 is runing…
now thread task 87 is runing…
now thread task 88 is runing…
now thread task 89 is runing…
now thread task 90 is runing…
now thread task 91 is runing…
now thread task 92 is runing…
now thread task 93 is runing…
now thread task 94 is runing…
now thread task 95 is runing…
now thread task 96 is runing…
now thread task 97 is runing…
now thread task 98 is runing…
now thread task 99 is runing…


http://www.mrgr.cn/news/59882.html

相关文章:

  • EfficientNet-B6模型实现ISIC皮肤镜图像数据集分类
  • C++共同体
  • Android 原生开发与Harmony原生开发浅析
  • 复盘秋招22场面试(四)形势重新评估与后续措施
  • 用哪种建站程序做谷歌SEO更容易?
  • virtuoso设计一个CMOS反相器并进行仿真
  • 【Linux】MySQL主从复制
  • 宝安区石岩上排停车场(月卡350元)
  • 使用Python实现深度学习模型:智能极端天气事件预测
  • git合并上传小技巧
  • flutter vscode app 的输出在哪里
  • 新160个crackme - 084-slayer_crackme1
  • shutdown abort关库,真的可能起不来吗?
  • 一文彻底搞定MySQL中的JSON类型,效率飞起。
  • 软硬件开发面试问题大汇总篇——针对非常规八股问题的提问与应答(代码规范与生态管理)
  • shodan2---清风
  • 2025 - AI人工智能药物设计 - 中药网络药理学和毒理学的研究
  • opencv-platform实现人脸识别
  • 二十三、Python基础语法(包)
  • Upload-labs通关
  • Python 从入门到实战41(NumPy数值计算)
  • kNN 的花式用法(原来还能这么玩 kNN)
  • Java NIO 应知应会 (一)
  • FFmpeg 4.3 音视频-多路H265监控录放C++开发六,使用SDLVSQT显示yuv文件
  • UE ---- 射击游戏
  • 【Linux网络】传输层协议UDP与TCP