スレッド プール - 手書きスレッド プール C++11 バージョン (プロデューサー/コンシューマー モデル)

このプロジェクトは、C++11 ベースのスレッド プールです。テンプレート関数のジェネリック プログラミング、std::future、std::packages_task、std::bind、std::forward 完全転送、std::make_shared スマート ポインター、decltype 型推論など、C++ の多くの新機能が使用されていますが、これらに限定されません。 、std::unique_lock およびその他の C++11 の新機能。


このプロジェクトにはある程度の難易度があります。推奨される参考記事シリーズ

C++11実践技術(1) autoとdecltypeの使い方

C++11実践技術(2) std::functionとバインドバインダー

C++11実践技術(3) std::future、std::promise、std::packages_task、async

C++ 演算子キーワードの使用 (オーバーロードされた演算子、ファンクター、型変換演算子)

参照の右辺値と移動セマンティクスおよび前方完全転送

C++ 標準ライブラリの lock_guard、unique_lock、shared_lock、scoped_lock、recursive_mutex をロックします


コード構造

本プロジェクトのスレッドプール機能は以下の機能に分割して実現しています。

threadpool.init(isize_t num); スレッド数を設定
threadpool::get(TaskFuncPtr& task); タスクキュー内のタスクを読み取る
threadpool::run(); get() を通じてタスクを読み取り、
threadpool.start()を実行する; スレッド プールを開始し、run() を通じてタスクを実行します
threadpool.exec(); タスクをタスク キューにカプセル化します
threadpool.waitForAllDone(); すべてのタスクが実行されるまで待機します
threadpool.stop(); スレッドを分離して解放します想い出

スレッドプール.init

init の機能はスレッド プールを初期化し、主にクラスのメンバー変数にスレッドの数を設定することです。

bool ZERO_ThreadPool::init(size_t num)
{
    
    
	std::unique_lock<std::mutex> lock(mutex_);

	if (!threads_.empty())
	{
    
    
		return false;
	}

	threadNum_ = num;
	return true;
}

threadNum_: init 関数で割り当てられたスレッドの数を保存します。

ここで、unique_lockやlock_guardを用いたロック方法により、自動ロック・ロック解除を実現できます。ただし、unique_lock は一時的にロックを解除したり再ロックしたりできますが、lock_guard はできないため、特殊な場合 (条件変数が使用される場合) には unique_lock を使用する必要があります。(lock_guard は比較的シンプルで、比較的パフォーマンスが良いです)

スレッドプール::get

タスク キュー (実際にはコンシューマ モジュール)からタスクを取得します。

bool ZERO_ThreadPool::get(TaskFuncPtr& task)
{
    
    
	std::unique_lock<std::mutex> lock(mutex_);
	if (tasks_.empty()) //判断任务是否存在
	{
    
    
		//要终止线程池   bTerminate_设置为true,任务队列不为空
		condition_.wait(lock, [this] {
    
     return bTerminate_ || !tasks_.empty(); });
	}
	if (bTerminate_)
		return false;
	if (!tasks_.empty())
	{
    
    
		task = std::move(tasks_.front());  // 使用了移动语义
		tasks_.pop(); //释放资源,释放一个任务
		return true;
	}
	return false;
}
  • 条件変数condition_.wait(lock, [this] { return bTerminate_ || !tasks_.empty(); }); は、条件が完了するまで待ってから終了する必要があります。つまり、タスクが終了するか、タスク キューが空でない場合、条件変数のブロック状態を終了し、次のロジックの実行を継続します。

  • task = std::move(tasks_.front()); は、移動セマンティクスを使用し、tasks_.front() の内容を task に移動します。コンテンツのコピーを削減できます。移動後はtasks_.front()の内容が不定になるのでそのままポップしてください。

スレッドプール::実行

これがタスクを実行する部分です。get を呼び出してタスク キュー内のタスクを取得し、タスクを実行することも含まれます。

