【UE4源代码观察】学习队列模板TQueue

队列

“队列”是一个基础的数据结构,UE4对其有模板实现:TQueue,它在\Engine\Source\Runtime\Core\Public\Containers\Queue.h中定义。

本篇记录了对它源码的观察。大体上,可以看到一个队列结构的教科书式的实现。但是代码实现上还牵扯到一些我还不熟悉的知识,因此我也将其记录下来。

TQueue

/**
 * Template for queues.
 *
 * This template implements an unbounded non-intrusive queue using a lock-free linked
 * list that stores copies of the queued items. The template can operate in two modes:
 * Multiple-producers single-consumer (MPSC) and Single-producer single-consumer (SPSC).
 *
 * The queue is thread-safe in both modes. The Dequeue() method ensures thread-safety by
 * writing it in a way that does not depend on possible instruction reordering on the CPU.
 * The Enqueue() method uses an atomic compare-and-swap in multiple-producers scenarios.
 *
 * @param ItemType The type of items stored in the queue.
 * @param Mode The queue mode (single-producer, single-consumer by default).
 * @todo gmp: Implement node pooling.
 */
template<typename ItemType, EQueueMode Mode = EQueueMode::Spsc>
class TQueue

TQueue是队列结构的模板。
这个模板实现了一种“不限数目”、“不能插队(non-intrusive)”、“不需要线程锁(lock-free)”的链表,而表中存放的是对象的拷贝。这个模板可在两种模式下操作:“多方生产单方使用(MPSC)”和“单方生产单方使用(SPSC)”。
它在两种模式下都是“线程安全的”。Dequeue()能够保证线程安全,因为它在写法上就不依赖于CPU上可能的指令重新排序。Enqueue()在MPSC模式中使用了原子“比较交换(compare-and-swap)”操作

前置知识

关键字 volatile

C Language Keywords中指出volatile关键字的作用:

Every reference to the variable will reload the contents from memory rather than take advantage of situations where a copy can be in a register.
此关键字表示变量将一定从内存中加载,而不会优化为从寄存器中加载它的拷贝

之所以禁止这种优化,是因为它在一些情况下会出问题(尤其是在多线程中?)。我对硬件方面不了解,理解可能有误。
详细见《C语言再学习 – 关键字volatile_不积跬步,无以至千里-CSDN博客_c语言中volatile》中的讨论。

关键字 explicit

默认情况下,当自己定义了一种转换构造函数(指“只有一个参数”或者“其余参数都有默认值”的构造函数),那么编译器可以使用“隐式”的转换,例如:

class ClassA
{
public:
	 ClassA(int data) {}
};

int main()
{
	ClassA a = 3;
}

上面ClassA的一个构造函数是使用int类型变量作为参数,那么ClassA a = 3;语句将显示地用3来作为参数调用ClassA(int data)这个转换构造函数。

但是,这种“默认”有时候是自己不想要的,如果使用explicit关键字,则可以避免。例如,下面代码将不会编译通过:

class ClassA
{
public:
	 explicit ClassA(int data) {}
};

int main()
{
	ClassA a = 3;
}

报错为:

1>error C2440: “初始化”: 无法从“int”转换为“ClassA”
1>note: class“ClassA”的构造函数声明为“explicit”

要想使用,必须清楚地表示要调用转换构造函数:

int main()
{
	ClassA a(3);
}

详细见:用户定义的类型转换 (C++) | Microsoft Docs

使用“= delete”禁用“拷贝构造函数”与“赋值操作符”

默认情况下,一个类会自动生成复制构造函数复制赋值运算符,例如:

class ClassA
{
};

int main()
{
	ClassA a;
	ClassA b = a;
}

虽然没有显式地声明复制赋值运算符复制构造函数,但ClassA b = a依旧可以执行。

如果想禁止这种功能,则可以用= delete来禁止,例如下面代码将不会编译通过:

class ClassA
{
public:
	ClassA() {};
	ClassA(const ClassA&) = delete;
	ClassA& operator=(const ClassA&) = delete;
};

int main()
{
	ClassA a;
	ClassA b = a;
}

报错:

1>error C2280: “ClassA::ClassA(const ClassA &)”: 尝试引用已删除的函数
1>note: 参见“ClassA::ClassA”的声明
1>note: “ClassA::ClassA(const ClassA &)”: 已显式删除函数

详细见:显式默认设置的函数和已删除的函数 | Microsoft Docs

右值引用“&&”,UE4的MoveTemp

“右值引用”的讨论详见上一篇博客《学习C++右值引用》
而UE4的MoveTemp相当于std::move

宏 MS_ALIGN 和 GCC_ALIGN

#define MS_ALIGN(n) __declspec(align(n))

指定了对齐。而对齐的目的主要是处于跨硬件平台效率上的目的。
详细见:align (C++) | Microsoft Docs

