队列

“队列”是一个基础的数据结构,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的默认构造函数必须是可以访问的。否则编译将不通过。

【UE4源代码观察】学习队列模板TQueue相关推荐

  1. 【UE4源代码观察】尝试调试UBT

    前言 在之前的博客<[UE4源代码观察]手动建立一个使用UBT进行编译的空白工程>中我尝试动手搭建了一个用UBT进行编译的空白的工程.但是对UBT其中的逻辑并不理解. 后来在学习UE4源代 ...

  2. 【UE4源代码观察】观察 RHI、D3D11RHI、RenderCore 这三个模块的依赖关系

    基本概念 RHI(Render Hardware Interface)的职责是对OpenGL.DirectX3D.Vulkan这些图形接口API进行封装,来统一上层的调用.D3D11RHI就是 RHI ...

  3. 【UE4源代码观察】手动建立一个使用UBT进行编译的空白工程

    我想观察UE4是怎么编译的,于是查阅官方文档,了解到UE4有一套自己的编译工具:UnrealBuildTool,简称UBT.关于UBT的官方文档参阅:虚幻编译工具.我想尝试自己手动建立一个使用UBT进 ...

  4. 【UE4源代码观察】观察D3D11是如何被RHI封装的

    我想了解D3D11是如何被UE4的RHI封装的.我知道这牵扯到很多复杂的内容,但通过观察一些最基础的API被谁在哪调用,可以对RHI是如何封装的有一个大致了解.在之前的博客<创建一个最小的D3D ...

  5. UE4 Material 101学习笔记——30-37 植物叶片(透光/mask/面片隐藏/法线调整/AO/渐隐/世界空间色彩/随风舞动)

    UE4 Material 101学习笔记--30-37 植物叶片(透光/mask/面片隐藏/法线调整/AO/渐隐/世界空间色彩/随风舞动) Lec30 叶子透光 Foliage Translucenc ...

  6. UE4 Material 101学习笔记——08-12 凹凸和视差贴图/纹理压缩/布料/体积冰/摇曳树叶

    UE4 Material 101学习笔记--08-12 凹凸和视差贴图/纹理压缩/布料/体积冰/摇曳树叶 Lec08 凹凸和视差贴图 Bump Offset and Parallax Occlusio ...

  7. UE4 Material 101学习笔记——23-29 水涟漪/水深/折射反射/Gerstner海浪/波光焦散/泡沫/FlowMap

    UE4 Material 101学习笔记--23-29 水涟漪/水深/折射反射/Gerstner海浪/波光焦散/泡沫/FlowMap Lec23 水的表面涟漪 Water Ripples Shader ...

  8. 图论01.最短路专题_学习笔记+模板

    图论01.最短路专题_学习笔记+模板 一.定义与性质 ● 需要的前导知识点 路径 最短路 有向图中的最短路.无向图中的最短路 单源最短路.每对结点之间的最短路 ● 最短路的性质 对于边权为正的图,任意 ...

  9. UE4场景设计学习教程

    视频:MPEG4视频(H264) 1920×1080 25fps 1400kbps |音频:AAC 44100Hz立体声128kbps 语言:西班牙语+中英文字幕(根据原英文字幕机译更准确) |时长: ...

最新文章

  1. Java中一个令人惊讶的bug
  2. 成为数据科学家、人工智能和机器学习工程师的自学之路
  3. 算法分析结课总结--回溯算法
  4. labelimg选中高亮
  5. SubSonic中RecordBaseT.Load(IDataReader dataReader)与LoadAndCloseReader(IDataReader dataReader)的使用区别...
  6. 同域下iframe操作时,js访问document出现拒绝访问的问题原因
  7. selenium.common.exceptions.WebDriverException: Message: ‘chromedriver’解决
  8. Scrapy_LinkExtractor
  9. 文本相似性度量---------字符串近似相等
  10. CentOS基本的命令与快捷建
  11. C# 多线程BackgroundWorker
  12. SSIS变量如何获取当前的系统时间(字符串格式年月日)
  13. 【腾讯云】企业认证题库200题
  14. 思岚激光雷达A2 Ros配置
  15. 蓝牙技术|伦茨科技智能语音遥控器方案简介
  16. 集合 -- 如何安全删除 HashMap 中的元素
  17. 量化选股之经典的因子选股
  18. 日更100天(42)每天进步一点点
  19. 一年白干!程序员赵某仿制老东家 APP,获取服务器数据,被判 4 年 6 个月
  20. SQLSTATE[42S02]: Base table or view not found: 1146 Table 'blog.user' doesn't exist (SQL: select * f

热门文章

  1. 树莓派修改多网卡的连接
  2. 装系统提示:Error1962:No operating system found.解决办法在此
  3. 攻防基础-木马病毒介绍
  4. jQuery实现拍打灰太狼小游戏
  5. 联邦滤波matlab程序,联邦滤波器仿真
  6. 边缘计算网关有哪些优势特色?边缘计算网关和智能网关的区别?
  7. 小程序统一服务消息_小程序客服消息接入微信教程
  8. hal库开启中断关中断_(2)STM32使用HAL库操作外部中断——理论讲解
  9. Keil5下载程序报错问题总结
  10. 红外循迹TCRT5000 舵机SG90