首页 > 其他分享 >Golang 协程调度器原理及GPM模型

Golang 协程调度器原理及GPM模型

时间:2022-12-04 17:44:37浏览次数:30  
标签:GPM 协程 队列 goroutine 调度 Golang 线程 运行

目录

进程和线程

进程:运行中的程序,是对应用程序的封装,一个应用程序的启动到关闭的过程对应着一个进程的出生到死亡的过程,从进程中可以获取到程序运行的相关信息。是操作系统调度和执行的基本单位。
线程:存在于进程中的一条执行路径,是CPU进行调度和资源分配的最小单元。

线程和进程的区别

  • 线程只拥有启动所需的最小资源,一个进程中至少有一个以上的线程,线程又称之为轻量级进程。
  • 线程的资源和地址空间都取自进程映像。
  • 线程拥有线程上下文,线程的上下文保存了当前线程所指向代码的PC计数器,一个数据栈、处理器状态和私有的一些数据。
  • 线程是CPU调度的最小单位,是进程中的一条执行路径,是资源分配的最小单位。

在操作系统中,线程通常以CPU时间片轮转的方式进行调度,CPU将一个连续的时间划分为多个时间片,指定线程在特定时间片内运行,并且进行轮转,使得多个线程可以在一个CPU核心的调度下,在一个连续的时间并发执行。通常一个操作系统最大的线程并发数为CPU核数总和,也就是一个CPU核心同一个时刻只能运行一个线程。

在这种线程调度方式中,需要进行频繁的线程上下文切换,保存线程执行现场以及状态、堆栈信息和计数器,所以使用线程时,如果线程过多调度的性能损耗也会加大,甚至很多时候由于上下文切换开销过大,导致线程并发执行效率不如串行执行效率高,这就是传统的内核态线程调度的缺点。

线程按照其调度所在空间,可分为内核级线程及用户级线程

内核级线程

内核级线程依赖于操作系统的线程实现,每个内核级线程都对应着操作系统进程内部的线程实现,线程的调度和控制依赖于操作系统内核的线程,通常操作系统对外提供相应的内核线程操作API供程序使用。操作系统内核可以感知到线程的存在和操作。

内核级线程的优点:

  • 借助操作系统的实现,可利用CPU多核处理器的优势实现并发执行
  • 一个进程内的线程被阻塞后,其他线程仍可继续执行

内核级线程的缺点:

  • 线程的上下文切换需要借助操作系统内核,存在两次用户态和内核态的转化,效率较低

用户级线程

用户级线程指的是通过线程库来实现线程的调度,线程库运行在用户空间中,不依赖于内核的实现,所以用户级线程(又被称之为协程)可以做到对内核无感知,内核不会参与用户级线程的调度和控制,操作系统仍对进程进行直接控制。

用户级线程的优点:

  • 用户级线程上下文切换是在用户空间完成,无需借助内核,所以不用进行内核态转化,效率比较高
  • 用户级线程与具体操作系统无关,只依赖于线程库的实现
  • 用户级线程可以根据自身需要实现相应的调度算法,而无需受操作系统控制

用户级线程的缺点:

  • 操作系统侧以进程为调度单位,当线程阻塞时,该进程内所有线程都阻塞
  • 由于不依赖于操作系统的实现,无法直接利用多核CPU的优势

协程

一个"用户态线程"必须绑定一个"内核态线程",但CPU并不知道有"用户态线程"的存在,它只知道运行的是一个"内核态线程"。
用户线程可称之为"协程(co-routine)"
一个协程(co-routine)可以绑定一个线程(thread),那么多个协程(co-routine)是否可以绑定一个或者多个线程(thread)呢?

协程与线程的关系

N:1

N个协程绑定一个线程,优点是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常轻量快速,但缺点是,一个进程的所有协程都绑定在一个线程上。

缺点:

  • 某个程序用不了硬件的多核加速能力
  • 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了

1:1

1个协程绑定1个线程,这种最容易实现。协程的调度都由CPU完成了,不存在N:1缺点

缺点:

  • 协程的创建、删除和切换的代价都由CPU完成,有点略显昂贵

M:N

M个协程绑定N个线程,是N:1和1:1类型的结合,克服了以上2种模型的缺点,但实现起来最为复杂

协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程。

goroutine

Go为了提供更容易使用的并发方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有线程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上。

Go中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,并且几KB就足够goroutine运行完,
这就能在有限的内存空间内支持大量goroutine,支持了更多的并发。虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。

Goroutine特点

  • 占用内存更小(几KB)
  • 调度更灵活(runtime调度)

旧版本goroutine调度器

在旧版本的Goroutine的调度器中,仅有Goroutine(G表示)和线程(M表示)两种概念,如下。

