65.9K
CodeProject 正在变化。 阅读更多。
Home

reuse: 一个对象回收 C++ 类库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2022年2月23日

Apache

3分钟阅读

viewsIcon

8227

downloadIcon

125

回收创建成本高的对象, 提高应用程序性能

引言

在本文中,我将介绍 reuse 类库:库的接口,以及为其提供支持的对象池的实现。 使用此类库,您可以为自己的资源密集型类获得数据库连接池的优势。

reuse 类库

此类库驻留在一个文件 *reuse.h* 中,包含三个类:reusablepoolreuse

reusable

第一个类是基类 reusable

class reusable
{
protected:
	reusable(const std::wstring& initializer = L"")
		: m_initializer(initializer)
	{}

public:
	virtual void clean() {}
	virtual bool cleanInBackground() const { return false; }
	virtual std::wstring initializer() const { return m_initializer; }

private:
	std::wstring m_initializer;
};

您可以围绕 reusable 基类构建类库,在派生类型中以不同的方式实现 cleanInBackground()clean()

但您不必这样做。

只要您的类实现了 cleanInBackground()clean()initializer(),您就不需要使用 reusable 基类。 这是因为对象池是一个模板类,可以处理任何类型的对象,而不仅仅是派生自 reusable 的类型。

pool

主类是 pool

template <class T>
class pool
{
public:
	/// <summary>
	/// Declare a pool object for a type with given object count limits
	/// </summary>
	/// <param name="constructor">What object should be created based on the initializer?
	/// </param>
	/// <param name="maxInventory">How many objects can the pool hold 
	/// before objects put for recycling are dropped (deleted)?</param>
	/// <param name="maxToClean">How many objects can be in queue 
	/// for cleaned before objects are dropped (deleted)?</param>
	pool
	(
		const std::function<T* (const std::wstring&)> constructor,
        const bool allowBackgroudCleanup = true, 
		const size_t maxInventory = 1000U, 
		const size_t maxToClean = 1000U
	);

	/// <summary>
	/// Get a reuse object to get an object from the pool
	/// and automatically return it back to the pool
	/// </summary>
	/// <param name="initializer">Initializer for the object to return</param>
	/// <returns>
	/// An object for gaining access to a pooled object
	/// </returns>
	auto use(const std::wstring& initializer = L"")
	{
		return reuse<T>(*this, initializer);
	}

private:
	/// <summary>
	/// Get an object for a given initializer string
	/// Objects are created using initializer strings, used, then put() and clean()'d
	/// and handed back out
	/// </summary>
	/// <param name="initializer">Initializer for the object to return</param>
	/// <returns>Pointer to a new or reused object</returns>
	T* get(const std::wstring& initializer);

	/// <summary>
	/// Hand an object back to the pool for reuse
	/// </summary>
	void put(T* t);

	/// <summary>
	/// Thread routine for cleaning up objects in the background
	/// </summary>
	void cleanup();
};

要使用池,您需要使用一个构造函数来构造一个池,该构造函数返回类型为 T 的对象,并限制池内部资源使用量的大小。

然后,您使用初始化字符串调用 use() 以传递给您的构造函数。 use() 返回一个 reuse<T> 对象,您保留该对象以使用池化对象,并且当您使用完该对象时,reuse<T> 对象会将对象返回给池。

您提供的构造函数可以返回与模板类 T 兼容的任何内容。 它可以决定通过处理初始化器 string 返回的对象类型。 这允许使用类型安全但动态的对象池,如果您愿意,可以在其中使用继承,但这不是必需的。

reuse

reuse<T>pool<T>::use() 返回,用于访问池中的对象

template <class T>
class reuse
{
public:
    /// <summary>
    /// Declaring an instance of this class gets a T object from the pool
    /// When this reuse object goes out of scope, the T object is returned to the pool
    /// </summary>
    /// <param name="pool">pool to get objects from and put objects back</param>
    /// <param name="initializer">string to initialize the object</param>
    reuse(pool<T>& pool, const std::wstring& initializer = L"")
      : m_pool(pool)
    {
        m_t = m_pool.get(initializer);
    }

    /// <summary>
    /// Cannot copy because of pointer owned by other
    /// Moving is okay
    /// </summary>
    reuse(const reuse& other) = delete;

    /// <summary>
    /// Cannot assign because of pointer owned by other
    /// Moving is okay
    /// </summary>
    reuse& operator=(const reuse& other) = delete;

    /// <summary>
	/// Move constructor to carry along the object pointer
	/// without an unnecessary get() / put() pair of pool calls 
	/// </summary>
	reuse(reuse&& other)
		: m_pool(other.m_pool)
		, m_t(other.m_t)
	{
		other.m_t = nullptr;
	}
	
	// Free the object back to the pool
	~reuse()
	{
		m_pool.put(m_t);
	}

