多线程的简单介绍(翻译)

现代电脑已经具有同一时间执行多个操作的能力。由于硬件的发展以及更智能的操作系统的支持,我们的程序可以执行反馈的更快。
由于这个巨大的能力(同一时间执行多个操作),编程变得十分神奇同时又十分复杂。这项巨大的能力使得你必须去理解电脑内部的执行原理。在第一节中,我将解开多线程的一角,讲解多线程这项如此有魔力的东西。

进程与线程:以正确的方式命名事物

现代的操作系统可以在同一时间执行多个程序。那就是为什么你可以一边用你的浏览器看文章一边又可以听音乐的原因。每个程序都被看做一个进程被执行。操作系统知道许多软件技巧来使多个程序一起运行,当然也可以使用底层硬件。不管何种方式,最终的结果就是让你觉得你所有的程序都在同一时间进行着。

在操作系统中运行进程并不是同时执行多个操作的唯一方法。每个进程也可以同时运行他的子任务——线程。在被启动的时候,每个进程至少会出发一个被叫做主线程的线程。然后根据程序的需要,额外的线程或被启动或被终止。多线程是指在一个进程中运行多个线程。

例如,您的媒体播放器可能运行多个线程:一个用于呈现界面-这通常是主线程,另一个用于播放音乐等。

你可以这样去想:操作系统是一个包含许多进程的容器。其中每个进行又是多个线程的容器。在这个文章里面,我将只关注于线程但是以上的所有主题都是有趣值得深究的。

Operating systems can be seen as a box that contains processes, which in turn contain one or more threads.

操作系统可以被看做一个包含进程的大盒子,而每个进程又包含了一个或多个线程(至少一个主线程)

进程与线程之间的区别

每个进程都拥有一个属于自己的内存块。这些自己的内存块无法被其他的进程访问分享:你的浏览器程序无法访问媒体播放器的内存,反之亦然。当然,如果你同时启动的相同程序两次(例如打开了谷歌浏览器两次),他们之间也无法进行内存之间的访问。操作系统将每个实例视为一个新进程,并分配其各自独立的内存部分。因此,默认情况下,两个或多个进程无法共享数据,除非它们执行高级技巧——所谓的进程间通信(IPC)

与进程不同,线程共享操作系统分配给其父进程的同一块内存:媒体播放器主界面中的数据可以被音频引擎轻松访问,反之亦然。因此,两个线程更容易相互通信。除此之外,线程通常比进程更轻:它们占用的资源更少,创建速度更快,这就是为什么它们也被称为轻量级进程的原因。

线程是使程序同时执行多个操作的简便方法。如果没有线程,您将不得不为每个任务编写一个程序,将它们作为进程运行,并通过操作系统对它们进行同步。这将更加困难(IPC比较棘手),并且速度较慢(进程比线程更重)。

Green threads, of fibers

到目前为止提到的线程是一个操作系统的事情:一个想要启动一个新线程的进程必须与操作系统对话。不过,并非每个平台都支持线程。绿色线程(也称为光纤)是一种仿真,使多线程程序在不提供该功能的环境中工作。例如,如果底层操作系统没有本机线程支持,虚拟机可能实现绿色线程。

绿色线程的创建和管理速度更快,因为它们完全绕过了操作系统,但也有缺点。我将在下一集中写下这个话题。

“绿色线程”的名字是指Sun MyStand在90年代设计了原始Java线程库的绿色团队。如今Java不再使用绿色线程:它们在2000返回到本地的线程。其他一些编程语言(go、haskell或rust等)实现了绿色线程的等价物,而不是本机线程。

线程被用于什么

为什么一个进程应该使用多个线程?正如我之前提到的,并行工作大大加快了速度。假设您将要在电影编辑器中渲染电影。编辑器可能足够智能,可以将渲染操作分散到多个线程上,每个线程在多个线程上处理最终电影的一块。因此,如果使用一个线程,那么任务需要一个小时,而使用两个线程,则需要30分钟;使用四个线程,则需要15分钟,依此类推。

真的那么简单吗?有三点需要考虑:

不是每个程序都需要多线程。如果你的应用程序执行顺序操作或者经常等待用户做一些事情,多线程可能没有那么好;

您只需不向应用程序抛出更多的线程,以使其更快地运行:每个子任务都必须经过仔细的思考和设计,才能执行并行操作;

不能100%地保证线程将真正并行地执行其操作,也就是说,它实际上取决于底层硬件。

最后一个是至关重要的:如果您的计算机不同时支持多个操作,那么操作系统必须伪造它们。我们马上就知道。现在,让我们把并发性看作是同时运行任务的感知,而真正的并行性则是同时运行的任务。

(个人:单个CPU在执行指令的时候是一条一条执行的:通过CS:IP来获取指令)

什么使并发性和并行性成为可能?

计算机中的中央处理器(CPU)完成运行程序的繁重工作。它由几个部分组成,主要部分是所谓的核心部分:这就是实际执行计算的地方。核心一次只能运行一个操作。

这当然是一个主要的缺点。因此,操作系统开发了先进的技术,使用户能够同时运行多个进程(或线程),特别是在图形环境中,甚至在单核机器上。最重要的一个任务称为抢占式多任务处理,抢占是中断一个任务,切换到另一个任务,然后在以后恢复第一个任务的能力。

因此,如果您的CPU只有一个内核,那么操作系统的一部分工作就是将单核计算能力分散到多个进程或线程上,这些进程或线程在循环中一个接一个地执行。这个操作会让你产生一个以上的程序并行运行的错觉,或者一个程序同时做多个事情(如果是多线程的话)。虽然满足并发性,但真正的并行性——同时运行进程的能力——仍然缺失。