调度器的实现


M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。

缺点:

  • 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
  • M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程G1的时,为了继续执行G,需要把G1交给M1执行,这就造成了很差的局部性,因为G1和G是相关的,最好放在M上执行,而不是其他M1。
  • 系统调用(CPU在M之家的切换)导致频繁的线程阻塞和取消阻塞增加了系统开销。

Goroutine调度器的GMP模型设计思想

面对旧版本调度器的问题,Go出现了新版本的调度器。

Golang为了减少操作系统内核级线程上下文切换的开销以及提升调度效率,提出了GPM协程调度模型,GPM模型借助了用户级线程的实现思路,通过用户态的协程调度,能够在线程上实现多个协程的并发执行。

GPM三个字母分别表示的是Goroutine、Processor及Machine。

  • Goroutine代表着Golang中的协程,通过Goroutine封装的代码片段将以协程方式并发执行,是GPM调度器调度的基本单位。
  • Processor代表执行Goroutine的上下文环境及资源,是GPM调度器中关联内核级线程与协程的中间调度器。如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。
  • Machine是内核线程的封装,一个M与一个内核级线程一一对应,为Goroutine的执行提供了底层线程能力支持。

GPM结构组成

GPM三大核心组成结构如下

GPM中,M与内核线程一一对应,M可以关联多个P,而P也可以调度多个G

GPM运行模型

在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上。

P在Golang的实现中对应着一个调度队列,其中存储着多个G用于调度,需要注意的是P具备状态的,当其达到特定状态时,其含有的G才可被调度,并且P的数量也代表着实际上的最大Goroutine并行执行数(因为一个P需要在运行时取出一个G与M关联,所以当有N个P时最多可同时取出N个G关联M执行)。

P的数量可通过runtime.GOMAXPROCS函数进行设定,默认为当前系统的CPU核数。

  • 全局队列(Global Queue):存放等待运行的G
  • P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
  • P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。
  • M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。

Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。

P、M数量限制

P的数量:

  • 由启动时环境变量$GOMAXPROCS或者由runtime的方法GOMAXPROCS()决定。意味着在程序执行的任意时刻都只有$GOMAXPROCS个goruntine在同时运行。

M的数量:

  • go语言本身的限制: go程序启动时,会设置最大M的最大数量,默认10000,但是内核很难支持这么多的线程数,所以这个值可以忽略
  • runtime/debug中的SetMaxThreads函数,设置M的最大数量
  • 一个M阻塞了,会创建新的M

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。

P和M何时会被创建

  • P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
  • M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。

P的结构及状态转换

核心字段
const (
       _Pidle = iota
       _Prunning
       _Psyscall 
       _Pgcstop
       _Pdead
    )

type p struct {
   status      uint32 
   schedtick   uint32 
   syscalltick uint32 
   m           muintptr
   runqhead uint32
   runqtail uint32
   runq     [256]guintptr
   runnext guintptr
   gFree struct {
      gList
      n int32
   }
}

P结构体中,重要的字段如下:

status:表示当前P的状态,为上述五个状态之一
schedtick :调度计数器,每被调度一次则自增1
syscalltick:系统调用计数器,每进行一次系统调用则自增1
m:即将要关联的m,M的nextp字段对应着该P
runq:可运行的G队列,默认容量为256个G
runqhead:可运行G队列头,标识目前正在运行的G
runnext:下一个将要运行的G
gFree:空闲G列表,存储着状态为Gdead的G,当其数目过多时,将会被转移到调度器全局G列表,用于被其他P再次使用(相当于一个G缓存池)
P状态及状态流转

五个状态如下:

  • Pidle:当前p尚未与任何m关联,处于空闲状态
  • Prunning:当前p已经和m关联,并且正在运行g代码
  • Psyscall:当前p正在执行系统调用
  • Pgcstop:当前p需要停止调度,一般在GC前或者刚被创建时
  • Pdead:当前p已死亡,不会再被调度

P的状态流转图如下

在P创建之初,会被置为Pgcstop状态,在完成初始化之后,会马上进入Pidel状态,进入该状态后的P可被调度器调度,当P与某个M相关联时,会进入到Prunning状态,当其执行系统调用时,会进入到Psyscall状态,当P应为全局P列表的缩小而被删除时会进入Pdead状态,不会再进行状态流转和调度。当正在执行的P由于某些原因停止调度时,会统一流转成Pidle空闲状态,等待调度,避免线程饥饿。

M的结构及状态转换

一个G就代表一个goroutine,也与go函数对应。我们使用go语句时,实际上是向Go调度器提交了一个并发任务。Go的编译器会把go语句变成内部函数newproc的调用,并把go函数以及其参数部分传递给这个函数,G和P一样具有着多个状态进行转换。

