并发、进程、线程的基本概念和综述
并发
并发表示两个或者更多任务(独立的活动)同时发生(进行)。例如,一面唱歌一面弹琴,一面走路一面说话,画画的时候听小说等。回归到计算机领域,所谓并发,就是一个程序同时执行多个独立的任务。
以往计算机只有单核CPU(中央处理器)的时候,这种单核CPU某一个时刻只能执行一个任务,它实现多任务的方式就是由操作系统调度,每秒钟进行多次所谓的“任务切换”,也就是这个任务做一小会如做10ms,再切换到下个任务再做10ms等,诸如此类。因为任务切换的速度很快,所以在人类的感觉中,好像是多个任务在同时进行中(并行执行),其实这是一种并发的假象(不是真正的并发)。当然,这种任务之间的切换(也称上下文切换)也有一定的时间开销,例如操作系统要保存任务切换时的各种状态、执行进度等信息,因为一会儿任务切换回来的时候要复原这些信息。
随着计算机硬件的发展,专门用于服务器和高性能计算领域的“多处理器计算机”,甚至是家用台式机等均已出现,早先都是单核CPU,而现在在一块芯片上有多个CPU,双核、四核屡见不鲜,甚至还有8核、10核甚至更多核等。这些多处理器计算机以及多核计算机能够真正实现并行执行多个任务(这叫硬件并发),因为有多个CPU,就可以同时做多件事情。当然,如果并发的任务数量超过了CPU的数量,如现在有10个任务,但却只是双核CPU,那任务切换这种事情肯定还是存在的。而当代的计算机,不难发现它们同时都在处理几百上千个任务,例如打开任务管理器就可以看到,如图所示,其中线程数量就可以理解为并发的任务数量。
例如有4个任务,CPU是一个双核CPU,任务切换如图所示(当然可能任务切换也不一定是这样的平均和均衡,因为操作系统的任务调度算法是很复杂的)。
可以看到,使用并发的原因主要就是能够让多件事情同时做,从而提高整体做事情的效率,也就是提高整体的运行性能。
可执行程序
可执行程序就是磁盘上的一个文件(也叫程序),如在Windows操作系统下,一个扩展名为. exe
的程序一般就是一个可执行程序。而在Linux操作系统下,有可执行权限的文件如权限是-rwxrw-r--
,这里的x表示的就是可执行权限,有这种权限的文件一般就是可执行程序。
进程
知道了什么叫可执行程序,可执行程序当然是能运行的,在Windows操作系统下,双击一个. exe
可执行程序,这个程序就运行起来了;又如,在Visual Studio 2019中,使用Ctrl+F5快捷键也可以运行程序;而在Linux操作系统下,如一个可执行程序叫作a
,那么,输入./a
,然后按Enter键就可以把这个可执行程序运行起来。
这样就引出了进程的概念:一个可执行程序运行起来,这就叫创建了一个进程。如果再次运行这个可执行程序,就又创建了一个进程,如此反复,多次运行一个可执行程序,就可以创建出多个进程。
所以,进程就是运行起来了的可执行程序。
线程
请先记住两件事:
- 每个进程都有一个主线程,这个主线程是唯一的,也就是一个进程中只能有一个主线程。 (自带)
- 当运行一个可执行程序,产生了一个进程之后,这个主线程就随着这个进程默默启动起来了。
这里以创建的MyProject项目为例说明一下。
当按Ctrl+F5键执行MyProject项目的可执行程序(编译链接后会生成可执行程序)后,程序从main
主函数开始执行代码,一直遇到main
主函数中的return 0;
语句行,从而结束整个程序的运行。实际上是由主线程来执行main
主函数中的代码,如图所示。
所以要把线程理解成一条代码的执行通路(道路)。
这就好比从北京到深圳,这里有一条道路,主线程走的是这条道路。
在这里,北京就相当于执行步骤main
主函数的开始代码,深圳就相当于main
主函数的return0;
结束代码。要从main
主函数的开始代码执行,一直执行到main
函数的结束代码return0;
为止。
除了主线程之外,可以通过编写代码来创建其他线程,其他线程可以走别的道路,去不同的地方。例如,从北京到南京就是一条新的道路,这就是上面讲的并发的概念,非常简单。每创建一个新线程,就可以在同一时刻多做一件不同的事情。
当然,线程并不是越多越好,每个线程都需要一个独立的堆栈空间(耗费内存,如一个线程占用1MB堆栈空间),而且线程之间的切换也要保存很多中间状态等,这也涉及上面提到过的上下文切换。所以,如果线程太多,上下文切换的就会很频繁,而上下文切换是一种必须但是没有价值和意义的额外工作,会耗费本该属于程序运行的时间。
举个最实际的例子,例如要开发一个游戏服务器,同时针对100个玩家提供服务,其中有一个玩家(玩家1)要充值,充值需要游戏服务器去联络充值服务器,如图所示。
那么,和充值服务器通信、充值、充值服务器反馈假如大概需要10s的等待时间(一般来说,手机游戏充值,都需要10~20s甚至更长的等待时间),等待充值服务器给玩家1反馈信息。那么,假如在等待的10s时间内,另一个玩家(玩家2)如果有一个新需求,玩过手机游戏的读者都知道,如玩家2要抽一张卡,那作为游戏服务器的开发者总不可能让玩家2等10s,等玩家1充值完再为玩家2提供抽卡服务,否则玩家2肯定要抓狂了。
所以,游戏服务器必须采用多线程方式来处理多个玩家的各种不同需求。如图所示,一个线程处理玩家1的充值,另外一个线程处理玩家2的抽卡,两者互不影响,同时进行。
对线程做一个总结:
- 线程是用来执行代码的。
- 把线程理解成一条代码的执行通路(道路),一个新线程代表一条新的通路。
- 一个进程自动包含一个主线程,主线程随着进程默默启动并运行,并可以包含多个其他线程(非主线程,需要用代码来创建其他线程),但创建线程的数量最大一般都不建议超过200~300个,至于到底多少个合适,在实际项目中要不断调整和优化,有时候线程多了效率还会降低。
- 因为主线程是自动启动的,所以一个进程中最少也是有一个线程(主线程)的。所以进程与线程有点像父亲和孩子的关系。
- 多线程程序可以同时做多件事,所以运行效率更高。但到底有多高,并不是一个很容易评估和量化的事情,仍旧需要在实际编程与实际项目中体会和调整。
很多人对于多线程开发觉得复杂、难用、不好控制。其实,多线程是一种非常强大的工具,能把多线程程序写好,写高效和稳定,不但是作为一个高级开发人员实力的体现,也是很实际的项目必须采用的一种开发方式,因为只有这样,程序的运行效率才能满足实际生产境的需要(就像上面举的例子,不可能让玩家2白白地等10s)。换句话说,很多场合下,必须用多线程开发技术,所开发的程序才有实际的商用价值。
并发的实现方法
这里还是要总结一下,把并发、进程、线程概念往一起串一串。
怎样实现并发呢?以下两种实现手段都可以:
- 通过多个进程来实现并发,每个进程做一件事。这里所说的进程,指的是这种只包含一个主线程的进程,这种手段并不需要在程序代码中书写任何与线程有关的代码。
- 在单独的一个进程中创建多个线程来实现并发,这种情况下就得书写代码来创建除主线程外的其他线程了(主线程不需要创建,进程一启动,主线程自动就存在并开始运行了)。
多进程并发
看一看多进程并发。执行一个可执行程序就生成了一个进程,如果思路拓展一点,想一想,例如启动了一个Word(Office办公软件之一)用来打字,就是一个Word进程:启动测览器观看一个网页,就是一个浏览器进程。
回归到项目中来,例如写一个网络游戏,有账号服务器、游戏逻辑服务器,这每一个都是一个可执行程序,把它们运行起来,每个服务器就是一个进程,这些进程之间可能也需要互相通信,例如账号服务器要把账号信息发送给游戏逻辑服务器用来进行身份验证。进程之间的通信手段比较多,如果是同一台计算机上的进程之间的通信,可以使用管道、文件、消息队列、共享内存等技术来实现,而在不同的计算机之间的进程通信可以使用socket(网络套接字)等网络通信技术来实现。由于进程之间数据保护问题,即便是在同一个计算机上,进程之间的通信也是挺复杂的。
多线程并发
多线程就是在单个的进程中创建多个线程。所以,线程有点类似于轻量级的进程,每个线程都是独立运行的,但是一个进程中的所有线程共享地址空间(共享内存),还有诸如全局变量、指针、引用等,都是可以在线程之间传递的,所以可以得出一个结论:使用多线程的开销远远小于多进程。
当然,多线程使用共享内存虽然灵活,但是也带来了新的问题——数据一致性问题。例如,线程A要写一块数据,同时线程B也要写这块数据,那么,就需要采取一定的技术手段,让它们有先有后地去写,而不能同时去写,如果同时去写,可能写进去的数据就会出现互相覆盖等数据不一致的错误。
虽然多进程并发和多线程并发可以混合使用,但是一般来讲,建议优先考虑使用多线程的技术手段来实现并发而不是多进程。在本章中也只讲解多线程的并发相关的技术。所以,后续谈到并发,指的都是多线程并发。
总结
和多进程并发比较来讲,多线程并发的优缺点如下。
优点:线程启动速度更快,更轻量级;系统资源开销更少;执行速度更快。
缺点;使用起来有一定难度,要小心处理数据的一致性问题。
C++11新标准线程库
以往要写多线程的程序,每个不同的操作系统平台都有不同的线程创建方法,如Windows操作系统下,用CreateThread
函数创建线程,并且还有一堆和线程相关的其他函数和概念,如临界区、互斥量等。 Linux 操作系统下也一样,如用 pthread_create
创建线程。
所以可以看到,这些代码都不一样,不能跨操作系统(跨平台)使用。当然,如果使用一些跨平台的多线程库如POSIX thread(pthread)
是可以的,这样就可以在不同的操作系统平台上写相同的多线程相关程序代码。但是,为了支持pthread
,在Windows操作系统下要配置一番,如果换成Linux操作系统,也要在Linux操作系统下配置一番,这两个不同的操作系统平台,配置方法多多少少会有不同,所以pthread
使用起来也并不是那么方便。
现在,好消息来了,从C++11新标准开始,C++语言本身增加了针对多线程的支持。这意味着可以使用C++语言本身提供的编程接口(方法)来编写和具体的操作系统平台无关的多线程程序,极大地增加了程序的可移植性,在Windows下开发的C++多线程程序代码可以不用修改源代码,直接拿到同样支持C++11新标准的Linux平台的C++编译器上编译(这就是跨平台)。在实际的开发中,如果要求必须实现跨平台开发时,这会大量地减少开发人员的工作量,实在是很好。
代码地址