当前位置: 首页 > news >正文

网站介绍词如何套用别人网站模板

网站介绍词,如何套用别人网站模板,com域名购买,如何查看网站抓取频率仓库:https://gitee.com/mrxiao_com/2d_game_3 黑板#xff1a;线程同步/通信 目标是从零开始编写一个完整的游戏。我们不使用引擎#xff0c;也不依赖任何库#xff0c;完全自己编写游戏所需的所有代码。我们做这个节目不仅是为了教育目的#xff0c;同时也是因为编程本…仓库:https://gitee.com/mrxiao_com/2d_game_3 黑板线程同步/通信 目标是从零开始编写一个完整的游戏。我们不使用引擎也不依赖任何库完全自己编写游戏所需的所有代码。我们做这个节目不仅是为了教育目的同时也是因为编程本身就是一种乐趣。对于喜欢编程的人来说深入了解底层工作原理、亲自动手实现功能是一件非常有成就感且有趣的事。 目前我们正在进行多线程的工作。昨天我们刚刚开始着手这部分内容。之所以做多线程是因为现代处理器在时钟频率即主频上已经无法再继续提升。大约在4 GHz左右处理器遇到了热力学的瓶颈不能再通过提高时钟频率来加速计算。因此现代处理器的性能提升主要依赖于其他技术例如宽寄存器比如我们之前讨论过的SIMD指令集和多核处理后者能够让多个执行流并行工作从而提高应用程序的性能。 我们现在正着手展示如何在渲染器中实现多线程。昨天我们简要介绍了什么是多线程如果错过了的话请回去看一下昨天的内容其中涵盖了本周我们将会提到的所有基本概念。昨天的内容也介绍了如何在Win32环境下创建线程。接下来我们需要做的就是讨论如何实际利用这些线程来做有用的工作。为了做到这一点我们需要讨论线程之间如何通信、如何知道自己该做什么工作等问题。 因此今天我们将进行另一个黑板讲解可能不会像之前那么长讲解线程之间如何协作并解决一些基本的同步问题。之后我们会回到Win32的代码中展示如何在Win32下实现这些内容。这样一来到明天为止我们就能够让线程实际完成一些有用的工作了。至于今天能否做到这一点我们还不确定因为黑板讲解有时需要花费较多时间来彻底解释一些概念往往会消耗不少时间。 让我们开始讨论线程同步Thread Synchronization以及线程间的通信。昨天我已经展示了在Win32中创建线程有多么简单。实际上我们可以创建任意数量的线程这一点没有问题并且我们也展示了如何在Win32下创建与系统中处理器数量相等的线程。稍后我们会展示如何查询系统了解应该创建多少个线程等信息。 目前的问题是虽然我们能轻松创建线程但这些线程究竟如何开始执行实际的工作还不清楚。所以接下来我们需要思考几个关键问题。首先我们需要讨论线程如何获取工作并执行任务。 黑板进程 昨天提到的一个重要点是线程和进程并不是相同的概念。回顾一下我们讨论过进程的概念进程有各自独立的内存空间。因此如果两个进程要共享工作或者互相传递数据它们需要做很多特别的事情来实现这一点。比如它们必须在操作系统中建立某种管道或者显式地请求操作系统允许它们共享特定的内存区域。为了让进程能够看到彼此的工作成果或者获取彼此产生的数据必须采取这些额外的步骤和操作。 然而线程与进程不同线程是共享相同内存空间的。线程之间可以直接共享数据无需像进程那样进行额外的通信或设置共享内存。这就是线程和进程的一个根本区别。 黑板线程 在一个进程内所有线程都运行在同一个内存空间中。这意味着线程之间可以共享和操作相同的数据。比如在渲染器的例子中我们有一个推送缓冲区push buffer它是我们需要操作的地方还有一个帧缓冲区frame buffer以及一些位图等数据。所有这些数据已经预先布局并且主线程可以访问它们。 当我们创建新线程时这些线程也能够直接访问这些数据。就像昨天展示的那样创建线程时可以共享全局变量也可以通过Win32的线程启动函数传递指针。因此我们可以很容易地让线程访问需要的数据。只要将数据位置传递给线程它就能获得访问权限。 因为线程总是可以访问同一个进程中的所有内存空间所以无需额外的工作来确保每个线程都能看到它们需要的数据。这与进程不同进程之间需要通过特殊的方式共享数据而线程之间则天然具备对同一内存空间的访问能力因此这个问题不存在。 黑板问题 #1 - 知道该做什么工作 当引入多线程时首先会面临两个主要问题。第一个问题是如何确定每个线程应该做什么工作。因为一旦有了多个线程问题变得复杂了。如何让线程知道自己应该执行什么任务呢 在之前写的单线程代码中假设所有代码都是按顺序执行的且只有一个核心负责执行。这种情况下编程的思维非常简单我们只是逐行编写代码假设代码是按顺序执行的并且是由一个执行流来完成的。但是一旦我们引入了多个线程这种顺序的假设就不成立了。代码中的每一部分都可能被任何一个线程在任何时候执行。 因此必须重新思考如何管理线程的工作。我们需要一种方式让每个线程知道自己应该执行哪些任务、处理哪些数据。具体来说我们需要设计一种方法使得线程能够知道在何时执行哪些例程routines以及它们需要在哪些数据上工作。 在过去的编程过程中这一切都是隐含的因为代码从一个地方开始依次调用函数任务就按顺序完成了。然而当引入多线程后情况变得更加复杂。我们不再是只有一个主线程按顺序执行而是有多个线程并行执行。为了使代码能够正常运行必须设计一个新的模型来确保每个线程知道自己应该做什么什么时候做。 黑板问题 #2 - 同步/工作何时完成/可见 在多线程编程中除了需要解决如何分配工作给不同线程的问题之外还需要解决线程同步和工作完成的可见性问题。具体来说第二个问题是确保线程之间的工作能够在正确的时间进行同步以及如何知道工作是否完成或可见。这个问题在单线程编程中是不存在的因为在单线程中所有操作都按顺序执行不需要考虑线程间的同步问题。 首先在单线程代码中任务是顺序执行的。例如函数A执行完之后函数B才会执行这种顺序关系是显式的也就是通过代码的顺序来保证的。如果函数B依赖于函数A的结果我们只需要确保在调用函数B之前调用函数A即可。但一旦引入多线程问题变得更加复杂因为多个线程可能会并发执行而线程间的依赖关系不再仅仅依赖于代码的顺序。 假设有多个不同的方式来调用函数A然后它们的结果需要传入函数B处理。在这种情况下如果函数A的每个调用非常耗时比如每次调用需要一毫秒我们可能希望这些调用能够并行执行。于是我们可以将每个函数调用分配给不同的线程去执行。比如线程0执行第一次调用线程1执行第二次线程2执行第三次而函数B在所有线程完成它们的工作后执行。 问题就出现在这里函数B不能在所有线程的工作都完成之前开始执行。如果有线程依赖其他线程的结果那么我们就需要一种机制来确保只有在所有依赖的线程完成任务后目标线程才能继续执行。这就是多线程编程中的一个关键问题线程必须知道其他线程的工作是否已经完成以便它们能正确地执行接下来的任务。 解决这些问题对于实现高效的多线程程序至关重要。虽然在渲染器这样的简单场景中可以通过将任务划分为“桶”来处理这些问题但随着问题变得更复杂我们仍然需要在多线程编程中仔细设计线程之间的协调和同步机制。 黑板概念化线程如何工作 在多线程环境下问题的解决方法变得更加复杂主要是因为多处理器系统不再保证一些基本的假设像是内存操作的顺序性等。要理解和解决这些问题需要从线程如何协同工作以及它们如何看到彼此结果的基本模型来思考。 举个简单的例子假设有几个函数A如“渲染一个瓦片”然后有一个函数B如“显示到屏幕”。我们需要设计一种方式使得多个线程可以并行执行分别渲染不同的瓦片最后有一个线程等待所有其他线程完成渲染后再将所有结果合并最终进行显示。这个模型很像渲染器中多线程处理渲染任务的方式。 具体来说每个线程负责渲染一个瓦片最后一个线程负责将这些渲染结果汇总并显示到屏幕上。在这种情况下需要确保每个渲染线程完成后最后的汇总线程才能开始工作。这个过程中涉及到线程之间的同步确保最终的汇总操作在所有渲染工作完成之后执行。 黑板进行忙等待循环 在多线程编程中我们面临的第一个问题是如何安排线程去做任务。假设我们启动了多个线程每个线程都在忙碌等待即所谓的忙等待它们会一直在一个无限循环中检查是否有工作需要做。如果有工作它们就执行否则就继续检查。 此时线程的任务调度问题变得非常关键。如果我们将任务的状态存储在一个指针里当有任务需要处理时指针会指向任务的数据否则指针为空表示没有任务。线程在执行时会检查这个指针发现指向任务时就会去处理这个任务。 这个过程对于单个线程来说并不复杂线程只需要读取指针查看是否有任务需要处理。但问题出在当有多个线程同时运行时多个线程可能会同时看到指针指向的任务从而导致多个线程同时处理同一个任务。这样就造成了资源的浪费导致不必要的重复计算。 为了避免这个问题必须确保每个线程处理的任务是唯一的也就是每个线程只能处理特定的工作而不与其他线程重复。这就需要一种机制来管理线程之间的任务分配确保每个线程都能独立执行不同的任务避免竞争条件和数据冲突。 这个问题的复杂性在于线程是并行执行的而多个线程的操作可能会相互干扰因此需要特殊的同步机制来协调它们的工作。这是多线程编程中的一个典型挑战。 黑板防止多个线程做相同的工作 为了避免多个线程重复执行相同的任务一种直觉的解决方法是当一个线程看到指针指向工作任务时它马上将该指针设置为零这样其他线程就看不到该任务避免重复执行。也就是说在处理任务之前线程会把任务指针保存并清空确保其他线程不能再看到这个任务。 然而这种方法实际上并不可行原因在于多线程编程引入了一些细节单线程程序员通常不需要考虑。这些问题源自于多核处理器同时执行多个线程时线程之间的执行顺序是不可预测的。 例如假设有两个线程同时在检查指针如果它们都看到指针指向一个任务它们都会进入“任务存在”的判断并试图将任务指针设置为零。这时它们可能会同时读取指针并保存该值然后各自将指针设置为零并开始执行任务。问题在于虽然指针被正确设置为零但两个线程仍然会执行相同的任务造成重复的工作。 这种情况发生的原因是两个线程是并行执行的它们可能在几乎相同的时间内检查指针、保存任务指针并且互相之间并没有同步机制来确保它们不会同时处理同一任务。这种竞态条件是多线程编程中的一个常见问题单纯依靠传统的编程方法并不能保证解决。 因此仅凭当前的编程语言基础尤其是C语言无法保证完全避免这种问题除非使用更高级的同步机制或者转向像C11这种提供了更强大线程控制和同步工具的语言。在C语言中处理这种并发问题需要引入更多的同步原语如锁、原子操作等来确保线程之间的互斥和正确的任务分配。 黑板x64 提供了特殊指令 尽管多线程代码存在许多挑战实际上它是能够工作的许多人都在实际项目中使用并发布了多线程代码。为什么这能做到呢原因在于现代x64处理器提供了一些专门的指令帮助解决并发执行中的问题。这些指令专门设计用来支持多线程编程确保代码在多个线程并行执行时能够正确运行。 这些特殊指令的工作原理通常是在某些操作中提供一种保证确保只有一个线程能够看到某个操作的结果避免多个线程同时看到并操作同一个结果。通过这些指令可以保证线程在执行时不会出现竞态条件从而确保多线程代码能够正确且高效地执行。 例如x64处理器提供的原子操作指令可以让一个线程在执行操作时锁定内存中的某个值确保其他线程在该线程完成操作之前无法访问这个值。这种机制通过确保操作的互斥性避免了多个线程同时进行冲突的操作确保每个线程看到的都是正确和独立的数据。 黑板“锁交换”locked exchange 在多线程编程中x64架构提供了一些特殊的指令用来确保多线程操作的正确性避免多个线程同时执行相同的操作。其中一个关键的指令是“锁交换”Locked Exchange。该指令的作用是替换内存中某个位置的值并确保没有其他线程能够同时执行这个操作。这样可以确保线程操作的独立性和正确性。 具体来说如果我们希望替换内存中某个位置比如指针指向的位置的值为零且只允许当前线程执行这个操作可以使用锁交换指令来实现。通过锁交换操作会被原子化执行即在进行替换之前其他线程不能访问该位置的数据。这样确保了只有一个线程能够成功地获取并替换该值避免了并发冲突。 例如我们可以将工作指针指向的值与零交换。如果成功执行返回值就是替换前的指针值。如果工作指针指向一个有效值就会继续进行工作。如果有多个线程同时尝试执行相同的操作锁交换会确保只有一个线程能够成功地替换值其他线程会看到已经被替换的零值并跳过操作。 除了锁交换还有其他类似的同步原语如互锁递增这些指令可以保证每个线程看到的值都是唯一的。例如互锁递增指令可以确保每次递增操作的结果都不会被其他线程干扰从而可以生成一个递增的整数序列。 总之多线程编程的关键之一是通过使用这些特殊的同步原语来确保操作的原子性和线程间的正确同步。这样可以避免竞态条件确保多线程代码在多个CPU核心上并行执行时能够按照预期的顺序正确执行。 黑板“互锁比较交换”interlocked compare exchange 在多线程编程中interlocked compare exchange 是一个非常强大的指令它的功能类似于前面提到的“锁交换”locked exchange但它具有更高的灵活性。与“锁交换”指令不同interlocked compare exchange 不仅能够进行原子交换操作还能基于条件执行交换操作确保只有在某个特定条件下值才会被替换。 具体来说interlocked compare exchange 允许你指定一个内存位置如果该位置的当前值与预期值相等则用新的值替换它。这意味着如果两个线程同时尝试操作相同的内存位置只有一个线程会成功执行交换另一个线程会看到这个内存位置的值已经发生了变化从而避免了竞态条件。 这个指令的优势在于它不仅确保操作是原子的还允许在交换之前检查条件使得编程更加灵活。例如如果某个线程在执行任务时需要检查某个值是否满足某个条件才能决定是否继续执行任务那么interlocked compare exchange 就是一个非常有用的工具。它可以保证只有当内存中的值与预期值相符时才会进行替换操作从而避免了不同线程之间的冲突。 虽然这个指令在性能上可能比一些简单的原子操作稍微慢一些但在大多数情况下这种性能差异并不显著。而且它非常适用于需要条件判断的场景使得线程同步变得更加简洁和易于管理。通过 interlocked compare exchange我们能够在编程中采用一种更加灵活的方式来解决复杂的同步问题而不需要将每一个操作都强行压缩成一个单一的交换操作。 总的来说interlocked compare exchange 提供了一种强大且灵活的方式来进行线程同步它不仅能保证操作的原子性还能允许程序在多线程环境中进行更复杂的条件判断和操作。对于那些需要在执行前进行一些检查的多线程任务来说这个指令是一个非常有用的工具。 interlocked compare exchange 使得多线程程序可以实现更灵活和高效的同步特别是在处理工作分配和避免竞态条件时。它允许线程在进行内存交换操作时设置条件只有在内存中存储的值与预期的值相匹配时交换才会发生。如果当前的值与预期不符交换就不会进行这为线程提供了一个简单的检查机制。 具体来说当一个线程试图交换内存位置的值时interlocked compare exchange 会检查该位置的当前值是否与预期值相同。如果相同才会将该位置的值替换为新的值。如果不相同交换操作不会执行意味着这个线程无法修改内存中的值。 这种机制的关键在于它允许线程在进行工作前先“抢占”检查某个位置的值是否已经被其他线程修改过。比如线程可以在进行工作之前先检查工作是否仍然有效如果其他线程已经替换了工作内容这个线程就不再进行操作而是跳过这部分工作重新开始查找新的任务。这样就能有效地避免不同线程间的冲突和重复工作。 通过这种方式interlocked compare exchange 使得线程之间能够更智能地协调工作避免无意义的重复操作同时提高程序的效率。对于处理需要条件判断的任务来说它提供了一种简单而强大的同步机制。这种机制可以帮助程序在多线程环境中保持良好的执行顺序确保只有一个线程能够成功修改内存位置而其他线程则会感知到这个变更从而避免竞争条件。 黑板用其他原语巧妙处理 interlocked compare exchange 使得编写多线程代码时可以在不进行复杂同步操作的情况下依然保证线程安全。即使代码结构并不完美只要合理使用这个原语线程安全性仍然能够得到保证。这种机制允许开发者在多线程环境中做出一些不完美的设计但依然能够避免竞态条件和同步错误。 然而如果使用其他的同步原语事情就没那么简单了。使用这些其他的同步原语时必须非常小心和谨慎地设计代码因为任何没有严格安排的操作都可能导致同步问题甚至可能导致崩溃。例如在某些情况下代码中的工作可能依赖于其他线程的执行顺序如果这些操作没有在适当的时机和方式下进行就可能发生竞态条件导致程序无法按预期工作。 interlocked compare exchange 通过简单的内存比较和交换机制能够避免这些复杂的同步问题。它的优势在于如果两个线程并发地执行它能确保只有一个线程会成功交换内存位置而其他线程则会因不满足条件而跳过这次操作。这样线程间的冲突就得以避免程序能够更稳定地运行。 接下来计划是利用这些同步机制来构建一个系统使得多个线程能够安全地执行任务。 黑板在多线程上下文中的“读和写” 在多线程环境中一旦开始考虑多线程处理必须时刻意识到每个线程在执行过程中可能处于的任意状态。每个线程的执行顺序和时机都是不可预测的因此要特别关注多个线程可能同时访问同一块内存时所带来的问题。 读取操作通常没有问题。例如如果多个线程要读取一个不变的数据集例如常量就不需要担心同步问题因为多个线程可以安全地同时读取相同的数据。然而写操作则非常复杂。一旦多个线程可能同时写入同一内存位置就需要解决同步问题。这是因为多个线程可能会在同一位置写入不同的数据从而导致不可预料的结果。更严重的是如果两个线程的写入值不同这时候就需要确定到底哪个线程的写入结果才是最终正确的。 如果两个线程写入的是相同的值那么这并不算问题因为结果不会发生变化但问题的复杂性在于当线程写入的内容不同或者在写入过程中发生冲突时必须确保正确的同步机制以避免数据的不一致性或丢失。 当多个线程可能会写入同一个位置时即使它们不读取该位置的数据情况也会变得更加复杂。这样会导致线程间的竞争可能导致一个线程的写入被另一个线程覆盖或者出现数据同步的问题这些都需要特别关注和处理。 黑板缓存行 在多线程环境中处理器通过缓存行cache line来管理内存操作。缓存行的大小通常较大比如128字节、256字节等这取决于具体的处理器架构。处理器中的每个核心会将内存按缓存行划分并在缓存中操作这些内存块。当多个线程访问不同内存位置时它们可能会访问相同的缓存行导致潜在的同步问题尤其是在写入时。 假设有两个线程分别操作结构体中的不同变量比如一个线程读写变量A另一个线程读写变量B。表面上看似乎不需要同步机制因为它们操作的是不同的变量。但实际上处理器可能将这两个变量A和B放置在同一缓存行中。这时尽管线程操作的是不同的变量但它们实际上是在访问同一个缓存行这可能导致数据冲突和错误的结果。 例如当线程1操作A时它将整个缓存行加载到其缓存中同时包含A和B。然后线程2操作B时也会将相同的缓存行加载到它的缓存中。如果线程1先更新A线程2再更新B最后两者分别将修改后的缓存行写回内存时可能会出现问题。具体来说缓存行的写回顺序可能导致其中一个线程的修改被覆盖从而出现数据丢失或错误的情况。 尽管现代的x64架构通常保证写操作是按照一定顺序可见的避免了这种错误发生但不同的处理器架构之间可能有所不同。因此即使在支持强内存顺序的处理器上也需要考虑缓存行争用的问题因为这可能会导致性能下降。如果多个核心频繁争用同一缓存行处理器需要不断地交换缓存数据这会增加同步成本并影响整体性能。 为了避免这种情况可以尽量确保线程操作的内存位置不在同一缓存行内确保每个线程的工作负载分布在不同的缓存行中。这样可以避免不必要的同步开销并提高性能。即使在强内存一致性的架构上也应考虑缓存行的布局避免频繁的缓存行争用。 总之了解缓存行和处理器如何管理内存访问是优化多线程程序性能的关键之一。 我们现在准备从基础开始操作线程展示如何用线程开始执行一些简单的工作。虽然目前还很基础但这将帮助理解如何实际使用线程进行任务处理。接下来会打开之前创建的线程处理函数并进行进一步演示。虽然此时的工作还很简单但目的是让大家能够看到线程的工作机制并能开始对线程的使用有所掌握。 win32_game.cpp创建四个做不同工作的线程 接下来我们打算实现一个简单的例子创建15个线程每个线程执行不同的工作。为了实现这一目标我们首先创建一个结构体暂时命名为 ThreadInfo用于存储每个线程的逻辑索引。这个逻辑索引用来表示每个线程的编号例如线程0、线程1等以便我们在后续操作中能够区分不同的线程。 我们会通过 win32_thread_info 来传递这些信息并将其作为 lpParameter 传递给线程的启动函数这样线程就可以知道自己的编号以及其他可能的额外信息。 在实现时我们会用一个循环来创建多个线程。在循环中每次创建一个线程并设置其逻辑索引将结构体的地址传递给线程启动函数。线程开始后暂时让它们休眠等待进一步操作。此时线程创建成功后它们会按照顺序执行但只是暂时没有实际操作只是保证线程被成功创建。 让线程寻找待办的工作 接下来我们希望让线程能够实际执行工作具体来说就是让每个线程从一个工作队列中获取任务并执行。为了简单起见我们将使用一个数组来作为工作队列这个队列充当了一个迷你队列Mini Queue的角色。我们在工作队列的每一项中包含一个字符指针指向要打印的字符串未来也可以根据需要扩展存储其他类型的工作内容。 首先我们会定义一个工作队列项结构包含字符串指针并为这个队列预留一定数量的空间比如256个项。此外我们还会设置一个变量来跟踪队列中当前的任务数量EntryCount并将其初始化为0。 接下来创建线程后每个线程将会遍历队列执行其中的工作。首先我们向队列中推送一些任务比如10个字符串任务。为了实现这个推送操作我们将实现一个 PushString 函数它将一个字符串添加到工作队列中。 这个 PushString 函数非常简单首先检查队列是否已满即当前项数是否小于队列最大容量然后将传入的字符串存入队列的下一个可用位置。 但是问题来了我们需要从队列中获取任务并处理。我们在队列中记录了当前的任务数量但没有记录下一个要处理的任务项。所以我们新增一个变量 NextEntryToDo用来表示下一个要处理的任务项。 每个线程在工作时会检查 NextEntryToDo 是否小于 EntryCount即队列中是否还有任务。如果有任务它就从队列中获取任务打印出相应的字符串并输出一个格式化的调试信息其中包含线程的编号和打印的字符串内容。 此时代码是没有线程同步的保护的因此这是一个不安全的版本多个线程同时访问队列可能会导致竞态条件和数据不一致的问题。不过这个版本展示了工作队列的基本结构和线程如何处理任务。接下来会在这个基础上加入线程同步机制以确保线程安全和正确的任务执行。 运行并检查调试输出 在运行程序时输出的结果出现了一些异常这与预期结果相差甚远。具体来说线程14打印了许多奇怪的内容这些内容看起来不应该是正确的输出。原本的预期是每个线程应该打印出被推入队列的字符串但结果却非常奇怪显然出现了问题。 最初认为这是由于线程同步问题导致的竞态条件但通过进一步分析代码发现问题的根本原因可能在于我们没有正确处理线程的任务分配导致多个线程同时操作了共享资源如工作队列造成了数据的混乱。这些奇怪的打印结果本应是由不同线程各自独立地处理队列中的任务但由于缺乏同步措施导致了错误的输出。 这表明在没有任何同步机制的情况下运行代码是非常不安全的尤其是在多线程环境中多个线程可能同时访问共享的数据结构导致数据丢失或错误。为了确保代码的正确性需要添加适当的同步机制以防止这种情况发生。 认识到的时刻你需要让 ThreadInfo 的值保持 在调试过程中发现了一个问题原本设计用于保存线程信息的结构体在每次循环中都被覆盖导致线程无法正确引用自己的信息。具体来说线程信息ThreadInfo是存储在栈上的临时数据这意味着每个线程的信息在创建之后会立即消失因为栈上数据会被复写。因此多个线程在执行时会引用错误的数据。 为了解决这个问题需要确保每个线程的线程信息结构体在整个生命周期内都能持续存在。因此应该将线程信息放到堆上而不是栈上这样线程在执行时可以安全地引用这些信息。 一旦修复了这个问题程序应该能够正确地将每个线程的信息持续存在并且每个线程能够正确地引用并处理其对应的任务而不会发生信息丢失或错误引用的情况。 检查输出 在测试过程中输出结果如预期般出现了问题显示不同线程打印出不同的字符串虽然部分结果有些合理但其他部分则完全不符合预期。例如线程 1 打印了 3 个字符串线程 0 打印了 2 个字符串虽然这些行为看似不完全错误但这表明线程没有正确地同步导致了不一致的输出。 这表明尽管没有明显的代码错误但因为线程没有适当的同步机制线程之间的工作调度和访问顺序出现了问题。这种情况可能是由于多个线程在访问共享数据时没有按照预期的顺序进行处理从而导致了结果的错乱。为了避免这种情况必须对线程进行同步控制确保每个线程按照正确的顺序从队列中取出任务并执行。 解释发生了什么 在这段代码中存在两个主要问题需要解决。第一个问题是关于NextEntryToDo这一行代码的线程安全性。由于没有进行适当的同步机制处理两个线程可能会看到相同的值从而引发竞争条件。具体来说多个线程可能同时读取到相同的NextEntryToDo值然后同时进行自增操作导致重复访问同一个工作项。 第二个问题是在编译器优化方面。如果编译器未意识到多个线程可能会修改NextEntryToDo这个变量它可能会进行不当优化。例如编译器可能会认为只有当前线程会修改这个值因此它可能会在优化过程中提取或重排代码从而影响程序的正确性。这种情况下编译器可能会将变量缓存到寄存器中忽略其他线程的更新导致不一致的行为。 为了修复这些问题首先需要确保线程安全即对NextEntryToDo操作使用适当的同步机制避免并发冲突。其次需要使用合适的C语言关键字告知编译器该变量在多线程环境下可能会被多个线程修改从而避免编译器对变量进行不当的优化。 对 EntryCount 的写入没有按顺序进行 另一个问题出现在这里输入字符串。也就是说在执行 entryCount 后存在另一个问题写操作的顺序并不正确。可以看到在 entryCount 增加后某个线程可能会读取到这个值并开始执行工作加载数据。如果这个线程在此时开始工作那么必须确保 StringToPrint 已经填充完成否则它将读取到垃圾数据。 另外一个需要做的事情是确保写入操作按顺序进行确保当一个线程看到 entryCount 增加时相关的工作数据已经正确地刷新到内存中。要确保线程在读取时能够看到正确的内容并且这个过程要严格按照内存模型来处理。 这个简单的代码片段中实际上存在三个独立的问题需要修正这显示了在进行多线程编程时必须小心处理的多个细节。 读取顺序也不对 在讨论多线程编程时不仅写操作存在问题读取操作也会出现类似的问题尤其是在编译器的优化过程中。具体来说尽管某些架构如 x64 架构保证了内存访问的强一致性但在编译器层面编译器可以对代码进行优化例如将读取操作提前到不合适的地方这可能会导致竞争条件读取到未更新的值。 为了防止这种问题的发生需要确保编译器不会进行不当的优化操作。为此可能需要使用一些机制来显式地阻止编译器将读取操作和写入操作的顺序打乱。这些问题在多线程编程中是非常常见的尤其是在涉及到不同线程间共享数据的情况下。 因此通常建议在实现多线程功能时先编写一些简单而有效的原语例如工作队列并集中处理这些多线程同步问题。这样可以避免在代码的各个地方重复处理同步问题减少出错的机会。通过集中处理这些问题可以确保代码的一致性并减少因频繁处理多线程问题而带来的头痛和bug。 总体而言解决这些多线程同步问题一次并在代码中进行集中管理要比在代码中到处纠结多线程问题来得更高效、清晰。 你使用“for (;”而不是“while(1)”是风格上的选择还是有我忽略的好处/坏处 在这段代码中使用分号代替了其他形式的表达式目的是为了避免编译器发出警告。一些编译器在面对特定的条件表达式时会抱怨并提示“条件表达式常量”。这是因为某些编译器认为某些条件表达式不符合预期可能会给出警告。为了避免这种警告通常会禁用该警告或者通过这种方式书写代码来避开编译器的检查。 这主要是为了避免在不同的代码库中工作时特别是在使用一些较为严格的编译选项时引发不必要的警告。因为有时这些警告并不会真正影响代码的运行只会影响编译过程中的输出所以通过使用这种方法可以使代码更干净不会无端增加警告的数量。因此这种做法更多的是为了保持代码的整洁性避免引入多余的警告。 总结来说问题并不在于这段代码的实际效果而是为了避免特定的编译警告并使得在跨不同代码库工作的过程中避免引发潜在的不必要的警告。 如果没有互锁并看到相同的值会有什么问题就像你的“TODO”所提示的那样 在这段代码中出现了一个问题即多个线程同时操作共享资源导致多个线程看到相同的值从而执行了重复的工作。具体来说目标是确保每个工作项只能被一个线程执行一次但是在当前实现中线程0、2和1都执行了相同的工作项这明显是不对的。 最初只有一个线程时代码表现正常打印出从0到9的工作项而且每个工作项都只执行了一次。然后当增加第二个线程时代码依然能够正常工作线程0和线程1按顺序分别处理工作项输出的顺序也合理。尽管如此线程1在输出第6个工作项时稍微滞后可能是由于线程的创建顺序或调度延迟问题但仍然能够保持正确性。 然而当线程数增加到更多时例如4个线程就开始出现问题工作项1被两个线程线程0和线程1都处理了。也就是说虽然目标是让每个工作项只处理一次但由于线程之间没有进行正确的同步导致了多个线程同时处理同一个工作项从而造成了重复的工作。 这个问题的根本原因在于共享变量如 NextEntryToDo的操作没有使用原子操作因此多个线程可能会同时读取相同的值从而造成重复工作。虽然 OutputDebugString 可以保证只有一个线程能同时写入输出但其他操作依然缺乏同步保障。 黑板为什么两个线程做了相同的工作 这个问题的根本原因与CPU如何工作有关特别是在多核处理器的情境下。处理器有寄存器而不是直接在内存中操作数据。当一个核心如核心0需要处理某个工作单元时它首先会检查 NextEntryToDo即下一个要做的工作项的值。假设 NextEntryToDo 的值是1核心0就会把这个值加载到它的寄存器中。这时核心0的寄存器中就存储了值1。 与此同时另一个核心核心1也可能需要执行相同的操作它也会加载 NextEntryToDo 的值这样核心1的寄存器中也会存储值0。需要注意的是处理器通常是先将值加载到寄存器中进行操作而不是直接在内存中进行修改。因此核心0和核心1的寄存器都存储了相同的值0实际上它们各自都复制了一份相同的内存值。 接下来核心0和核心1都执行相同的增量操作它们都将 NextEntryToDo 的值从0更新为1。由于它们并没有同步两个核心都写回了相同的值1到内存中这意味着它们都认为工作单元0应该被处理并且它们都将工作单元1标记为已完成。因此工作单元1实际上被两个核心同时处理了这会导致重复的工作。 如果这种情况发生在渲染任务中意味着两个核心在渲染相同的图像块tile而不是分别渲染不同的图像块从而浪费了计算资源。实际上性能下降了因为两个核心执行了重复的工作。 我在其他地方看到的用于多线程代码的互斥锁是否依赖这些互锁指令还是它们完全不同 在多线程编程中mutex互斥锁通常是操作系统提供的原语它们实际上是基于处理器的互锁指令interlocked instructions来实现的。互锁指令确保多个线程访问共享资源时能够正确同步避免数据竞争和不一致性的问题。互斥锁通过这些底层的互锁机制来实现线程之间的互斥访问从而保证同一时刻只有一个线程可以访问临界区的代码或资源。 此外还有一种称为事务性内存Transactional Memory的方法这种方法在英特尔的处理器中逐步推出。事务性内存的工作原理与传统的mutex有所不同它并不完全依赖于互锁指令。事务性内存采用不同的同步原理来处理线程间的协调问题目的是提供更高效的并发控制但它与传统的互斥锁并不相同。 目前互斥锁通常仍然是通过互锁指令来实现的而事务性内存等新型同步原语则可能在未来成为更主流的选择。 你对无锁队列lockless queues有什么看法 对于无锁使用lock-less这个概念实际上并不完全清楚人们在说它时指的是什么。很多时候不知道具体是指哪一种方式。如果是指完全不使用任何互锁指令那么在某些情况下这可能是有意义的尤其是当需要处理大量队列操作时因为这种方式可能会提升性能。然而如果队列操作并不复杂那么我会质疑这种方式是否值得采用因为对于现代处理器来说互锁指令的开销并不大。 此外锁式增量lock increment队列的实现相对简单且容易正确实现而没有使用锁的实现则可能会复杂得多因此在大多数情况下如果队列的复杂度没有增加到需要大量额外优化的程度我认为使用标准的工作队列方式是最简单且可靠的。通常我并不倾向于写复杂的多线程代码而是采用标准的工作队列方法并确保每个工作项的规模足够大这样就不需要太多关于多线程细节的担忧。 至于无限循环通常这类设计是为了保持线程持续运行等待任务或条件触发继续执行但具体的目的取决于实现的上下文。 无限循环的意义是什么它不是最终会结束吗 在多线程编程中虽然理论上一个循环可以是无限的但实际上线程通常会在某些条件下终止。例如线程可能在进程退出时被终止因此不必担心循环永远无法结束。尽管如此使用线程时循环不能真的是完全无限的。在多线程库的设计中通常会确保线程在某些条件下能被正确地终止避免无限循环导致的资源浪费或程序崩溃。 你推荐哪些线程库为什么例如 boost 或 pthreads 在多线程编程中推荐使用简单的原子操作来实现线程同步比如锁定增量、比较交换、交换锁、锁增量等这些操作都非常容易实现。通过这些基本的原子操作可以构建出一个简单有效的工作队列。相比使用其他第三方多线程库自己实现同步机制更能确保代码的简洁和高效同时避免了继承外部库中可能存在的bug。某些多线程库可能存在不必要的复杂性甚至可能在某些情况下带来更多的问题。 什么更好一个工作调度器每个线程都可以从中获取工作还是为每个线程设置独立的队列 在多线程任务分配中是否使用工作队列取决于工作负载的类型。以渲染为例可以考虑不使用队列直接将每个线程分配特定的任务区域例如不同的图块这样线程之间就不需要通信了。这种方式是有效的并且可能在某些情况下比使用工作队列更高效。但同时工作队列方式更为通用适用于更多场景因此即使这种策略可能不一定是最优的使用工作队列仍然值得作为学习和教育目的的展示。 然而即使在渲染这种场景下直接分配任务给线程并不总是最合适的策略。原因在于不同的任务如不同的图块可能需要不同的时间来完成。如果某些任务特别复杂或需要更多计算单独分配任务给固定线程可能会导致负载不均衡从而影响性能。在这种情况下使用工作队列可以动态地平衡负载确保每个线程始终能处理下一个可用的任务避免了某些线程长时间等待的情况。 黑板“单生产者/单消费者”与“单生产者/多消费者” 在讨论多线程任务分配时有两种常见的策略一种是为每个线程分配独立的队列另一种是使用单一的队列所有线程从中取任务。两者的核心区别在于任务分配的方式以及线程如何从队列中取任务。 第一种方法中每个线程有自己的独立队列任务被分配到不同的队列中这意味着每个线程负责自己的任务区域比如渲染任务中的图块。在这种方法下任务是静态分配的每个线程只处理自己负责的部分。缺点是如果某个任务需要较长时间才能完成比如某个图块需要更多的计算时间那么分配到这个任务的线程将长时间处于忙碌状态其他线程则可能空闲导致性能浪费。 第二种方法是使用单一的工作队列所有线程从同一个队列中获取任务。此时如果有某个任务例如一个图块需要较长的时间其他线程仍然可以从队列中获取剩余的任务避免了其他线程的等待。因此这种方法在负载不均的情况下可以更有效地分配任务减少总体的处理时间。 这种策略的好处在于假设有一个任务需要很长时间比如100毫秒而其他任务需要的时间较短如1毫秒那么使用单一队列的多线程方式可以使得任务的负载分布更加均匀。即使有线程在处理长时间的任务其他线程也能及时处理自己的任务从而减少总体的工作时间。与此相反如果每个线程都有自己的队列任务较重的线程将拖慢整体进度。 然而如果任务量较少例如只有几十个任务使用单一队列的多线程方式可能带来一些额外的开销因为需要处理队列的入队和出队操作。对于较少的工作单元可能不值得为了解决负载不均而增加额外的队列操作成本。 总结来说当任务数量较多时使用单一队列单生产者多消费者模式更有利于任务的均匀分配从而优化性能。而当任务较少时使用独立队列单生产者单消费者模式可能会减少一些额外的同步开销。因此选择哪种方式取决于具体的工作负载和性能需求。 我认为“无锁”的意义在于它不使用操作系统级别的锁 在并发编程中Lock-Free无锁指的是一种设计或实现方式能够避免在多个线程访问共享资源时使用传统的锁机制如互斥锁。传统的锁如互斥锁或读写锁会使得某些线程在访问共享资源时被阻塞直到锁被释放。无锁编程的目标是通过原子操作和其他低级别的同步机制避免线程因锁而产生阻塞从而提高程序的并发性和性能。 Lock-Free 的核心思想是即使多个线程同时访问共享数据也不需要使用加锁操作来确保线程安全。相反使用一些原子操作例如比较并交换 CAS来确保数据的一致性。这样多个线程在尝试修改共享数据时如果发生冲突线程会重新尝试执行操作而不是等待锁的释放。 总的来说Lock-Free 并不是完全没有锁而是避免了传统的锁机制它通过原子操作等方式保证并发程序的安全性因此能显著提高性能尤其在高并发场景中。 黑板“无锁”lock free 在讨论Lock-Free无锁时有两种不同的理解方式 完全无锁这种情况下程序设计完全不依赖于任何锁机制包括操作系统级的锁或处理器级的锁。例如在单生产者单消费者的场景下可以通过一个队列来实现无锁操作。具体做法是确保在一个线程中完成所有写操作后再在另一个线程中读取索引变量避免任何形式的锁。这种方式确保了线程不会因为等待锁的释放而被阻塞因此可以实现真正的无锁。 不阻塞线程另一种无锁的定义是指程序不会因为一个线程的操作而阻塞其他线程。虽然这种方式仍然使用处理器的原子操作例如处理器内的锁但是它避免了线程等待某个线程完成操作后才能继续执行。换句话说虽然处理器内部可能有锁机制但不会导致线程停滞等待。虽然这种方式通常被称为无锁但它的实际效果是“线程不会因为其他线程的操作而停止”。 因此无锁有不同的解释。第一种方式代表完全无锁第二种方式则是通过处理器锁来实现线程间的同步避免线程被阻塞。这两者有着本质的区别而第二种方式可能被称为“便捷的无锁”因为它仍然依赖于硬件级别的锁。 InterlockedIncrement 是一个无锁操作(这个术语有点混乱原始术语是“非阻塞”non-blocking这个术语更有用) 原始术语是“非阻塞”non-blocking它对于理解无锁操作更有用。如果大家都使用“非阻塞”来描述那些仅仅避免线程阻塞的情况而使用“无锁”来描述完全没有任何锁的操作那就更清晰明了。这样一来术语“非阻塞”会更加准确因为这确实是在说操作不会使线程停滞等待其他线程的完成。与此相比所谓的“无锁”通常还是依赖于处理器级的锁来避免线程的阻塞因此仍然存在锁的机制尽管不是操作系统级别的锁。 如果我们将这些术语区分开来就能更加明确**“无锁”表示完全没有任何形式的锁而“非阻塞”**表示即使使用了锁线程依然不会因等待而被阻塞。实际上很多人提到“无锁”时通常是在谈论这种非阻塞的情况。 在一个同步线程中同步接收网络包是否合理 在处理网络数据包接收时通常只需要一个同步线程就足够了因为大多数网络卡的速度不足以让现代处理器如x64架构的处理器感到瓶颈。x64处理器的读取速度远高于网络卡的速度。因此除非是非常高端的网络卡或者在超级快速的光纤链路上否则几乎总是网络数据包接收的瓶颈来自于入站链路而不是数据包的解队列。 然而问题并不总是出在接收数据包本身更多时候是处理这些数据包的过程。比如操作系统的TCP/IP栈可能存在性能瓶颈或者处理每个数据包所需的操作可能非常复杂导致处理速度比接收速度慢。在这种情况下可能需要多个线程来并行处理这些数据包。然而单纯的将数据包从网络接口卡NIC读取出来对于x64处理器来说几乎不可能成为瓶颈。只有在使用极为优秀的互联网连接时才可能达到超过x64处理器处理速度的水平。 在开发周期的后期是否会讨论其他操作系统如 Mac 和 Linux的线程处理 在开发周期的后期可能会涉及到在不同操作系统如Mac和Linux上实现多线程。通常来说这些操作系统的多线程实现与Windows上的并没有太大区别基本的操作是调用类似于启动线程的函数因此并不会非常复杂。然而这部分内容通常会在项目开始进行移植时才会涉及。 关于多线程的优点它主要体现在提升程序的并行处理能力。通过将任务分配给不同的线程可以有效利用多核处理器的能力从而提升整体性能特别是在需要处理大量独立任务或计算密集型任务时多线程能够显著提高效率。 拥有一个工作队列而不是每次添加工作时都创建一个新线程这有什么优势 任务队列的使用不是为了每次添加任务时创建一个新线程而是为了提升性能。任务队列的目的是更有效地管理和调度任务使得多个线程能够更高效地执行而不是单纯地依赖于线程的创建与销毁。通过使用队列程序能够避免频繁创建和销毁线程带来的性能开销。队列的作用是为了更好地利用系统资源实现更高效的并行处理。 此外要理解任务队列的两个主要目的一方面是为了处理任务的重叠即利用多个线程并行处理任务另一方面队列的核心目的是为了优化性能使得系统在多核处理器上能够更加高效地分配和调度任务。 黑板重叠 vs 性能 线程有两种主要用途一种是为了“重叠工作”另一种是为了“性能优化”。 线程用于重叠工作这种情况通常是在多个任务同时进行的场景下使用线程比如应用程序的UI线程和其他30个任务线程。目标并不是提高性能而是确保多个任务并行进行避免其中某个任务阻塞整个程序的运行。例如如果有30个任务需要同时执行其中一个是UI任务其他是后台任务通过多线程可以确保UI线程始终能响应用户输入即使某些任务可能在执行过程中存在延迟。使用多线程只是为了让多个任务可以并行处理而非优化每个任务的执行效率。 线程用于性能优化在这种情况下线程的目的是为了最大化地利用CPU的处理能力。例如当一个CPU有多个核心时如果创建的线程数超过了核心数操作系统就需要将这些线程调度执行而调度本身需要消耗大量资源。如果采用操作系统的调度机制它会涉及到上下文切换、保存和恢复寄存器状态等额外开销。而如果自己管理任务队列通过一个简单的操作比如互锁交换来获取任务这样就能避免这些额外的开销从而提高性能。通过自定义的工作队列可以显著减少CPU周期的浪费减少调度和上下文切换的消耗。 综上线程在“重叠工作”场景下目的是让多个任务可以并行执行主要关注任务的并行性而非性能而在“性能优化”场景下线程的使用旨在减少操作系统调度带来的开销最大化利用硬件资源提升整体性能。对于性能优化尽量避免让操作系统调度过多工作自己管理任务队列能够减少不必要的成本提高执行效率。 总结与未来展望 接下来的任务是开始实现工作队列并将其应用到实际的多线程渲染工作中。通过今天的示范可以看到我提到的那些问题并不是理论上的而是实际会发生的情况比如出现错误值或者内存被覆盖等问题这些都是我们在开发过程中需要解决的。 明天我们会集中解决这些问题首先修复现有的错误然后将这个工作队列抽象成一个可以在游戏中使用的组件进而实现渲染过程中的多线程。这一过程中还有一个需要注意的问题——缓存行对齐问题特别是在内存访问时避免不同线程的循环操作覆盖彼此的内存。这一点在单线程时并不明显但一旦引入多线程后这个问题就会显现出来。我们会稍后再详细讨论这方面的内容。 我还记得当我第一次学习多线程时特别是在多线程的处理上x64架构给我带来了不少惊讶。x64处理器在同步方面有一些独特的行为和其他处理器不常见的特性比如强排序写入等这些功能比很多其他处理器都要好。这种强大的特性实际上减轻了开发者对内存顺序和同步的要求。 但是理论上我们仍然可能遇到一些内存排序问题。虽然x64可能自动处理某些情况但为了确保跨平台的兼容性我们最好还是提前做一些准备避免依赖特定平台的特性特别是当我们移植到其他架构时。例如如果后续我们需要移植到Android平台可能会遇到一些特定架构如Neon的限制导致在不同平台上的行为不一致。 总的来说明天我们将专注于实现工作队列并解决相关问题后续会确保渲染流程正常工作并处理与内存对齐相关的潜在问题。即使在x64平台上内存对齐问题可能不会直接影响性能但为了确保代码的可移植性和稳定性我们还是要考虑这方面的因素。
http://www.hkea.cn/news/14476403/