void ZERO_ThreadPool::run()  // 执行任务的线程
{
    
    
	//调用处理部分
	while (!isTerminate()) // 判断是不是要停止
	{
    
    
		TaskFuncPtr task;
		bool ok = get(task);        // 读取任务
		if (ok)
		{
    
    
			++atomic_;
			try
			{
    
    
				if (task->_expireTime != 0 && task->_expireTime < TNOWMS)
				{
    
    //如果设置了超时,并且超时了,就需要执行本逻辑
				//超时任务,本代码未实现,有需要可实现在此处
				}
				else
				{
    
    
					task->_func();  // 执行任务
				}
			}
			catch (...)
			{
    
    
			}
			--atomic_;
			}
		}
	}
}

atomic_ : タスクを実行する場合、パラメーターは +1 であり、実行後はパラメーターは -1 です。これは、後でスレッド プールを停止したときに、まだ実行中のタスク (未完了のスレッド) が存在するかどうかを判断するためです。

スレッドプール.スタート

スレッドを作成し、ベクターにスレッド プールを格納します。後でスレッド プールを解放する場合は、スレッドを 1 つずつ解放するのが最善です。

bool ZERO_ThreadPool::start()
{
    
    
	std::unique_lock<std::mutex> lock(mutex_);
	if (!threads_.empty())
	{
    
    
		return false;
	}
	for (size_t i = 0; i < threadNum_; i++)
	{
    
    
		threads_.push_back(new thread(&ZERO_ThreadPool::run, this));
	}
	return true;
}

thread_.push_back(new thread(&ZERO_ThreadPool::run, this)); スレッドを作成し、スレッドのコールバック関数が実行されます。

スレッドプール.exec

exec はタスクをタスク キューに保存することです。このコードは、C++ の新機能を多数使用するこのプロジェクトの最も難しい部分です。

/*
	template <class F, class... Args>
	它是c++里新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数
	auto exec(F &&f, Args &&... args) -> std::future<decltype(f(args...))>
	std::future<decltype(f(args...))>:返回future,调用者可以通过future获取返回值
	返回值后置
	*/
	template <class F, class... Args>
	auto exec(int64_t timeoutMs, F&& f, Args&&... args) -> std::future<decltype(f(args...))>//接受一个超时时间 `timeoutMs`,一个可调用对象 `f` 和其它参数 `args...`,并返回一个 `std::future` 对象,该对象可以用于获取任务执行的结果。
	{
    
    
		int64_t expireTime = (timeoutMs == 0 ? 0 : TNOWMS + timeoutMs);  // 根据超时时间计算任务的过期时间 `expireTime`,如果超时时间为 0,则任务不会过期。
		//定义返回值类型
		using RetType = decltype(f(args...));  // 使用 `decltype` 推导出 `f(args...)` 的返回值类型,并将其定义为 `RetType`(这里的using和typedef功能一样,就是为一个类型起一个别名)。
		// 封装任务 使用 `std::packaged_task` 将可调用对象 `f` 和其它参数 `args...` 封装成一个可执行的函数,并将其存储在一个 `std::shared_ptr` 对象 `task` 中。
		auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

		TaskFuncPtr fPtr = std::make_shared<TaskFunc>(expireTime);  // 封装任务指针,设置过期时间 创建一个 `TaskFunc` 对象,并将任务的过期时间 `expireTime` 传递给它。
		fPtr->_func = [task]() {
    
      // 具体执行的函数 将封装好的任务函数存储在 `TaskFunc` 对象的 `_func` 成员中,该函数会在任务执行时被调用。
			(*task)();
		};

		std::unique_lock<std::mutex> lock(mutex_);
		tasks_.push(fPtr);              // 将任务插入任务队列中
		condition_.notify_one();        // 唤醒阻塞的线程,可以考虑只有任务队列为空的情况再去notify

		return task->get_future();; //返回一个 `std::future` 对象,该对象可以用于获取任务执行的结果。
	}

可変個引数テンプレート関数が使用されます。
task_: タスクを保存するためのキュー
condition_.notify_one(): タスクを保存して条件変数を起動します
std::future: タスクを非同期にポイントし、future 機能を使用してタスク関数の戻り結果を取得します。
std::bind: パラメータ リストを関数にバインドして、新しい呼び出し可能オブジェクトを生成します。
std::packages_task: タスクと機能をバインドするテンプレート。これはタスクのカプセル化の一種です。

