Go語言中調度器之GMP模型

前言

隨著服務器硬件迭代升級,配置也越來越高烹骨。為充分利用服務器資源翻伺,并發(fā)編程也變的越來越重要。在開始之前沮焕,需要了解一下并發(fā)(concurrency)和并行(parallesim)的區(qū)別吨岭。

并發(fā): 邏輯上具有處理多個同時性任務的能力。

并行: 物理上同一時刻執(zhí)行多個并發(fā)任務峦树。

通常所說的并發(fā)編程辣辫,也就是說它允許多個任務同時執(zhí)行,但實際上并不一定在同一時刻被執(zhí)行魁巩。在單核處理器上急灭,通過多線程共享CPU時間片串行執(zhí)行(并發(fā)非并行)。而并行則依賴于多核處理器等物理資源谷遂,讓多個任務可以實現(xiàn)并行執(zhí)行(并發(fā)且并行)葬馋。

多線程或多進程是并行的基本條件,但單線程也可以用協(xié)程(coroutine)做到并發(fā)埋凯。簡單將Goroutine歸納為協(xié)程并不合適点楼,因為它運行時會創(chuàng)建多個線程來執(zhí)行并發(fā)任務,且任務單元可被調度到其它線程執(zhí)行白对。這更像是多線程和協(xié)程的結合體掠廓,能最大限度提升執(zhí)行效率,發(fā)揮多核處理器能力甩恼。

Go編寫一個并發(fā)編程程序很簡單蟀瞧,只需要在函數之前使用一個Go關鍵字就可以實現(xiàn)并發(fā)編程沉颂。

func main() {    go func(){
        fmt.Println("Hello,World!")
    }()
}

Go調度器組成

Go語言雖然使用一個Go關鍵字即可實現(xiàn)并發(fā)編程,但Goroutine被調度到后端之后悦污,具體的實現(xiàn)比較復雜铸屉。先看看調度器有哪幾部分組成。

1切端、G

G是Goroutine的縮寫彻坛,相當于操作系統(tǒng)中的進程控制塊,在這里就是Goroutine的控制結構踏枣,是對Goroutine的抽象昌屉。其中包括執(zhí)行的函數指令及參數;G保存的任務對象茵瀑;線程上下文切換间驮,現(xiàn)場保護和現(xiàn)場恢復需要的寄存器(SP、IP)等信息马昨。

Go不同版本Goroutine默認棧大小不同竞帽。

// Go1.11版本默認stack大小為2KB

_StackMin = 2048
 
// 創(chuàng)建一個g對象,然后放到g隊列
// 等待被執(zhí)行
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
    _g_ := getg()

    _g_.m.locks++
    siz := narg
    siz = (siz + 7) &^ 7

    _p_ := _g_.m.p.ptr()
    newg := gfget(_p_)    
    if newg == nil {        
       // 初始化g stack大小
        newg = malg(_StackMin)
        casgstatus(newg, _Gidle, _Gdead)
        allgadd(newg)
    }    
    // 以下省略}

2、M

M是一個線程或稱為Machine鸿捧,所有M是有線程棧的屹篓。如果不對該線程棧提供內存的話,系統(tǒng)會給該線程棧提供內存(不同操作系統(tǒng)提供的線程棧大小不同)匙奴。當指定了線程棧抱虐,則M.stack→G.stack,M的PC寄存器指向G提供的函數饥脑,然后去執(zhí)行恳邀。