相关文章:

  • 网站流量太高 如何做负载均衡网站中的给我留言怎么做
  • 网站编辑年终总结自己做网站如何赚钱吗
  • 百度联盟 网站备案信息南京网络营销
  • 关于网站开发中网站上传网站接入服务单位名称
  • 公司网站建设推荐乐云seo企业网站项目的流程
  • 网站建设基本知识代码网站全屏宽度是多少
  • 如何查公司网站开发时间网站建设辶金手指排名十二
  • 百度怎么建设网站常用的电子商务网站
  • 定制开发一般多少钱无锡网站制作优化推广
  • 无锡网站建设制作温州网站设计服务
  • 网站开发专业广州营销课程培训班
  • 便利的邯郸网站建设asp.net网站开发基础
  • 新校区建设网站承德做网站
  • 网站工作建设站电话云服务器使用教程
  • 深圳松岗网站建设网页传奇游戏排行榜2022
  • 制作网站制作公司WordPress 多厂商
  • 公民道德建设网站如何做色流量网站
  • 福州网站建设公司哪个好适合seo软件
  • 网站建设行业淘宝装修模板nodejs做网站容易被攻击吗
  • 知企业网站怎么打不开wordpress安装问题
  • 佛山高端网站建设报价线下推广怎么做
  • 电子商务网站软件建设的垂直网站建设步骤
  • 免费的建设网站软件下载wordpress投稿验证码
  • 家政公司网站的建设建设银行跨行转账网站
  • 网站建设与管理考试题常熟沿江开发区人才网最新招聘
  • 网站技术培训学校如何用外网ip做网站
  • 做网站对电脑要求高吗加盟手机网站源码
  • 去哪找人做网站网站网警备案流程
  • 做明星简介网站侵权吗正规的网站制作电话
  • 固始做网站的公司优化网站标题