この関数は汎用プログラミング テンプレート関数を使用しており、タイムアウト時間timeoutMs、呼び出し可能オブジェクトf、パラメータの3 つの入力パラメータがありますargs...戻り値に接尾辞を付けた std::future オブジェクトを返します。ここで戻り値の接尾辞は、データ型を推測するための decltype(f(args...) の使用を容易にするために使用されます。

auto task = std::make_shared<std::packages_task<RetType()>>(std::bind(std::forward(f), std::forward(args)...)); は渡したタスクですin 関数とパラメータはオブジェクトにバインドされており、関数全体とみなすことができ、その戻り値は RetType 型で、入力パラメータはありません。したがって、パッケージ化およびパッケージ化には std::packages_task<RetType()> のような形式を使用します。カプセル化されたオブジェクトはスマート ポインター (std::make_shared) で管理されます。

同時に、スマート ポインターによって管理される TaskFunc オブジェクトを作成する必要があります。このオブジェクトには 2 つの項目が含まれており、1 つはタイムアウト時間、もう 1 つはカプセル化したタスク オブジェクトです。これら 2 つの項目を
TaskFuncPtr fPtr = std::make_shared(expireTime); および fPtr->_func = task {(*task)();}; の 2 つのコードを通じて渡します。

最後に、タスク関数の実行結果の戻り値が、task->get_future() を通じて返されます。

threadpool.waitForAllDone

すべてのタスクの実行が完了するまで待ちます。

bool ZERO_ThreadPool::waitForAllDone(int millsecond)
{
    
    
	std::unique_lock<std::mutex> lock(mutex_);
	if (tasks_.empty() && atomic_ == 0)
		return true;
	if (millsecond < 0)
	{
    
    
		condition_.wait(lock, [this] {
    
     return tasks_.empty() && atomic_ == 0; });
		return true;
	}
	else
	{
    
    
		return condition_.wait_for(lock, std::chrono::milliseconds(millsecond), [this] {
    
     return tasks_.empty() && atomic_ == 0; });
	}
}

条件変数を使用して、タスクの実行が完了するのを待ちます。タイムアウト実行機能をサポートします。

ここで unique_lock を使用する必要があります。条件変数condition_ はロックが解除され、待機中にスリープ状態になります。lock_guard にはこの操作インターフェイスがありません。

スレッドプール.停止

スレッドプールを終了します。waitForAllDone を呼び出して、すべてのタスクが完了するのを待ってから終了します。

void ZERO_ThreadPool::stop()
{
    
    
	{
    
    
		std::unique_lock<std::mutex> lock(mutex_);
		bTerminate_ = true;
		condition_.notify_all();
	}
	waitForAllDone();
	for (size_t i = 0; i < threads_.size(); i++)
	{
    
    
		if (threads_[i]->joinable())
		{
    
    
			threads_[i]->join();
		}
		delete threads_[i];
		threads_[i] = NULL;
	}
	std::unique_lock<std::mutex> lock(mutex_);
	threads_.clear();
}

joinなどのスレッドの実行が完了するとリターンします。

メイン関数呼び出し

class Test
{
    
    
public:
	int test(int i) {
    
    
		cout << _name << ", i = " << i << endl;
		Sleep(1000);
		return i;
	}
	void setName(string name) {
    
    
		_name = name;
	}
	string _name;
};
void test3() // 测试类对象函数的绑定
{
    
    
	ZERO_ThreadPool threadpool;
	threadpool.init(2);
	threadpool.start(); // 启动线程池
	Test t1;
	Test t2;
	t1.setName("Test1");
	t2.setName("Test2");
	auto f1 = threadpool.exec(std::bind(&Test::test, &t1, std::placeholders::_1), 10);
	auto f2 = threadpool.exec(std::bind(&Test::test, &t2, std::placeholders::_1), 20);
	cout << "t1 " << f1.get() << endl;
	cout << "t2 " << f2.get() << endl;
	threadpool.stop();
}
int main()
{
    
    
	test3(); // 测试类对象函数的绑定
	cout << "main finish!" << endl;
	return 0;
}

操作結果:
ここに画像の説明を挿入

このプロジェクトの完全なコードのダウンロード アドレスは、C++11 のスレッド プールに基づいています。

おすすめ

転載: blog.csdn.net/weixin_44477424/article/details/132155242