C++并发编程:使用C++实现线程安全的栈

引言

在多线程编程中,数据结构的线程安全性是至关重要的。本文将详细介绍如何使用C++20标准库中的一些新特性来实现一个线程安全的栈。

什么是线程安全的栈?

简单来说,一个线程安全的栈是一个可以被多个线程同时访问而不会导致数据不一致或其他未定义行为的栈。

自定义异常类:empty_stack

在实现线程安全的栈之前,我们首先定义一个自定义异常,用于表示栈为空的情况。

struct empty_stack : std::exception
{
	const char* what() const noexcept override
	{
		return "Stack is empty!";
	}
};

这里,我们继承了std::exception类,并重写了what()方法。

主体结构:threadsafe_stack

然后,我们定义了一个名为threadsafe_stack的模板类。

template <typename T>
class threadsafe_stack
{
private:
	std::stack<T> data;
	mutable std::mutex m;
	// ... 后续代码
};

其中,data是用于存储数据的STL栈,而m是一个可变的互斥量,用于在多线程环境中保护data

入栈操作:push

我们使用push方法来添加一个新元素到栈顶。

void push(T new_value) noexcept
{
	std::scoped_lock lk(m);
	data.push(std::move(new_value));
}

在这里,我们使用std::scoped_lock来自动管理锁的生命周期,并使用std::move来进行移动语义,以提高性能。

出栈操作:pop

对于出栈操作,我们提供了两种方式:一种返回一个shared_ptr,另一种将值存储在一个引用参数中。

返回shared_ptrpop

std::shared_ptr<T> pop()
{
	std::scoped_lock lk(m);
	if (data.empty()) throw empty_stack();
	auto const res(std::make_shared<T>(std::move(data.top())));
	data.pop();
	return res;
}

将值存储在引用中的pop

void pop(T& value)
{
	std::scoped_lock lk(m);
	if (data.empty()) throw empty_stack();
	value = std::move(data.top());
	data.pop();
}

在这两种情况下,我们都首先检查栈是否为空,并在必要时抛出自定义的empty_stack异常。

检查栈是否为空:empty

此外,我们还提供了一个empty方法,用于检查栈是否为空。

bool empty() const noexcept
{
	std::scoped_lock lk(m);
	return data.empty();
}

测试:test_threadsafe_stack

最后,我们通过一个简单的测试函数来演示如何使用这个线程安全的栈。

void test_threadsafe_stack()
{
	threadsafe_stack<int> s;

	std::thread t1([&]()
	{
		for (int i = 0; i < 10; ++i)
		{
			s.push(i);
			std::cout << "Pushed " << i << std::endl;
		}
	});

	std::thread t2([&]()
	{
		for (int i = 0; i < 10; ++i)
		{
			try
			{
				auto val = s.pop();
				std::cout << "Popped " << *val << std::endl;
			}
			catch (const empty_stack& e)
			{
				std::cout << "Exception: " << e.what() << std::endl;
			}
		}
	});

	t1.join();
	t2.join();
}

总结

使用C++20的新特性,如std::scoped_lock,我们能更方便地实现线程安全的数据结构。本文详细介绍了如何实现一个线程安全的栈,并提供了完整的代码示例。希望这能帮助你更好地理解多线程编程和C++20的一些新特性。