宏 TSAN_AFTER 和 TSAN_BEFORE

这两个宏暂时都被定义为了空的:

#if USING_THREAD_SANITISER
	#if !defined( TSAN_SAFE ) || !defined( TSAN_BEFORE ) || !defined( TSAN_AFTER ) || !defined( TSAN_ATOMIC )
		#error Thread Sanitiser macros are not configured correctly for this platform
	#endif
#else
	// Define TSAN macros to empty when not enabled
	#define TSAN_SAFE
	#define TSAN_BEFORE(Addr)
	#define TSAN_AFTER(Addr)
	#define TSAN_ATOMIC(Type) Type
#endif

相关内容待后续研究。

FPlatformAtomics::InterlockedExchangePtr

以原子操作的形式,将对象设置为指定的值并返回对原始对象的引用。

FPlatformMisc::MemoryBarrier

实现了内存屏障

关于内存屏障,可见讨论:Memory barrier是什么? - 知乎

概括讲:CPU可能并不会完全按代码中的顺序来执行语句,例如:

a = 3;
b = 4;

这两条语句实际上谁先谁后执行在单线程中是无所谓的,CPU可能会对其做一些优化,但这中优化在多线程中可能会导致问题。而加了内存屏障,可以保证屏障上一定先执行,屏障下的后执行。

队列思路

基本上来看,这里队列实现思路比较标准,运行起来会是这样的结构:
在这里插入图片描述

初始化时:
在这里插入图片描述

入列一个对象:
在这里插入图片描述
出列一个对象:
在这里插入图片描述

观察代码

TNode类

TNode是在TQueue中的一个私有的定义,代表了链表中的一个节点:

/** Structure for the internal linked list. */
	struct TNode
	{
		/** Holds a pointer to the next node in the list. */
		TNode* volatile NextNode;

		/** Holds the node's item. */
		ItemType Item;

		/** Default constructor. */
		TNode()
			: NextNode(nullptr)
		{ }

		/** Creates and initializes a new node. */
		explicit TNode(const ItemType& InItem)
			: NextNode(nullptr)
			, Item(InItem)
		{ }

		/** Creates and initializes a new node. */
		explicit TNode(ItemType&& InItem)
			: NextNode(nullptr)
			, Item(MoveTemp(InItem))
		{ }
	};

NextNode是指向了链表中的下一个节点。
Item是模板ItemType类型的数据。

随后,TQueue将定义头节点与尾节点:

/** Holds a pointer to the head of the list. */
MS_ALIGN(16) TNode* volatile Head GCC_ALIGN(16);

/** Holds a pointer to the tail of the list. */
TNode* Tail;

构造函数

创建一个新的节点,“头游标”和“尾游标”都将指向它:

/** Default constructor. */
TQueue()
{
	Head = Tail = new TNode();
}

析构函数

删除当前的Tail并将继续查看他的“下一个节点”,直至“下一个节点”为空。

/** Destructor. */
~TQueue()
{
	while (Tail != nullptr)
	{
		TNode* Node = Tail;
		Tail = Tail->NextNode;

		delete Node;
	}
}

队列操作:出列

从队尾“拿出”一个元素(指返回这个元素并返回这个元素)
需要注意的是:它只能在“consumer(使用方)”线程调用

/**
* Removes and returns the item from the tail of the queue.
 *
 * @param OutValue Will hold the returned value.
 * @return true if a value was returned, false if the queue was empty.
 * @note To be called only from consumer thread.
 * @see Empty, Enqueue, IsEmpty, Peek, Pop
 */
bool Dequeue(ItemType& OutItem)
{
	TNode* Popped = Tail->NextNode;

	if (Popped == nullptr)
	{
		return false;
	}
	
	TSAN_AFTER(&Tail->NextNode);
	OutItem = MoveTemp(Popped->Item);

	TNode* OldTail = Tail;
	Tail = Popped;
	Tail->Item = ItemType();
	delete OldTail;

	return true;
}

值得一提的是注意Popped->ItemMoveTemp实现了移动语意,表示这个元素已经变为了“右值”,随后也可以看到Popped->Item = ItemType()即被赋为了新的值。

队列操作:Pop

移除末端元素,直至“尾游标”的“下一个节点”是空:
需要注意的是:它只能在“consumer(使用方)”线程调用

/**
* Removes the item from the tail of the queue.
 *
 * @return true if a value was removed, false if the queue was empty.
 * @note To be called only from consumer thread.
 * @see Dequeue, Empty, Enqueue, IsEmpty, Peek
 */
bool Pop()
{
	TNode* Popped = Tail->NextNode;

	if (Popped == nullptr)
	{
		return false;
	}
	
	TSAN_AFTER(&Tail->NextNode);

	TNode* OldTail = Tail;
	Tail = Popped;
	Tail->Item = ItemType();
	delete OldTail;

	return true;
}