	/// <summary>
	/// Access the pooled object
	/// </summary>
	T& get() { return *m_t; }

private:
	pool<T>& m_pool;
	T* m_t;
};

pool 实现

pool 实现围绕 get()put()cleanup() 展开。

get()

特殊考虑用于对初始化器字符串不感兴趣的用例。 为此用例保留了一个桶 (vector<T*>)。

当使用初始化器字符串时,每个唯一的初始化器字符串都有一个专用桶。 这允许每个池使用多个初始化器字符串,例如用于不同数据库的不同连接字符串,所有这些都使用相同类型的数据库连接类,或者使用具有公共基类的不同数据库连接类。 这可以成为解决您的问题的动态解决方案。

T* get(const std::wstring& initializer)
{
	if (m_keepRunning)
	{
		std::unique_lock<std::mutex> lock(m_bucketMutex);

		// Consider using the null bucket used with empty initializer strings
		if (initializer.empty()) 
		{
			if (!m_unBucket.empty())
			{
				T* ret_val = m_unBucket.back();
				m_unBucket.pop_back();
				m_size.fetch_add(-1);
				return ret_val;
			}
		}
		else // we're using initializer strings
		{
			// Find the bucket for the initializer string
			// See if a matching bucket exists and has objects to hand out
			const auto& it = m_initBuckets.find(initializer);
			if (it != m_initBuckets.end() && !it->second.empty())
			{ 
				T* ret_val = it->second.back();
				it->second.pop_back();
				m_size.fetch_add(-1);
				return ret_val;
			}
		}
	}

	// Failing all of that, including whether we should keep running,
	// construct a new T object with the initializer
	return m_constructor(initializer);
}

put()

如果要在重用对象之前回收对象,则必须调用其 clean() 函数。

是否应立即调用 clean() 或在后台调用 clean() 取决于对象。

/// <summary>
/// Hand an object back to the pool for reuse
/// </summary>
void put(T* t)
{
	if (t == nullptr)
		return;

	if (m_keepRunning)
	{
		if (t->cleanInBackground()) // queue up the object for background cleaning
		{
			std::unique_lock<std::mutex> lock(m_incomingMutex);
			if (m_incoming.size() < m_maxToClean)
			{
				m_incoming.push_back(t);
				m_incomingCondition.notify_one();
				return;
			}
		}
		else // clean up and directly add to the right bucket
		{
			if (m_size.load() < m_maxInventory)
			{
                t->clean(); 

				const std::wstring& initializer = t->initializer();
				std::unique_lock<std::mutex> lock(m_bucketMutex);
				if (initializer.empty())
					m_unBucket.push_back(t);
				else
					m_initBuckets[initializer].push_back(t);
				m_size.fetch_add(1);
				return;
			}
		}
	}

	// Failing all of that, including whether we should keep running, drop the object (delete)
	delete t;
}

cleanup()

后台清理处理获取要清理的对象,清理它们,然后将它们添加到库存以供重用。