type m struct {    
    /*
        1.  所有調用棧的Goroutine,這是一個比較特殊的Goroutine。
        2.  普通的Goroutine棧是在Heap分配的可增長的stack,而g0的stack是M對應的線程棧灶轰。
        3.  所有調度相關代碼,會先切換到該Goroutine的棧再執(zhí)行谣沸。
    */
    g0       *g
    curg     *g         // M當前綁定的結構體G

    // SP、PC寄存器用于現(xiàn)場保護和現(xiàn)場恢復
    vdsoSP uintptr
    vdsoPC uintptr

    // 省略…}

3笋颤、P

P(Processor)是一個抽象的概念乳附,并不是真正的物理CPU。所以當P有任務時需要創(chuàng)建或者喚醒一個系統(tǒng)線程來執(zhí)行它隊列里的任務伴澄。所以P/M需要進行綁定赋除,構成一個執(zhí)行單元。

P決定了同時可以并發(fā)任務的數量非凌,可通過GOMAXPROCS限制同時執(zhí)行用戶級任務的操作系統(tǒng)線程举农。可以通過runtime.GOMAXPROCS進行指定敞嗡。在Go1.5之后GOMAXPROCS被默認設置可用的核數颁糟,而之前則默認為1航背。

// 自定義設置GOMAXPROCS數量
func GOMAXPROCS(n int) int {    
    /*
        1.  GOMAXPROCS設置可執(zhí)行的CPU的最大數量,同時返回之前的設置继谚。
        2.  如果n < 1,則不更改當前的值爬橡。
    */
    ret := int(gomaxprocs)

    stopTheWorld("GOMAXPROCS")    
    // startTheWorld啟動時,使用newprocs。
    newprocs = int32(n)
    startTheWorld()    
    return ret
}
// 默認P被綁定到所有CPU核上
// P == cpu.cores

func getproccount() int32 {    
    const maxCPUs = 64 * 1024
    var buf [maxCPUs / 8]byte


    // 獲取CPU Core
    r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])

    n := int32(0)    
    for _, v := range buf[:r] {        
       for v != 0 {
            n += int32(v & 1)
            v >>= 1
        }
    }    
    if n == 0 {
       n = 1
    }    
    return n
}
// 一個進程默認被綁定在所有CPU核上,返回所有CPU core坝锰。
// 獲取進程的CPU親和性掩碼系統(tǒng)調用
// rax 204                          ; 系統(tǒng)調用碼
// system_call sys_sched_getaffinity; 系統(tǒng)調用名稱
// rid  pid                         ; 進程號
// rsi unsigned int len             
// rdx unsigned long *user_mask_ptr
sys_linux_amd64.s:
TEXT runtime·sched_getaffinity(SB),NOSPLIT,$0
    MOVQ    pid+0(FP), DI
    MOVQ    len+8(FP), SI
    MOVQ    buf+16(FP), DX
    MOVL    $SYS_sched_getaffinity, AX
    SYSCALL
    MOVL    AX, ret+24(FP)
    RET

Go調度器調度過程

首先創(chuàng)建一個G對象婚脱,G對象保存到P本地隊列或者是全局隊列今魔。P此時去喚醒一個M。P繼續(xù)執(zhí)行它的執(zhí)行序障贸。M尋找是否有空閑的P涡贱,如果有則將該G對象移動到它本身。接下來M執(zhí)行一個調度循環(huán)(調用G對象->執(zhí)行->清理線程→繼續(xù)找新的Goroutine執(zhí)行)惹想。

M執(zhí)行過程中,隨時會發(fā)生上下文切換督函。當發(fā)生上線文切換時嘀粱,需要對執(zhí)行現(xiàn)場進行保護,以便下次被調度執(zhí)行時進行現(xiàn)場恢復辰狡。Go調度器M的棧保存在G對象上锋叨,只需要將M所需要的寄存器(SP、PC等)保存到G對象上就可以實現(xiàn)現(xiàn)場保護宛篇。當這些寄存器數據被保護起來娃磺,就隨時可以做上下文切換了,在中斷之前把現(xiàn)場保存起來叫倍。如果此時G任務還沒有執(zhí)行完偷卧,M可以將任務重新丟到P的任務隊列,等待下一次被調度執(zhí)行吆倦。當再次被調度執(zhí)行時听诸,M通過訪問G的vdsoSP、vdsoPC寄存器進行現(xiàn)場恢復(從上次中斷位置繼續(xù)執(zhí)行)蚕泽。