队列操作:清空

清空所有的元素。
需要注意的是:它只能在“consumer(使用方)”线程调用

/**
 * Empty the queue, discarding all items.
 *
 * @note To be called only from consumer thread.
 * @see Dequeue, IsEmpty, Peek, Pop
 */
void Empty()
{
	while (Pop());
}

队列操作:入列

在头部增加一个新的元素。
需要注意的是:它只能在“producer(生产方)”线程调用

/**
* Adds an item to the head of the queue.
 *
 * @param Item The item to add.
 * @return true if the item was added, false otherwise.
 * @note To be called only from producer thread(s).
 * @see Dequeue, Pop
 */
bool Enqueue(const ItemType& Item)
{
	TNode* NewNode = new TNode(Item);

	if (NewNode == nullptr)
	{
		return false;
	}

	TNode* OldHead;

	if (Mode == EQueueMode::Mpsc)
	{
        OldHead = (TNode*)FPlatformAtomics::InterlockedExchangePtr((void**)&Head, NewNode);
		TSAN_BEFORE(&OldHead->NextNode);
		FPlatformAtomics::InterlockedExchangePtr((void**)&OldHead->NextNode, NewNode);
	}
	else
	{
		OldHead = Head;
		Head = NewNode;
		TSAN_BEFORE(&OldHead->NextNode);
		FPlatformMisc::MemoryBarrier();
        OldHead->NextNode = NewNode;
	}

	return true;
}

可以看到它对MpscSpsc做了区别的对待:Spsc中的逻辑看起来更直观,不过Mpsc的逻辑是一样的,只不过使用了FPlatformAtomics::InterlockedExchangePtr保证了原子操作。

另外,“入列”操作还有另一个使用“右值引用”的版本,逻辑上相似:

bool Enqueue(ItemType&& Item)
{
	TNode* NewNode = new TNode(MoveTemp(Item));
	...

队列操作:检查是否是为空

/**
 * Checks whether the queue is empty.
 *
 * @return true if the queue is empty, false otherwise.
 * @note To be called only from consumer thread.
 * @see Dequeue, Empty, Peek, Pop
 */
bool IsEmpty() const
{
	return (Tail->NextNode == nullptr);
}

队列操作:Peek

看一下队尾操作,但并不移除它。有两种选择:

1)将结果返回到OutItem

/**
 * Peeks at the queue's tail item without removing it.
 *
 * @param OutItem Will hold the peeked at item.
 * @return true if an item was returned, false if the queue was empty.
 * @note To be called only from consumer thread.
 * @see Dequeue, Empty, IsEmpty, Pop
 */
bool Peek(ItemType& OutItem) const
{
	if (Tail->NextNode == nullptr)
	{
		return false;
	}

	OutItem = Tail->NextNode->Item;

	return true;
}

2)返回指针:

/**
* Peek at the queue's tail item without removing it.
 *
 * This version of Peek allows peeking at a queue of items that do not allow
 * copying, such as TUniquePtr.
 *
 * @return Pointer to the item, or nullptr if queue is empty
 */
ItemType* Peek()
{
	if (Tail->NextNode == nullptr)
	{
		return nullptr;
	}

	return &Tail->NextNode->Item;
}

FORCEINLINE const ItemType* Peek() const
{
	return const_cast<TQueue*>(this)->Peek();
}

禁用“拷贝构造函数”与“赋值操作符”

/** Hidden copy constructor. */
TQueue(const TQueue&) = delete;

/** Hidden assignment operator. */
TQueue& operator=(const TQueue&) = delete;

隐含的条件

由于Dequeue等操作中使用了ItemType(),所以一个隐含的条件是模板类ItemType的默认构造函数必须是可以访问的。否则编译将不通过。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
UE4中的TQueue是一个基于模板的实现的队列数据结构。它被定义在\Engine\Source\Runtime\Core\Public\Containers\Queue.h中。这个队列使用一种无锁的链表来存储入队的元素的副本。TQueue可以以两种模式操作:多生产者单消费者(MPSC)和单生产者单消费者(SPSC)。在两种模式下,队列都是线程安全的。通过使用特定的方式编写Dequeue()方法,它确保了线程安全,不依赖于CPU上可能的指令重排序。而Enqueue()方法在多生产者场景中使用原子比较和交换来实现。 UE4的TQueue是一个通用的队列数据结构,可以用于存储任何类型的元素,并且支持多线程环境下的安全操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [【UE4源代码观察学习队列模板TQueue](https://blog.csdn.net/u013412391/article/details/107475132)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [tqueue-开源](https://download.csdn.net/download/weixin_42118160/20177106)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值