/// <summary>
/// Thread routine for cleaning up objects in the background
/// </summary>
void cleanup()
{
    // Bail early on cleanup if we're not to clean in the background
    if (!m_allowBackgroudCleanup)
    {
        m_doneCleaning = true;
        return;
    }

	while (m_keepRunning)
	{
		// Get something to clean
		T* t;
		{
			std::unique_lock<std::mutex> lock(m_incomingMutex);
			m_incomingCondition.wait_for
			(
				lock,
				10ms,
				[&] { return !m_incoming.empty() || !m_keepRunning; }
			);
			if (m_incoming.empty() || !m_keepRunning)
				continue;

			t = m_incoming.back();
			m_incoming.pop_back();
		}

		// Delete the object if our shelves are full
		if (m_size.load() >= m_maxInventory)
		{
			delete t;
			continue;
		}

		// Clean it
		t->clean();

		// Add the object to the right pool
		std::wstring initializer = t->initializer();
		std::unique_lock<std::mutex> lock(m_bucketMutex);
		if (initializer.empty())
			m_unBucket.push_back(t);
		else
			m_initBuckets[initializer].push_back(t);
		m_size.fetch_add(1);
	}
	
	// All done.
	m_doneCleaning = true;

示例 reuse 库用法

reuse 单元测试

单元测试显示了库的用法,并涵盖了同步和异步清理

#include "CppUnitTest.h"

#include "../reuse/reuse.h"

#include <thread>

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

namespace reuse
{
	std::atomic<int> test_class_count = 0;

	class test_class : public reusable
	{
	public:
		test_class(const std::wstring& initializer, const bool cleanInBackground)
			: reusable(initializer)
			, m_cleanInBackground(cleanInBackground)
		{
			test_class_count.fetch_add(1);
		}

		~test_class()
		{
			clean();
			test_class_count.fetch_add(-1);
		}

		virtual void clean()
		{
			data = "";
		}
		virtual bool cleanInBackground() const { return m_cleanInBackground; }

		void process()
		{
			data = "914";
		}

		std::string data;

	private:
		const bool m_cleanInBackground;
	};

	TEST_CLASS(reusetests)
	{
	public:
		TEST_METHOD(TestReuse)
		{
			// Test cleaning in the foreground and background
			for (int run = 1; run <= 4; ++run)
			{
				std::vector<bool> should_clean_in_bgs{ true, false };
				for (bool should_clean_in_bg : should_clean_in_bgs)
				{
					// Create our pool with a constructor function
					// that passes in how to do the cleaning
					pool<test_class>
						pool
						(
							[&](const std::wstring& initializer)
							{
								return new test_class(initializer, should_clean_in_bg);
							},
                            should_clean_in_bg
					    );

					// NOTE: We'll hold onto a pool pointer in p so after we're done using 
					// the object, we can inspect the object 
                    // as it sits in the pool's inventory.
					test_class* p = nullptr;
					{
						// Use the pool to get a test_class object
						auto use = pool.use(L"init");

						// Access the test_class object inside the reuse object
						test_class& obj = use.get();
						p = &obj; // stash off the pointer for later

						// See that the object is clean
						Assert::AreEqual(std::string(), obj.data);
						Assert::AreEqual(std::wstring(L"init"), obj.initializer());

						// Use the object
						obj.process();
						Assert::AreEqual(std::string("914"), obj.data);
					}
					if (should_clean_in_bg) // wait for cleanup
						std::this_thread::sleep_for(1s);

					// See that the object is clean in the pool, ready for reuse
					Assert::AreEqual(std::wstring(L"init"), p->initializer());
					Assert::AreEqual(std::string(), p->data);
				}
			}
		}
	};
}

reuse 性能分析

性能分析程序使用 SQLite 包装类 - 4db,大幅缩减 - 来比较每次创建新数据库连接所花费的时间与重用连接所花费的时间

#include "../reuse/reuse.h"

#include "db.h"

#include <chrono>
#include <iostream>

// Define our SQLite (well, 4db) reusable wrapper
class sqlite_reuse : public reuse::reusable
{
public:
	sqlite_reuse(const std::wstring& filePath)
		: reuse::reusable(filePath)
		, m_db(filePath)
	{}

	fourdb::db& db() { return m_db; }

	// defaults to no-op clean and foreground (no-op) clean
	// and initializer() for free
private:
	fourdb::db m_db; // our resource-intense cargo
};

typedef std::chrono::high_resolution_clock high_resolution_clock;
typedef std::chrono::milliseconds milliseconds;

int wmain(int argc, wchar_t* argv[])
{
	if (argc < 2)
	{
		std::cout << "Usage: <db file path>" << std::endl;
		return 0;
	}

	std::wstring db_file_path = argv[1];

	size_t loopCount = 1000;

	std::string sql_query = "SELECT tbl_name FROM sqlite_master WHERE type = 'table'";

	for (int run = 1; run <= 10; ++run)
	{
		// Do the standard DB connect / query / close
		{
			std::cout << "Traditional: ";
			auto start = high_resolution_clock::now();
			for (size_t c = 1; c <= loopCount; ++c)
			{
				// This one-liner does it all
				fourdb::db(db_file_path).exec(sql_query);
			}
			auto elapsedMs = std::chrono::duration_cast<milliseconds>
                             (high_resolution_clock::now() - start);
			std::cout << elapsedMs.count() << "ms" << std::endl;
		}

		// Pool the database connection and reuse it
		{
			std::cout << "Pooled: ";
			auto start = high_resolution_clock::now();
			{
				// Create the pool
				reuse::pool<sqlite_reuse>
					pool
					(
						[](const std::wstring& initializer)
						{
							return new sqlite_reuse(initializer);
						}
					);
				for (size_t c = 1; c <= loopCount; ++c)
				{
					// This even longer one-liner does it all
					pool.use(db_file_path).get().db().exec(sql_query);
				}
			}
			auto elapsedMs = std::chrono::duration_cast<milliseconds>
                             (high_resolution_clock::now() - start);
			std::cout << elapsedMs.count() << "ms" << std::endl;
		}
	}

	return 0;
}

我们比较的是以下成本

fourdb::db(db_file_path).exec(sql_query);

pool.use(db_file_path).get().db().exec(sql_query);

运行 1,000 次,每次都创建一个新连接并使用它需要 400 毫秒。 重用数据库连接仅需 30 毫秒。

结论

我已经展示了 reuse 类库是多么容易集成和使用,以及对象回收可以带来多么显著的性能提升。

历史

  • 2022年2月22日:初始版本
© . All rights reserved.