1晌梨、P 隊列
通過上圖可以發(fā)現(xiàn),P有兩種隊列:本地隊列和全局隊列须妻。

  • 本地隊列: 當前P的隊列仔蝌,本地隊列是Lock-Free,沒有數據競爭問題荒吏,無需加鎖處理敛惊,可以提升處理速度。
  • 全局隊列:全局隊列為了保證多個P之間任務的平衡绰更。所有M共享P全局隊列豆混,為保證數據競爭問題篓像,需要加鎖處理。相比本地隊列處理速度要低于全局隊列皿伺。

2员辩、上線文切換

簡單理解為當時的環(huán)境即可,環(huán)境可以包括當時程序狀態(tài)以及變量狀態(tài)鸵鸥。例如線程切換的時候在內核會發(fā)生上下文切換奠滑,這里的上下文就包括了當時寄存器的值,把寄存器的值保存起來妒穴,等下次該線程又得到cpu時間的時候再恢復寄存器的值宋税,這樣線程才能正確運行。

對于代碼中某個值說讼油,上下文是指這個值所在的局部(全局)作用域對象杰赛。相對于進程而言,上下文就是進程執(zhí)行時的環(huán)境矮台,具體來說就是各個變量和數據乏屯,包括所有的寄存器變量、進程打開的文件瘦赫、內存(堆棧)信息等辰晕。

3、線程清理
Goroutine被調度執(zhí)行必須保證P/M進行綁定确虱,所以線程清理只需要將P釋放就可以實現(xiàn)線程的清理含友。什么時候P會釋放,保證其它G可以被執(zhí)行校辩。P被釋放主要有兩種情況窘问。

  • 主動釋放:最典型的例子是,當執(zhí)行G任務時有系統(tǒng)調用宜咒,當發(fā)生系統(tǒng)調用時M會處于Block狀態(tài)南缓。調度器會設置一個超時時間,當超時時會將P釋放荧呐。
  • 被動釋放:如果發(fā)生系統(tǒng)調用汉形,有一個專門監(jiān)控程序,進行掃描當前處于阻塞的P/M組合倍阐。當超過系統(tǒng)程序設置的超時時間概疆,會自動將P資源搶走。去執(zhí)行隊列的其它G任務峰搪。

終于要來說說Golang中最吸引人的goroutine了岔冀,這也是Golang能夠橫空出世的主要原因。不同于Python基于進程的并發(fā)模型概耻,以及C++使套、Java等基于線程的并發(fā)模型罐呼。Golang采用輕量級的goroutine來實現(xiàn)并發(fā),可以大大減少CPU的切換≌旄撸現(xiàn)在已經有太多的文章來介紹goroutine的用法嫉柴,在這里,我們從源碼的角度來看看其內部實現(xiàn)奉呛。

重申一下重點:goroutine中的三個實體

goroutine中最主要的是三個實體為GMP计螺,其中:

G: 代表一個goroutine對象,每次go調用的時候瞧壮,都會創(chuàng)建一個G對象登馒,它包括棧、指令指針以及對于調用goroutines很重要的其它信息咆槽,比如阻塞它的任何channel陈轿,其主要數據結構:

type g struct {
  stack       stack   // 描述了真實的棧內存,包括上下界

  m              *m     // 當前的m
  sched          gobuf   // goroutine切換時秦忿,用于保存g的上下文      
  param          unsafe.Pointer // 用于傳遞參數麦射,睡眠時其他goroutine可以設置param,喚醒時該goroutine可以獲取
  atomicstatus   uint32
  stackLock      uint32 
  goid           int64  // goroutine的ID
  waitsince      int64 // g被阻塞的大體時間
  lockedm        *m     // G被鎖定只在這個m上運行
}

其中最主要的當然是sched了小渊,保存了goroutine的上下文。goroutine切換的時候不同于線程有OS來負責這部分數據茫叭,而是由一個gobuf對象來保存酬屉,這樣能夠更加輕量級,再來看看gobuf的結構:

type gobuf struct {
    sp   uintptr
    pc   uintptr
    g    guintptr
    ctxt unsafe.Pointer
    ret  sys.Uintreg
    lr   uintptr
    bp   uintptr // for GOEXPERIMENT=framepointer
}

其實就是保存了當前的棧指針揍愁,計數器呐萨,當然還有g自身,這里記錄自身g的指針是為了能快速的訪問到goroutine中的信息莽囤。

M:代表一個線程谬擦,每次創(chuàng)建一個M的時候,都會有一個底層線程創(chuàng)建朽缎;所有的G任務惨远,最終還是在M上執(zhí)行,其主要數據結構:

type m struct {
    g0      *g     // 帶有調度棧的goroutine

    gsignal       *g         // 處理信號的goroutine
    tls           [6]uintptr // thread-local storage
    mstartfn      func()
    curg          *g       // 當前運行的goroutine
    caughtsig     guintptr 
    p             puintptr // 關聯(lián)p和執(zhí)行的go代碼
    nextp         puintptr
    id            int32
    mallocing     int32 // 狀態(tài)

    spinning      bool // m是否out of work
    blocked       bool // m是否被阻塞
    inwb          bool // m是否在執(zhí)行寫屏蔽

    printlock     int8
    incgo         bool // m在執(zhí)行cgo嗎
    fastrand      uint32
    ncgocall      uint64      // cgo調用的總數
    ncgo          int32       // 當前cgo調用的數目
    park          note
    alllink       *m // 用于鏈接allm
    schedlink     muintptr
    mcache        *mcache // 當前m的內存緩存
    lockedg       *g // 鎖定g在當前m上執(zhí)行话肖,而不會切換到其他m
    createstack   [32]uintptr // thread創(chuàng)建的棧
}

結構體M中有兩個G是需要關注一下的北秽,一個是curg,代表結構體M當前綁定的結構體G最筒。另一個是g0贺氓,是帶有調度棧的goroutine,這是一個比較特殊的goroutine床蜘。普通的goroutine的棧是在堆上分配的可增長的棧辙培,而g0的棧是M對應的線程的棧蔑水。所有調度相關的代碼,會先切換到該goroutine的棧中再執(zhí)行扬蕊。也就是說線程的棧也是用的g實現(xiàn)搀别,而不是使用的OS的。

P:代表一個處理器厨相,每一個運行的M都必須綁定一個P领曼,就像線程必須在么一個CPU核上執(zhí)行一樣,由P來調度G在M上的運行蛮穿,P的個數就是GOMAXPROCS(最大256)庶骄,啟動時固定的,一般不修改践磅;M的個數和P的個數不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000)单刁;每一個P保存著本地G任務隊列,也有一個全局G任務隊列府适。P的數據結構:

type p struct {
    lock mutex

    id          int32
    status      uint32 // 狀態(tài)羔飞,可以為pidle/prunning/...
    link        puintptr
    schedtick   uint32     // 每調度一次加1
    syscalltick uint32     // 每一次系統(tǒng)調用加1
    sysmontick  sysmontick 
    m           muintptr   // 回鏈到關聯(lián)的m
    mcache      *mcache
    racectx     uintptr

    goidcache    uint64 // goroutine的ID的緩存
    goidcacheend uint64

    // 可運行的goroutine的隊列
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr

    runnext guintptr // 下一個運行的g

    sudogcache []*sudog
    sudogbuf   [128]*sudog

    palloc persistentAlloc // per-P to avoid mutex

    pad [sys.CacheLineSize]byte

其中P的狀態(tài)有Pidle, Prunning, Psyscall, Pgcstop, Pdead;在其內部隊列runqhead里面有可運行的goroutine檐春,P優(yōu)先從內部獲取執(zhí)行的g逻淌,這樣能夠提高效率。

除此之外疟暖,還有一個數據結構需要在這里提及卡儒,就是schedt,可以看做是一個全局的調度者:

type schedt struct {
   goidgen  uint64
    lastpoll uint64

    lock mutex

    midle        muintptr // idle狀態(tài)的m
    nmidle       int32    // idle狀態(tài)的m個數
    nmidlelocked int32    // lockde狀態(tài)的m個數
    mcount       int32    // 創(chuàng)建的m的總數
    maxmcount    int32    // m允許的最大個數

    ngsys uint32 // 系統(tǒng)中goroutine的數目俐巴,會自動更新

    pidle      puintptr // idle的p
    npidle     uint32
    nmspinning uint32 

    // 全局的可運行的g隊列
    runqhead guintptr
    runqtail guintptr
    runqsize int32

    // dead的G的全局緩存
    gflock       mutex
    gfreeStack   *g
    gfreeNoStack *g
    ngfree       int32

    // sudog的緩存中心
    sudoglock  mutex
    sudogcache *sudog
}

大多數需要的信息都已放在了結構體M骨望、G和P中,schedt結構體只是一個殼欣舵∏骛可以看到,其中有M的idle隊列缘圈,P的idle隊列劣光,以及一個全局的就緒的G隊列。schedt結構體中的Lock是非常必須的糟把,如果M或P等做一些非局部的操作赎线,它們一般需要先鎖住調度器。

goroutine的運行過程

所有的goroutine都是由函數newproc來創(chuàng)建的糊饱,但是由于該函數不能調用分段棧垂寥,最后真正調用的是newproc1。在newproc1中主要進行如下動作:

func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g {
    newg = malg(_StackMin)
    casgstatus(newg, _Gidle, _Gdead)
    allgadd(newg) 
    newg.sched.sp = sp
    newg.stktopsp = sp
    newg.sched.pc = funcPC(goexit) + sys.PCQuantum 
    newg.sched.g = guintptr(unsafe.Pointer(newg))
    gostartcallfn(&newg.sched, fn)
    newg.gopc = callerpc
    newg.startpc = fn.fn
    ......
}

分配一個g的結構體
初始化這個結構體的一些域
將g掛在就緒隊列
綁定g到一個m上

這個綁定只要m沒有突破上限GOMAXPROCS,就拿一個m綁定一個g。如果m的waiting隊列中有就從隊列中拿,否則就要新建一個m,調用newm滞项。

func newm(fn func(), _p_ *p) {
    mp := allocm(_p_, fn)
    mp.nextp.set(_p_)
    mp.sigmask = initSigmask
    execLock.rlock()
    newosproc(mp, unsafe.Pointer(mp.g0.stack.hi))
    execLock.runlock()
}

該函數其實就是創(chuàng)建一個m狭归,跟newproc有些相似,之前也說了m在底層就是一個線程的創(chuàng)建文判,也即是newosproc函數过椎,在往下挖可以看到會根據不同的OS來執(zhí)行不同的bsdthread_create函數,而底層就是調用的runtime.clone

clone(cloneFlags,stk,unsafe.Pointer(mp),unsafe.Pointer(mp.g0),unsafe.Pointer(funcPC(mstart)))

m創(chuàng)建好之后戏仓,線程的入口是mstart疚宇,最后調用的即是mstart1

func mstart1() {
    _g_ := getg()
    gosave(&_g_.m.g0.sched)
    _g_.m.g0.sched.pc = ^uintptr(0)
    asminit()
    minit()

    if _g_.m == &m0 {
        initsig(false)
    }

    if fn := _g_.m.mstartfn; fn != nil {
        fn()
    }
    schedule()
}

里面最重要的就是schedule了,在schedule中的動作大體就是找到一個等待運行的g赏殃,然后然后搬到m上敷待,設置其狀態(tài)為Grunning,直接切換到g的上下文環(huán)境,恢復g的執(zhí)行。

func schedule() {
    _g_ := getg()

    if _g_.m.lockedg != nil {
        stoplockedm()
        execute(_g_.m.lockedg, false) // Never returns.
    }
}

schedule的執(zhí)行可以大體總結為:

schedule函數獲取g => [必要時休眠] => [喚醒后繼續(xù)獲取] => execute函數執(zhí)行g => 執(zhí)行后返回到goexit => 重新執(zhí)行schedule函數

簡單來說g所經歷的幾個主要的過程就是:Gwaiting->Grunnable->Grunning仁热。經歷了創(chuàng)建,到掛在就緒隊列,到從就緒隊列拿出并運行整個過程榜揖。

casgstatus(gp, _Gwaiting, _Grunnable)
casgstatus(gp, _Grunnable, _Grunning)

引入了struct M這層抽象。m就是這里的worker,但不是線程抗蠢。處理系統(tǒng)調用中的m不會占用mcpu數量,只有干事的m才會對應到線程.當mcpu數量少于GOMAXPROCS時可以一直開新的線程干活.而goroutine的執(zhí)行則是在m和g都滿足之后通過schedule切換上下文進入的.

搶占式調度

當有很多goroutine需要執(zhí)行的時候举哟,是怎么調度的了,上面說的P還沒有出場呢迅矛,在runtime.main中會創(chuàng)建一個額外m運行sysmon函數妨猩,搶占就是在sysmon中實現(xiàn)的。

sysmon會進入一個無限循環(huán), 第一輪回休眠20us, 之后每次休眠時間倍增, 最終每一輪都會休眠10ms. sysmon中有netpool(獲取fd事件), retake(搶占), forcegc(按時間強制執(zhí)行gc), scavenge heap(釋放自由列表中多余的項減少內存占用)等處理.

func sysmon() {
    lasttrace := int64(0)
    idle := 0 // how many cycles in succession we had not wokeup somebody
    delay := uint32(0)
    for {
        if idle == 0 { // start with 20us sleep...
            delay = 20
        } else if idle > 50 { // start doubling the sleep after 1ms...
            delay *= 2
        }
        if delay > 10*1000 { // up to 10ms
            delay = 10 * 1000
        }
        usleep(delay)

        ......
    }       
}

里面的函數retake負責搶占:

func retake(now int64) uint32 {
    n := 0
    for i := int32(0); i < gomaxprocs; i++ {
        _p_ := allp[i]
        if _p_ == nil {
            continue
        }
        pd := &_p_.sysmontick
        s := _p_.status
        if s == _Psyscall {
            // 如果p的syscall時間超過一個sysmon tick則搶占該p
            t := int64(_p_.syscalltick)
            if int64(pd.syscalltick) != t {
                pd.syscalltick = uint32(t)
                pd.syscallwhen = now
                continue
            }
            if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {
                continue
            }
            incidlelocked(-1)
            if atomic.Cas(&_p_.status, s, _Pidle) {
                if trace.enabled {
                    traceGoSysBlock(_p_)
                    traceProcStop(_p_)
                }
                n++
                _p_.syscalltick++
                handoffp(_p_)
            }
            incidlelocked(1)
        } else if s == _Prunning {
            // 如果G運行時間過長秽褒,則搶占該G
            t := int64(_p_.schedtick)
            if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
                continue
            }
            if pd.schedwhen+forcePreemptNS > now {
                continue
            }
            preemptone(_p_)
        }
    }
    return uint32(n)
}

