目录
串行、并行、并发
串行Serial
一次只能处理一个任务或指令的方式。
在串行处理中,任务按顺序依次执行,每个任务必须等待前一个任务完成后才能开始执行。这意味着任务的执行是相互依赖的,无法同时进行。
并行Parallel
同时处理多个任务或指令的方式。
在并行处理中,多个任务可以同时进行,每个任务都有独立的处理资源。这样可以显著提高处理速度和效率。并行处理通常需要多个处理器或多个计算单元。
当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
并发Concurrent
同时处理多个任务的能力。
与并行不同,并发并不要求任务同时进行或独立处理资源。在并发处理中,任务可以按照一定的策略交替执行,每个任务都有时间片或资源分配给它。尽管在任意给定的时间点只有一个任务在执行,但是任务之间通过快速切换来实现看起来同时执行的效果。
在程序中,往往有很多很耗时的工作,比如上传文件、下载文件、跟客户聊天需要长时间建立连接。这种时候,一个线程是服务不了多个用户的,会产生因为资源独占产生的等待问题。并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。(并发指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)) 并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
总结
并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。
综上所述,串行是按顺序一个一个执行任务,一次只能处理一个任务;并行是同时处理多个任务,每个任务都有独立的处理资源;而并发是指同时处理多个任务的能力,任务之间可以交替执行,通过时间片切换或资源分配来实现
进程与线程
进程Process
进程是操作系统中的一个执行实例。它拥有独立的内存空间和资源,是程序的一次执行过程。每个进程都是相互独立的,它们之间不能直接共享内存,通信需要通过进程间通信(IPC)机制。进程的创建和销毁是相对较重的操作。
我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
进程一般由程序、数据集合和进程控制块三部分组成。
- 程序用于描述进程要完成的功能,是控制进程执行的指令集;
- 数据集合是程序在执行时所需要的数据和工作区;
- 程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
进程具有的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
- 并发性:任何进程都可以同其他进程一起并发执行;
- 独立性:进程是系统进行资源分配和调度的一个独立单位;
- 结构性:进程由程序、数据和进程控制块三部分组成。
线程Thread
线程是进程的一个执行实体,是进程内的一个轻量级执行单元。一个进程可以包含多个线程,它们共享同一进程的内存和资源,可以直接访问共享的数据。线程之间的切换开销较小,因为它们共享了进程的上下文。然而,线程之间的同步与互斥需要适当的同步机制,例如锁和信号量。
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
进程与线程
进程与线程的关系:
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
总之,线程和进程都是一种抽象的概念,线程是一种比进程更小的抽象,线程和进程都可用于实现并发。 在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。它相当于一个进程里只有一个线程,进程本身就是线程。所以线程有时被称为轻量级进程(Lightweight Process,LWP)
线程的生命周期
线程的生命周期共有五个状态:
- 创建:一个新的线程被创建,等待该线程被调用执行;
- 就绪:时间片已用完,此线程被强制暂停,等待下一个属于它的时间片到来;
- 运行:此线程正在执行,正在占用时间片;
- 阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
- 退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。
协程Coroutine
什么是协程
协程是一种用户态的轻量级线程,也称为协作式任务。与线程不同,协程由程序员控制,而不是由操作系统调度。协程可以在执行过程中主动暂停和恢复,可以保存和恢复上下文,并通过协程调度器决定执行哪个协程。协程允许非抢占式的任务切换,因此可以实现高效的并发和协作。
协程是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。
因为是自主开辟的异步任务,所以很多人也更喜欢叫它们纤程(Fiber),或者绿色线程(GreenThread)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
目的
在传统的J2EE系统中都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,因为这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底。
最常见的例子就是JDBC(它是同步阻塞的),这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。而另外过多的线程,也会带来更多的ContextSwitch开销。
对于上述问题,现阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其代表派是node.js以及Java里的新秀Vert.x。
而协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销。
特点
- 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
- 线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
- 由于在同一个线程上,因此可以避免竞争关系而使用锁。
- 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。
原理
当出现IO阻塞的时候,由协程的调度器进行调度,通过将数据流立刻yield掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine,而跑在由coroutine
负责调度的线程称为Fiber
。比如Golang里的 go关键字其实就是负责开启一个Fiber
,让func
逻辑跑在上面。
由于协程的暂停完全由程序控制,发生在用户态上;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上。
因此,协程的开销远远小于线程的开销,也就没有了ContextSwitch上的开销。
协程与线程
比较项 | 线程 | 协程 |
---|---|---|
占用资源 | 初始单位为1MB,固定不可变 | 初始一般为 2KB,可随需要而增大 |
调度所属 | 由 OS 的内核完成 | 由用户完成 |
切换开销 | 涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP…等寄存器的刷新等 | 只有三个寄存器的值修改 - PC / SP / DX. |
性能问题 | 资源占用太高,频繁创建销毁会带来严重的性能问题 | 资源占用小,不会带来严重的性能问题 |
数据同步 | 需要用锁等机制确保数据的一直性和可见性 | 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 |
同步与异步
在计算机编程中,同步(Synchronous)和异步(Asynchronous)是两种不同的执行模式,用于描述程序或代码的执行方式。
-
同步(Synchronous):
- 在同步执行模式下,代码按照顺序依次执行,每个操作都必须等待前一个操作完成后才能执行。也就是说,程序会阻塞当前线程直到操作完成。
- 同步操作通常是直观的,易于理解和调试,因为代码按照顺序执行,并且操作之间的关系清晰明了。
- 但是,同步操作可能会导致程序的性能下降,特别是当某些操作需要耗费大量时间时,整个程序会被阻塞。
-
异步(Asynchronous):
- 在异步执行模式下,代码不按照顺序执行,而是通过回调函数、消息队列或事件驱动等机制来处理操作。一个操作的开始并不会阻塞当前线程继续执行,而是在后台进行,当操作完成后再通知主线程。
- 异步操作可以提高程序的性能和响应性,特别是对于需要等待I/O操作或网络请求的情况。
- 但是,异步操作会增加代码的复杂性,因为需要处理回调函数、事件处理等异步操作的机制。
在实际编程中,同步和异步操作会根据需要进行选择。例如,对于需要立即返回结果并继续执行的情况,可以选择同步操作。而对于耗时操作或需要等待外部资源的情况,可以选择异步操作以避免程序阻塞。
在多线程编程、网络编程、GUI编程等领域,异步操作通常被广泛应用。通过合理选择同步和异步操作模式,可以提高程序的性能和用户体验。希望这个简要解释能帮助您更好地理解同步和异步的概念。如果您有任何问题或需要进一步解释,请随时告诉我。我将竭诚为您提供帮助。
特别注意:
异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或者交给另外的进程来处理。
竞态条件
竞态条件(又称竞争条件、竞争冒险):指一个系统或进程的输出,依赖于不受控制的事件出现顺序或出现时机。
示例
假设有两个线程,各自会将同一个全局数值变量加1。理想状态下,它们会以这样的顺序执行:
线程1 | 线程2 | 数据值 | |
---|---|---|---|
0 | |||
读取(0) | ← | 0 | |
增加(1) | 0 | ||
写回 | → | 1 | |
读取(1) | ← | 1 | |
增加(2) | 1 | ||
写回 | → | 2 |
在上文的理想状态下,运行的结果为预期的2。但是,如果两个线程在没有锁定或通过信号量同步的情况下同时运行,执行的结果将可能出错。如下展示了另一种情况:
线程1 | 线程2 | 数据值 | |
---|---|---|---|
0 | |||
读取(0) | ← | 0 | |
读取(0) | ← | 0 | |
增加(1) | 0 | ||
增加(1) | 0 | ||
写回 | → | 1 | |
写回 | → | 1 |
在这种情况下,因为线程的增加操作没有互斥,导致最终结果为1,而不是预期的2。
编程中的竞态条件
在多线程或并发编程中的竞态条件:由于多个线程访问共享资源的顺序不确定或时间差异导致的问题。
当多个线程同时访问共享资源,并且其中至少一个是写操作时,可能会发生竞态条件。竞态条件可能会导致程序出现不确定的行为,甚至导致数据不一致性或程序崩溃。
以下是竞态条件的一些特点和可能的解决方法:
-
特点:
- 竞态条件通常发生在多线程或并发环境中,由于线程执行顺序的不确定性导致。
- 竞态条件可能会导致数据竞争、数据不一致、死锁等问题。
- 竞态条件通常由于共享资源的并发访问引起,如共享变量、共享内存区域等。
-
解决方法:
- 使用互斥锁(Mutex):通过在访问共享资源之前获取锁并在访问完毕后释放锁,可以确保同一时间只有一个线程能够访问共享资源,从而避免竞态条件。
- 使用条件变量(Condition Variable):在某些情况下,可以使用条件变量来控制线程的等待和唤醒,避免竞态条件。
- 使用原子操作(Atomic Operations):某些编程语言或库提供原子操作,可以保证某些操作的原子性,从而避免竞态条件。
临界区
临界区是为了避免竞态条件而提出的概念,指的是:多线程程序中访问共享资源的那一部分代码或代码块。
通过在临界区内使用互斥锁、信号量或其他同步机制,可以保证同一时间只有一个线程能够进入临界区并访问共享资源,从而避免竞态条件。
同步与互斥
编程中,同步(Synchronization)和互斥(Mutual Exclusion)是两个相关但不完全相同的概念,用于处理多线程或并发操作时确保数据一致性和避免竞态条件的问题。
-
同步(Synchronization):
- 同步是一种机制,用于协调多个线程或进程之间的操作顺序,以确保它们按照一定的顺序执行,从而避免竞态条件。
- 分布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。所以同步就是在互斥的基础上,通过其它机制实现访问者对资源的有序访问。
- 同步可以包括各种技术,如信号量、条件变量、管程等,用于控制线程之间的交互和数据共享。
-
互斥(Mutual Exclusion):
- 互斥是一种机制,用于确保在任意给定时间只有一个线程能够访问共享资源,避免多个线程同时访问共享资源导致的数据不一致或竞态条件。
- 同一个资源同一时间只有一个访问者可以进行访问,其他访问者需要等前一个访问者访问结束才可以开始访问该资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
- 互斥通常通过锁(Lock)来实现,线程在访问共享资源之前获取锁,访问完毕后释放锁,以确保同一时间只有一个线程能够获得锁并访问资源。
虽然同步和互斥都是用于处理多线程或并发操作中的问题,但它们有一些区别:
- 同步是一种更广泛的概念,用于协调多个线程的操作顺序,而互斥是同步的一种特例,用于确保共享资源的互斥访问。
- 同步可以包括其他技术,如信号量、条件变量等,而互斥主要是通过锁来实现。
在实际编程中,同步和互斥通常是同时使用的,以确保多线程程序的正确性和避免竞态条件。通过合理使用同步和互斥机制,可以保证数据的一致性和避免多线程操作中的问题。
阻塞与非阻塞
阻塞(Blocking)和非阻塞(Non-blocking)是描述程序或操作的执行模式的两个重要概念,特别在I/O操作和多线程编程中经常被提及。
-
阻塞(Blocking):
- 阻塞指的是当一个操作或函数调用发生时,当前线程会被挂起或暂停,直到该操作完成后才会继续执行下一步。
- 在阻塞模式下,调用线程会一直等待操作的完成,期间不能执行其他操作,直到操作完成或超时。
-
非阻塞(Non-blocking):
- 非阻塞指的是当一个操作或函数调用发生时,当前线程不会被挂起,而是立即返回一个状态或值,可以继续执行其他操作。
- 在非阻塞模式下,调用线程可以继续执行其他操作,而不需要等待操作的完成,可以通过轮询或回调等方式来检查操作的状态。
在实际应用中,阻塞和非阻塞模式在各种场景下都有用处,具体取决于应用程序的需求和设计。以下是它们在不同领域的应用示例:
- I/O操作:在文件读写、网络通信等I/O操作中,可以使用阻塞或非阻塞模式。在阻塞模式下,线程会等待I/O操作完成后再继续执行,而在非阻塞模式下,线程可以继续执行其他任务,无需等待I/O操作完成。
- 多线程编程:在多线程编程中,线程之间的同步和通信可以采用阻塞或非阻塞方式。阻塞调用可能会导致线程阻塞等待,而非阻塞调用可以让线程自由执行其他任务并定期检查或轮询操作状态。
- 在设计和实现程序时,需要考虑到阻塞和非阻塞对程序性能、响应性和资源利用的影响,根据具体应用场景和需求选择合适的模式。
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
-
同步阻塞(Synchronous Blocking):
- 同步阻塞是指调用线程发出请求后,需要等待被调用的操作完成并返回结果,期间调用线程会被阻塞,无法执行其他任务。
- 在同步阻塞模式下,调用线程会一直等待操作完成,直到得到结果或超时。
-
同步非阻塞(Synchronous Non-blocking):
- 同步非阻塞是指调用线程发出请求后,会立即得到一个状态或值的返回,但仍需要轮询或等待操作完成,期间调用线程可以继续执行其他任务。
- 在同步非阻塞模式下,调用线程可以继续执行其他操作,但仍然需要等待操作完成。
-
异步阻塞(Asynchronous Blocking):
- 异步阻塞是指调用线程发出请求后,可以继续执行其他任务,但需要等待被调用的操作完成并返回结果,期间调用线程可能被阻塞。
- 在异步阻塞模式下,调用线程不会被挂起,但可能在等待操作完成时被阻塞。
-
异步非阻塞(Asynchronous Non-blocking):
- 异步非阻塞是指调用线程发出请求后,可以立即得到一个状态或值的返回,同时可以继续执行其他任务,而不需要等待操作的完成。
- 在异步非阻塞模式下,调用线程可以继续执行其他操作,操作的完成状态可以通过回调函数或轮询来获取。