核心字段
const (
   _Gidle = iota
   _Grunnable
   _Grunning 
   _Gsyscall 
   _Gwaiting 
   _Gmoribund_unused
   _Gdead
  _Genqueue_unused
   _Gcopystack
   _Gscan         = 0x1000
   _Gscanrunnable = _Gscan + _Grunnable
   _Gscanrunning  = _Gscan + _Grunning
   _Gscansyscall  = _Gscan + _Gsyscall
   _Gscanwaiting  = _Gscan + _Gwaiting
)

type g struct {
   stack       stack   // offset known to runtime/cgo
   stackguard0 uintptr // offset known to liblink
   stackguard1 uintptr
   m              *m      // current m; offset known to arm liblink
   sched          gobuf 
   atomicstatus   uint32
   waitreason     waitReason // if status==Gwaiting
   preempt        bool       // preemption signal, duplicates stackguard0 = st
   startpc        uintptr         // pc of goroutine function
}

G结构体中重要字段的含义:

stack:当前G所被分配的栈内存空间,由lo及hi两个内存指针组成
stackguard0:g0的最大栈内存地址,当超过了这个数值则需要进行栈扩张
stackguard1:普通用户G的最大栈内存地址,当超过了这个数值则需要进行栈扩张
m:当前关联该G实例的M实例
sched:记录G上下文环境,用于上下文切换
atomicstatus:G的状态值,表示上述几个状态
waitreason:处于Gwaiting的原因
preempt:当前G是否可抢占
startpc:当前G所绑定的函数内存地址
G的状态及转换

G有如下状态

  • Gidle:当前G刚被分配,还未初始化
  • Grunable:正在可运行队列等待运行
  • Gruning:正在运行中,执行G函数
  • Gsyscall:正在执行系统调用
  • Gwaiting:正在被阻塞,一般是该G正在执行网络I/O操作,或正在执行time.Timer、time.Sleep
  • Gdead:已经使用完正在闲置,放入空闲G列表中,可被再次使用(和P不同,P处于Pdead状态则无法被再次调度)
  • Gcopystack:表示当前G的栈正在被移动,可能是因为栈的收缩或扩容
  • Gscan:表明当前正在进行GC扫描,由于在GC扫描的过程中肯定会处于某个前置状态,所以又有以下组合
    • Gscanrunable :代表当前G正等待运行,同时栈正被GC扫描
    • Gscanrunning :表示正处于Grunning状态,同时栈在被GC扫描
    • Gscanwaiting:表示正处于Gwaiting状态,同时栈在被GC扫描
    • Gscansyscall:表示正处于Gsyscall状态,同时栈在被GC扫描

状态流转图

  • 任何G都会存在于全局G列表中,其余4个容器只存放当前作用域内具有某个状态的G
  • 从Gsyscall状态转出的G都会被放到调度器的可运行G队列
  • 刚被运行时系统初始化的G都会被放入本地P的可运行G队列
  • 从Gwaiting状态转出的G,有的会被放入本地P的可运行G队列,有的会被放到调度器的可运行G队列,还有的会被直接运行(比如刚完成网络I/O)
  • 如果本地P的可运行队列G已满,其中的一半G会被转移到调度器的可运行G队列
  • 调度器可运行G队列遵循FIFO(先进先出)

GPM调度器的结构

GPM调度器负责协调G、P、M三者具体的调度工作,每个GO程序中只存在一个GPM调度器,其源码位于runtime/runtime2.go之中,结构体名称为schedt,对应着的全局唯一实例为sched,结构体核心字段如下

type schedt struct {
   // 全局唯一id
   goidgen  uint64
   // 记录的最后一次从i/o中查询G的时间
   lastpoll uint64
   // 互斥锁 
   lock mutex
   // M的空闲链表,通过m.schedlink组成一个M空闲链表
   midle        muintptr
   // 正处于自旋状态的M数量
   nmidle       int32
   // 已经被锁定且正在自旋的M数量
   nmidlelocked int32
   // 下一个M的id,或者是目前已存在的M数量
   mnext        int64
   // M数量的最大值
   maxmcount    int32
   // 已被释放掉的M数量
   nmfreed      int64
   // 系统所开启的协程数量(非用户协程)
   ngsys uint32
   // 空闲P列表
   pidle      puintptr
   // 空闲的P数量
   npidle     uint32
   // 全局的G队列
   // 根据runqhead可以获取队列头的G及g.schedlink形成G链表
   runqhead guintptr
   runqtail guintptr
   // 全局G队列大小
   runqsize int32
   // 等待释放的M列表
   freem *m
   // 是否需要暂停调度(通常因为GC带来的STW)
   gcwaiting  uint32
   // 需要停止但是仍为停止的P数量
   stopwait   int32
   // 实现stopwait事件通知
   stopnote   note
   // 停止调度期间是否进行系统监控任务
   sysmonwait uint32
   // 实现sysmonwait事件通知
   sysmonnote note
}