枚舉所有的P 如果P在系統(tǒng)調用中(_Psyscall), 且經過了一次sysmon循環(huán)(20us~10ms), 則搶占這個P壶硅, 調用handoffp解除M和P之間的關聯(lián), 如果P在運行中(_Prunning), 且經過了一次sysmon循環(huán)并且G運行時間超過forcePreemptNS(10ms), 則搶占這個P

并設置g.preempt = true震嫉,g.stackguard0 = stackPreempt森瘪。

為什么設置了stackguard就可以實現(xiàn)搶占?

因為這個值用于檢查當前椖凳簦空間是否足夠, go函數的開頭會比對這個值判斷是否需要擴張棧票堵。

newstack函數判斷g.stackguard0等于stackPreempt, 就知道這是搶占觸發(fā)的, 這時會再檢查一遍是否要搶占。

搶占機制保證了不會有一個G長時間的運行導致其他G無法運行的情況發(fā)生逮栅。

總結

相比大多數并行設計模型悴势,Go比較優(yōu)勢的設計就是P上下文這個概念的出現(xiàn),如果只有G和M的對應關系措伐,那么當G阻塞在IO上的時候特纤,M是沒有實際在工作的,這樣造成了資源的浪費侥加,沒有了P捧存,那么所有G的列表都放在全局,這樣導致臨界區(qū)太大,對多核調度造成極大影響昔穴。