今天,现代CPU在引擎盖下有多个核心,每个核心一次执行一个独立的操作。这意味着对于两个或多个内核,真正的并行性是可能的。例如,我的Intel Core i7有四个内核:它可以同时运行四个不同的进程或线程。

操作系统能够检测CPU核心的数量,并为每个核心分配进程或线程。线程可以分配给操作系统喜欢的任何核心,并且这种调度对于正在运行的程序是完全透明的。另外,如果所有核心都忙的话,先发制人的多任务可能会出现。这使您能够运行比计算机中可用的实际数量或核心更多的进程和线程。

单核多线程应用程序:有意义吗?

在单核机器上实现真正的并行是不可能的。然而,如果您的应用程序可以从中受益,那么编写多线程程序仍然是有意义的。当一个进程使用多个线程时,即使其中一个线程执行缓慢或阻塞的任务,抢占式多任务也可以保持应用程序的运行。

比如说,你正在开发一个桌面应用程序,它从一个非常慢的磁盘上读取一些数据。如果只使用一个线程编写程序,整个应用程序将冻结,直到磁盘操作完成:分配给唯一线程的CPU电源在等待磁盘唤醒时被浪费。当然,操作系统正在运行除此之外的许多其他进程,但是您的特定应用程序将不会有任何进展。

让我们以多线程的方式重新考虑您的应用程序。线程A负责磁盘访问,而线程B负责主接口。如果线程A由于设备速度慢而被卡住,那么线程B仍然可以运行主界面,从而保持程序响应。这是可能的,因为有两个线程,操作系统可以在两个线程之间切换CPU资源,而不会陷入较慢的线程。

(个人:I/O等待的时候就可以执行其他的任务,而不是呆呆的在等待)

更多线程,更多问题

如我们所知,线程共享其父进程的同一块内存。这使得他们中的两个或多个在同一个应用程序中交换数据非常容易。例如:一个电影编辑器可能拥有一大部分包含视频时间线的共享内存。这样的共享内存正被几个指定用于将电影呈现到文件中的工作线程读取。它们都只需要一个指向该内存区域的句柄(例如指针),就可以从该内存区域中读取数据并将渲染帧输出到磁盘。

只要从同一个内存位置读取两个或多个线程,就可以顺利运行。当其中至少一个写到共享内存中,而其他人正在从中读取时,麻烦就开始了。此时可能出现两个问题:
数据争用-当编写器线程修改内存时,读线程可能正在从内存中读取数据。如果编写器还没有完成它的工作,读卡器将得到损坏的数据;
竞态条件-读线程应该只有在写入之后才能读取。如果正好相反呢?比数据争用更微妙的是,争用条件是两个或多个线程以不可预知的顺序执行它们的工作,而实际上,操作应该按照正确的顺序执行。您的程序可以触发一个争用条件,即使它受到数据争用的保护。

如果一段代码工作正常,即没有数据争用或争用条件,即使许多线程同时执行它,也可以说它是线程安全的。您可能已经注意到一些编程库声明自己是线程安全的:如果您正在编写一个多线程程序,您希望确保任何其他第三方函数可以跨不同的线程使用,而不会触发并发问题。

数据竞争的根本原因

我们知道一个CPU内核一次只能执行一条机器指令。这种指令被称为原子指令,因为它是不可分割的:它不能被分解成更小的操作。

不可分割的属性使得原子操作本质上是线程安全的。当一个线程对共享数据执行原子写入时,没有其他线程可以读取修改的一半。相反,当一个线程对共享数据执行原子读取时,它会读取在某一时刻出现的整个值。线程无法通过原子操作,因此不会发生数据争用。

坏消息是绝大多数的操作都是非原子的。甚至一些硬件上的x=1这样的简单分配也可能由多个原子机器指令组成,使分配本身成为一个整体而非原子。因此,如果一个线程读取X而另一个线程执行分配,则会触发数据争用。

竞态条件的根本原因

抢占式多任务使操作系统能够完全控制线程管理:它可以根据高级调度算法启动、停止和暂停线程。作为程序员,您不能控制执行的时间或顺序。事实上,不能保证这样的简单代码:

writer_thread.start()
reader_thread.start()

将以该特定顺序启动两个线程。运行这个程序几次,您会注意到它在每次运行时的行为是不同的:有时编写器线程首先启动,有时读卡器启动。如果您的程序需要编写器始终在读者面前运行,那么您肯定会遇到竞争情况。

这种行为被称为不确定性:结果每次都会改变,你无法预测。调试受竞争条件影响的程序非常烦人,因为您不能总是以可控方式重现问题。

教线程相处:并发控制

同步-一种确保一次只有一个线程使用资源的方法。同步是将代码的特定部分标记为“受保护的”,这样两个或多个并发线程就不会同时执行它,从而破坏共享数据;

原子操作-借助操作系统提供的特殊指令,可以将一组非原子操作(如前面提到的分配)转换为原子操作。这样,无论其他线程如何访问共享数据,共享数据始终保持有效状态;

不可变数据-共享数据被标记为不可变,没有什么可以更改它:线程只能从中读取,消除了根本原因。正如我们所知,线程可以安全地从相同的内存位置读取,只要它们不修改它。这是函数式编程背后的主要原理。

原文链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、据库操作、用户权限管理等关键技术。 据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的据库设计文件。这些文件通常包括据库结构图、据表设计文档,以及示例据SQL脚本。用户可以通过这些文件快速搭建项目所需的据库环境,并了解各个据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、据库操作、用户权限管理等关键技术。 据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的据库设计文件。这些文件通常包括据库结构图、据表设计文档,以及示例据SQL脚本。用户可以通过这些文件快速搭建项目所需的据库环境,并了解各个据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值