调度器的设计策略

复用线程:避免频繁的创建、销毁线程,而是对线程的复用

  • work stealing机制
    • 当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
  • hand off机制
    • 当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。

利用并行:GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行。GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行。

抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。

全局G队列:在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

work-stealing触发调度

work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行,如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为。

Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:

用户态阻塞/唤醒

当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为,尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine), 然后再是P的本地队列和全局队列。

系统调用阻塞

当M执行某一个G时候如果发生了阻塞操作,M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P。当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。

队列轮转

可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。

go func() 调度流程

  1. 通过go func() 来创建一个goroutine
  2. 有两个存储G的队列,一个是局部调度器的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局队列中。
  3. G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;
  4. 一个M调度G执行的过程是一个循环机制;
  5. 当M执行某一个G时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P;
  6. 当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。

调度器的生命周期

特殊的M0和G0

  • M0
    M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 在之后M0就和其他的M一样了。
  • G0
    G0是每次启动一个M都会第一个创建的gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数, 每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。

一个G由于调度被中断,此后如何恢复
中断的时候将寄存器里的栈信息,保存到自己的G对象里面。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。

调度分析示例

代码示例

package main
import "fmt"

func main() {
    fmt.Println("Hello world")
}

针对上面的代码对调度器里面的结构做一个分析

  1. runtime创建最初的线程m0和goroutine g0,并把2者关联。
  2. 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。
  3. 示例代码中的main函数是main.main,runtime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。
  4. 启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。
  5. G拥有栈,M根据G中的栈信息和调度信息设置运行环境
  6. M运行G
  7. G退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序

runtime.main的goroutine执行之前都是为调度器做准备工作,runtime.main的goroutine运行,才是调度器的真正开始,直到runtime.main结束而结束。

可视化GMP编程

  • go tool trace
  • Debug trace

标签:GPM,协程,队列,goroutine,调度,Golang,线程,运行
From: https://www.cnblogs.com/JevonWei/p/16950271.html

相关文章

  • golang的希尔排序
    1、什么是希尔排序:插入排序的一种又称“缩小增量排序”(DiminishingIncrementSort),是直接插入排序算法​的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.......
  • Golang文件服务器
    主调函数,设置路由表packagemainimport( "fmt" "net/http" "store/handler")funcmain(){ http.HandleFunc("/file/upload",handler.UploadHandler) http.Ha......
  • Golang反射获得变量类型和值
    1.什么是反射反射是程序在运行期间获取变量的类型和值、或者执行变量的方法的能力。Golang反射包中有两对非常重要的函数和类型,两个函数分别是:reflect.TypeOf能获取类......
  • 【Python】笔记:协程
    协程用作协程的生成器的基本行为协程使用生成器函数定义:定义体中有yield关键字defsimple_coroutine():print('->coroutinestart')x=yield#因为......
  • golang rang 字符串
    golang遍历字符串,有多种方式:``点击查看代码 //字符串,把字符串起来 s:="中国人,zgr" forpos,char:=ranges{ //range是按照字符来遍历,返回字符出现的位置......
  • 协程及其实现,图片懒加载
    一、什么是协程?答:协程就是一个无优先级的子程序调度组件,允许子程序在特定的地方挂起和恢复。协程包含于线程,线程包含于进程。只要内存足够,一个线程可以有任意多个协程,但某......
  • golang的单引号、双引号、反引号区别
    1、单引号在go语言中表示golang中的rune(int32)类型,byte(int8别称),单引号里面是单个字符,对应的值为改字符的ASCII值。Unicode是ASCII(美国信息交换标准码)字符编码的一个扩展......
  • Go-07 Golang中的数组
    packagemainimport"fmt"/*...Golang中的数组...*//* Go语言中的数组是指一系列相同类型数据的集合。数组中的元素必须要相同数据类型。数组中包含的每个数据被称......
  • golang gorm使用
     gorm链式操作:MethodChaining,Gorm实现了链式操作接口,所以你可以把代码写成这样: //创建一个查询tx:=db.Where("name=?","jinzhu")//添加更多条件ifso......
  • golang的插入排序算法
    1、什么是插入排序?先看一个例子:{7,6,1,9,3}无序数列中,我们约定好无序数列的第一个元素7作为有序数列{7},然后分别对{6,1,9,3}的数与7进行比较移位得到新的有序数列。第一次迭......