论坛
潜水/灌水快乐,沉淀知识,认识更多同行。
ToB圈子
加入IT圈,遇到更多同好之人。
朋友圈
看朋友圈动态,了解ToB世界。
ToB门户
了解全球最新的ToB事件
博客
Blog
排行榜
Ranklist
文库
业界最专业的IT文库,上传资料也可以赚钱
下载
分享
Share
导读
Guide
相册
Album
记录
Doing
搜索
本版
文章
帖子
ToB圈子
用户
免费入驻
产品入驻
解决方案入驻
公司入驻
案例入驻
登录
·
注册
只需一步,快速开始
账号登录
立即注册
找回密码
用户名
Email
自动登录
找回密码
密码
登录
立即注册
首页
找靠谱产品
找解决方案
找靠谱公司
找案例
找对的人
专家智库
悬赏任务
圈子
SAAS
ToB企服应用市场:ToB评测及商务社交产业平台
»
论坛
›
软件与程序人生
›
DevOps与敏捷开发
›
游戏引擎学习第123天
游戏引擎学习第123天
前进之路
金牌会员
|
前天 19:24
|
显示全部楼层
|
阅读模式
楼主
主题
834
|
帖子
834
|
积分
2502
仓库: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语言,无法包管完全制止这种问题,除非利用更高级的同步机制,或者转向像C++11这种提供了更强盛线程控制和同步工具的语言。在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 (;
本帖子中包含更多资源
您需要
登录
才可以下载或查看,没有账号?
立即注册
x
回复
使用道具
举报
0 个回复
倒序浏览
返回列表
快速回复
高级模式
B
Color
Image
Link
Quote
Code
Smilies
您需要登录后才可以回帖
登录
or
立即注册
本版积分规则
发表回复
回帖并转播
回帖后跳转到最后一页
发新帖
回复
前进之路
金牌会员
这个人很懒什么都没写!
楼主热帖
低代码平台 - 危险的赌注
后台性能测试规范
Docker 基础 - 1
Redis常见使用场景
小小项目-博客系统 - 服务器版本 - jav ...
泛型通配符?(问号)简介说明 ...
MySQL与Java常用数据类型的对应关系 ...
Python3程序捕获Ctrl+C终止信号 ...
如何从800万数据中快速捞出自己想要的 ...
LeetCode 力扣 205. 同构字符串
标签云
挺好的
服务器
快速回复
返回顶部
返回列表