而goroutine在使用上面的特點镰官,感覺既可以用來做密集的多核計算,又可以做高并發(fā)的IO應用吗货,做IO應用的時候泳唠,寫起來感覺和對程序員最友好的同步阻塞一樣,而實際上由于runtime的調度宙搬,底層是以同步非阻塞的方式在運行(即IO多路復用)笨腥。

所以說保護現(xiàn)場的搶占式調度和G被阻塞后傳遞給其他m調用的核心思想,使得goroutine的產生勇垛。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末脖母,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子窥摄,更是在濱河造成了極大的恐慌镶奉,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崭放,死亡現(xiàn)場離奇詭異哨苛,居然都是意外死亡,警方通過查閱死者的電腦和手機币砂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門建峭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人决摧,你說我怎么就攤上這事亿蒸。” “怎么了掌桩?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵边锁,是天一觀的道長。 經常有香客問我波岛,道長茅坛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任则拷,我火速辦了婚禮贡蓖,結果婚禮上,老公的妹妹穿的比我還像新娘煌茬。我一直安慰自己斥铺,他們只是感情好,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布坛善。 她就那樣靜靜地躺著晾蜘,像睡著了一般邻眷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上剔交,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天耗溜,我揣著相機與錄音,去河邊找鬼省容。 笑死抖拴,一個胖子當著我的面吹牛,可吹牛的內容都是我干的腥椒。 我是一名探鬼主播阿宅,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼笼蛛!你這毒婦竟也來了洒放?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤滨砍,失蹤者是張志新(化名)和其女友劉穎往湿,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體惋戏,經...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡领追,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了响逢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绒窑。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖舔亭,靈堂內的尸體忽然破棺而出些膨,到底是詐尸還是另有隱情,我是刑警寧澤钦铺,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布订雾,位于F島的核電站,受9級特大地震影響矛洞,放射性物質發(fā)生泄漏洼哎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一缚甩、第九天 我趴在偏房一處隱蔽的房頂上張望谱净。 院中可真熱鬧窑邦,春花似錦擅威、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春厉熟,著一層夾襖步出監(jiān)牢的瞬間导盅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工揍瑟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留白翻,地道東北人。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓绢片,卻偏偏與公主長得像滤馍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子底循,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內容

  • 調度器——GMP 調度模型 Goroutine 調度器巢株,它是負責在工作線程上分發(fā)準備運行的 goroutines。...
    沉睡的木木夕閱讀 410評論 0 0
  • 前言 隨著服務器硬件迭代升級熙涤,配置也越來越高阁苞。為充分利用服務器資源,并發(fā)編程也變的越來越重要祠挫。在開始之前那槽,需要了解...
    云爬蟲技術研究筆記閱讀 3,828評論 0 7
  • Go語言雖然使用一個Go關鍵字即可實現(xiàn)并發(fā)編程,但Goroutine被調度到后端之后等舔,具體的實現(xiàn)比較復雜倦炒。先看看調...
    我愛張智容閱讀 737評論 0 0
  • 一、調度器的由來 調度本身是指操作系統(tǒng)中為每個任務分配其所需資源的方法软瞎。 在操作系充中逢唤,線程是任務執(zhí)行的最小單位,...
    thepoy閱讀 745評論 0 1
  • 一涤浇、聊聊并發(fā)這件事 在基礎系列我們學習了Go的并發(fā)編程鳖藕,對并發(fā)的概念已經有了一定的了解。在各種現(xiàn)代高級語言中只锭,對并...
    GoFuncChan閱讀 2,331評論 0 4