亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Golang并發(fā)編程之GMP模型詳解

 更新時間:2023年03月22日 15:25:11   作者:IguoChan  
傳統(tǒng)的并發(fā)編程模型是基于線程和共享內(nèi)存的同步訪問控制的,共享數(shù)據(jù)受鎖的保護,線程將爭奪這些鎖以訪問數(shù)據(jù)。本文將介紹Go并發(fā)編程中的GMP模型,感興趣的可以了解一下

0. 簡介

傳統(tǒng)的并發(fā)編程模型是基于線程和共享內(nèi)存的同步訪問控制的,共享數(shù)據(jù)受鎖的保護,線程將爭奪這些鎖以訪問數(shù)據(jù)。通常而言,使用線程安全的數(shù)據(jù)結(jié)構(gòu)會使得這更加容易。Go的并發(fā)原語(goroutinechannel)提供了一種優(yōu)雅的方式來構(gòu)建并發(fā)模型。Go鼓勵在goroutine之間使用channel來傳遞數(shù)據(jù),而不是顯式地使用鎖來限制對共享數(shù)據(jù)的訪問。

Do not communicate by sharing memory; instead, share memory by communicating.

這就是Go的并發(fā)哲學,它依賴CSP(Communicating Sequential Processes)模型,它經(jīng)常被認為是 Go 在并發(fā)編程上成功的關鍵因素。

本文將介紹Go并發(fā)編發(fā)編程的的第一個議題:goroutine的實現(xiàn)及其調(diào)度原理。

1. 進程、線程和協(xié)程

進程,是一段程序的執(zhí)行過程,是指令、數(shù)據(jù)及其組織形式的描述,進程是正在執(zhí)行的程序的實例。進程擁有自己的獨立空間。

傳統(tǒng)的操作系統(tǒng)中,每個進程有一個地址空間和至少一個控制線程,這幾乎可以認為是進程的定義。而這個地址空間中,可以存在多個控制線程的情形,這些線程可以理解為輕量級的進程,除了他們共享地址空間。多線程有以下好處:

  • 在許多應用中同時發(fā)生著多種活動,其中某些活動會被阻塞,比如I/O操作,而某些程序則需要響應迅速,比如界面請求,因此多線程的程序設計模型會變得更簡單;
  • 線程比進程更加輕量級,所以其創(chuàng)建、銷毀和上下文切換都更快;
  • 在多CPU的系統(tǒng)中,多線程可以實現(xiàn)真正的并行。

在操作系統(tǒng)中,進程是操作系統(tǒng)資源分配的單位;線程是處理器調(diào)度和執(zhí)行的基本單位。

Linux中的進程和線程

在Linux中,所有的線程都當做進程來實現(xiàn),二者的區(qū)別在于:進程擁有自己的頁表(即地址空間),而線程沒有,只能和同一進程內(nèi)的其他線程共享同一份頁表。這個區(qū)別的根本原因在于二者調(diào)用系統(tǒng)時的傳參不同而已。

在Linux2.3.3開始,glibc的fork()函數(shù)創(chuàng)建進程時是調(diào)用系統(tǒng)調(diào)用clone(2)時指定flagsSIGCHLD(共享信號句柄表)。而pthread_create創(chuàng)建線程時,內(nèi)部也是調(diào)用clone函數(shù),其指定的flags如下:

const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
                            | CLONE_SIGHAND | CLONE_THREAD 
                            | CLONE_SETTLS | CLONE_PARENT_SETTID 
                            | CLONE_CHILD_CLEARTID 
                            | 0);

clone的函數(shù)形式如下:

int clone(int (* fn )(void *), void * stack , int flags , void * arg , ...
                 /* pid_t * parent_tid , void * tls , pid_t * child_tid */ ); 

其實Docker底層實現(xiàn)隔離技術,也利用了clone函數(shù)這一系統(tǒng)調(diào)用。

1.1 線程模型

線程可以分為內(nèi)核線程和用戶線程,用戶線程必須依托于內(nèi)核線程,實現(xiàn)調(diào)度,這樣就帶來了三種線程模型:多對一(M:1)、一對一(1:1)和多對多(M:N)(用戶線程對內(nèi)核線程)。一個用戶線程必須綁定一個內(nèi)核線程才能執(zhí)行,不過CPU并不知道有用戶線程的存在。

1.1.1 多對一用戶級線程模型

這種模型是多個用戶線程對應一個內(nèi)核調(diào)度線程,所有的線程的創(chuàng)建、銷毀和調(diào)度都由用戶空間的線程庫實現(xiàn),內(nèi)核不感知這些線程的切換。優(yōu)點是線程的上下文切換之間不需要陷入內(nèi)核,速度快。缺點是一旦有一個用戶線程有阻塞性的系統(tǒng)調(diào)用,比如I/O操作時,系統(tǒng)內(nèi)核接管后,會阻塞所有的線程。另外,在多處理器的機器上,這種線程模型是沒有意義的,無法發(fā)揮多核系統(tǒng)的優(yōu)勢。

1.1.2 一對一內(nèi)核級線程模型

一對一模型中,每個用戶線程擁有一個對應的內(nèi)核調(diào)度線程,也就是說,內(nèi)核會對每個線程進行調(diào)度。也因此,線程的創(chuàng)建、銷毀和上下文切換,都會陷入到內(nèi)核態(tài)。目前,Linux采用的NPTL(Native POSIX Threads Library)的線程模型就是一對一模型。比如以下例子:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *f(void *arg){
    if (!arg) {
        printf("arg is NULL\n");
    } else {
        printf("%s\n", (char *)arg);
    }

    sleep(100);
    return NULL;
}

int main() {
    pthread_t p1, p2;
    int res;
    char *p2String = "I am p2!";

    // 創(chuàng)建p1線程
    res = pthread_create(&p1, NULL, f, NULL);
    if (res != 0) {
        printf("創(chuàng)建線程1失??!\n");
        return 0;
    }
    printf("創(chuàng)建線程1\n");
    sleep(5);

    // 創(chuàng)建p1線程
    res = pthread_create(&p2, NULL, f, (void *)p2String);
    if (res != 0) {
        printf("創(chuàng)建線程2失敗!\n");
        return 0;
    }
    printf("創(chuàng)建線程2\n");
    sleep(100);

    return 0;
}

在程序中,我們創(chuàng)建了兩個線程,執(zhí)行如下:

$ gcc thread.c -o thread_c -lpthread

$ ./thread_c
創(chuàng)建線程1
arg is NULL
創(chuàng)建線程2
I am p2!

然后查看進程號和此進程下的線程數(shù)。

$ ps -ef | grep thread_c
chenyig+   5293   5087  0 19:02 pts/0    00:00:00 ./thread_c
chenyig+   5459   5347  0 19:03 pts/1    00:00:00 grep --color=auto thread_c

$ cat /proc/5293/status | grep Threads
Threads:    3

之所以線程數(shù)是3,是因為系統(tǒng)啟動進程的時候就自帶一個線程,再加上創(chuàng)建的兩個線程,所以總數(shù)是3,這也證明了Linux的線程模型是1:1的。

1.1.3 多對多兩級線程模型

在多對多模型中,結(jié)合了1:1模型和M:1模型的優(yōu)點,避免了他們的缺點。每個用戶線程擁有多個內(nèi)核調(diào)度線程,也可以多個用戶線程對應一個調(diào)度實體。缺點是線程的調(diào)度需要內(nèi)核態(tài)和用戶態(tài)一起實現(xiàn),導致模型實現(xiàn)十分復雜。NPTL也曾考慮過使用該模型,但是實現(xiàn)太過復雜,需要對內(nèi)核進行大范圍的改動,所以還是采用了1:1模型?,F(xiàn)階段,Go中的協(xié)程goroutine就是采用該模型實現(xiàn)的。

package main

import (
   "fmt"
   "sync"
   "time"
)

func f(i int) {
   fmt.Printf("I am goroutine %d\n", i)
   time.Sleep(100 * time.Second)
}

func main() {
   wg := sync.WaitGroup{}
   for i := 0; i < 100; i++ {
      idx := i
      wg.Add(1)
      go func() {
         defer wg.Done()
         f(idx)
      }()
   }
   wg.Wait()
}

運行后:

$ go build -o thread_go goroutine.go

$ ./thread_go
I am goroutine 7
I am goroutine 4
I am goroutine 0
I am goroutine 6
I am goroutine 1
I am goroutine 2
I am goroutine 9
I am goroutine 3
I am goroutine 5
I am goroutine 8

然后查看進程號和此進程下的線程數(shù)。

$ ps -ef | grep thread_go
chenyig+  69705  67603  0 17:17 pts/0    00:00:00 ./thread_go
chenyig+  69735  68420  0 17:17 pts/2    00:00:00 grep --color=auto thread_go

$ cat /proc/69705/status | grep Threads
Threads:    5

可以看到,用戶線程(goroutine)和內(nèi)核線程并不是一一對應的,而是多對多的情形。

2. GMP模型

Go在2012年正式引入GMP模型,然后在1.2版本中引入了協(xié)作式的搶占式調(diào)度,在1.14版本中實現(xiàn)了基于信號的搶占式調(diào)度,并一直沿用至今。

GMP模型中:

  • G:取Goroutine的首字母,即用戶態(tài)的線程,也叫協(xié)程;
  • M:取Machine的首字母,和內(nèi)核線程一一對應,為簡單理解,我們可以認為其就是內(nèi)核線程;
  • P:取Processor的首字母,表示處理器(可以理解成用戶態(tài)的協(xié)程調(diào)度器),是G和M之間的中間層,負責協(xié)程調(diào)度。

2.1 G

Goroutine是Go語言調(diào)度器中執(zhí)行的任務實體,其在runtime調(diào)度器中的地位與線程在操作系統(tǒng)中的差不多。作為更細粒度的資源調(diào)度單元,和線程相比,其占用更小的內(nèi)存和更低的上下文切換開銷。

Goroutine在運行時的結(jié)構(gòu)體是runtime.g,其結(jié)構(gòu)非常復雜,我們挑選一些重要的字段進行介紹。

type g struct {
   // Stack parameters.
   // stack describes the actual stack memory: [stack.lo, stack.hi).
   // stackguard0 is the stack pointer compared in the Go stack growth prologue.
   // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
   // stackguard1 is the stack pointer compared in the C stack growth prologue.
   // It is stack.lo+StackGuard on g0 and gsignal stacks.
   // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
   stack       stack   // offset known to runtime/cgo
   stackguard0 uintptr // offset known to liblink
   stackguard1 uintptr // offset known to liblink
   ...
}

以上是和Go運行時棧相關的字段,其中stack結(jié)構(gòu)體如下,只有棧頂和棧底的地址。stackguard0是運行用戶協(xié)程g的執(zhí)行棧(go棧)擴張或者收縮的檢查的搶占標記。而stackguard1是用于g0gsignal(這二者后面會介紹)的內(nèi)核棧(C棧)的擴張或者收縮的檢查的搶占標記。

// Stack describes a Go execution stack.
// The bounds of the stack are exactly [lo, hi),
// with no implicit data structures on either side.
type stack struct {
   lo uintptr
   hi uintptr
}

另外,還有以下三個字段和搶占息息相關。

type g struct {
   ...
   preempt       bool // preemption signal, duplicates stackguard0 = stackpreempt
   preemptStop   bool // transition to _Gpreempted on preemption; otherwise, just deschedule
   preemptShrink bool // shrink stack at synchronous safe point
   ...
}

此外,以下字段中,m表示當前協(xié)程占用的線程,可能為空。

type g struct {
   ...
   m         *m      // current m; offset known to arm liblink
   sched     gobuf
   ...
}

sched字段存儲了Goroutine調(diào)度相關的數(shù)據(jù),如下。

type gobuf struct {
   // The offsets of sp, pc, and g are known to (hard-coded in) libmach.
   //
   // ctxt is unusual with respect to GC: it may be a
   // heap-allocated funcval, so GC needs to track it, but it
   // needs to be set and cleared from assembly, where it's
   // difficult to have write barriers. However, ctxt is really a
   // saved, live register, and we only ever exchange it between
   // the real register and the gobuf. Hence, we treat it as a
   // root during stack scanning, which means assembly that saves
   // and restores it doesn't need write barriers. It's still
   // typed as a pointer so that any other writes from Go get
   // write barriers.
   sp   uintptr
   pc   uintptr
   g    guintptr
   ctxt unsafe.Pointer
   ret  uintptr
   lr   uintptr
   bp   uintptr // for framepointer-enabled architectures
}

其中:

  • sp:棧頂指針;
  • pc:程序計數(shù)器;
  • ctxt:函數(shù)閉包的上下文信息,即DX寄存器;
  • bp:棧底指針;

可以看到,goroutine的上下文切換需要保留的寄存器很少,無需保留其他的通用寄存器,至于為啥無需保留,我們留待后續(xù)解釋。

2.2 M

M表示操作系統(tǒng)的線程,Go語言使用私有結(jié)構(gòu)體runtime.m表示操作系統(tǒng)線程,和runtime.g一樣,這個結(jié)構(gòu)體包含了幾十個字段,我們也只挑選一些和我們了解其運行機制的介紹。

type m struct {
   g0      *g     // goroutine with scheduling stack
   ...
   curg          *g       // current running goroutine
   ...
}

其中,g0是持有調(diào)度棧的goroutine,curg是當前線程上運行的用戶goroutine。g0比較特殊,其會深度參與運行時的調(diào)度過程,包括goroutine的創(chuàng)建、大內(nèi)存分配和CGO函數(shù)的執(zhí)行。

另外,在runtime.m中,還有三個與處理器P相關的字段:p、nextpoldp。另外還是tls字段,通過tls實現(xiàn)m結(jié)構(gòu)體對象與工作線程之間的綁定。

type m struct {
   ...
   p             puintptr // attached p for executing go code (nil if not executing go code)
   nextp         puintptr
   oldp          puintptr // the p that was attached before executing a syscall
   ...
   tls           [tlsSlots]uintptr // thread-local storage (for x86 extern register)
   ...
}

2.3 P

處理器P是線程M和協(xié)程G之間的中間層,它能提供線程需要的上下文換環(huán)境,也負責調(diào)度線程上的等待隊列,通過處理器P的調(diào)度,每一個內(nèi)核線程都能執(zhí)行多個goroutine,且在goroutine陷入系統(tǒng)調(diào)用的時候及時讓出計算資源,提高線程的利用率。

因為調(diào)度器在啟動時就會創(chuàng)建GOMAXPROCS個處理器,所以Go語言程序的處理器數(shù)量一定會等于GOMAXPROCS,這些處理器會綁定到不同的內(nèi)核線程上。

type p struct {
   ...
   m           muintptr   // back-link to associated m (nil if idle)
   ...
   // Queue of runnable goroutines. Accessed without lock.
   runqhead uint32
   runqtail uint32
   runq     [256]guintptr
   
   runnext guintptr
   ...
}

以上,runtime.p表示P的私有結(jié)構(gòu),m表示其綁定的線程。runq表示其持有的運行goroutine隊列,最大256,runnext表示下一個要執(zhí)行的goroutine。

以上是GMP中協(xié)程G、線程M和處理器P的私有結(jié)構(gòu)簡介,下面將介紹Go語言調(diào)度器的實現(xiàn)。

3. 基礎調(diào)度過程

上圖簡單描述了GMP模型的工作原理,在用戶態(tài),處理器P將自身的運行隊列中的G交付給線程M執(zhí)行,通過用戶態(tài)的調(diào)度,實現(xiàn)goroutine之間的調(diào)度,每次切換耗費的時間約為~0.2us,低于線程上下文切換的~1us;且每次goroutine的創(chuàng)建,開辟的棧大小為2KB,而線程的創(chuàng)建,都會占用1M以上的內(nèi)存空間。所以說,無論是在時間上還是空間上,用戶態(tài)的goroutine的實現(xiàn)都比內(nèi)核線程的實現(xiàn)要輕量的多。

在圖中,深色G表示線程M正在執(zhí)行的goroutine,而隊列中的淺色G則表示等待執(zhí)行的goroutine隊列。而P的個數(shù)一般設置為CPU的核數(shù),當然用戶可以通過runtime.GOMAXPROCS函數(shù)進行設置。而M的個數(shù)不一定,當在M上執(zhí)行的G陷入內(nèi)核調(diào)用而阻塞時,調(diào)度器會解綁PM,優(yōu)先在空閑M隊列中找到一個M進行執(zhí)行,如果沒有空閑M,則創(chuàng)建一個新的M執(zhí)行剩余隊列中的G,充分利用CPU的資源,所以說M的個數(shù)不一定。

以上就是Golang并發(fā)編程之GMP模型詳解的詳細內(nèi)容,更多關于Golang GMP模型的資料請關注腳本之家其它相關文章!

相關文章

  • 詳解如何使用Bazel構(gòu)建Golang程序

    詳解如何使用Bazel構(gòu)建Golang程序

    這篇文章主要為大家介紹了如何使用Bazel構(gòu)建Golang程序?qū)嵗斀?,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • Golang利用Template模板動態(tài)生成文本

    Golang利用Template模板動態(tài)生成文本

    Go語言中的Go?Template是一種用于生成文本輸出的簡單而強大的模板引擎,它提供了一種靈活的方式來生成各種格式的文本,下面我們就來看看具體如何使用Template實現(xiàn)動態(tài)文本生成吧
    2023-09-09
  • Go語言實現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解

    Go語言實現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解

    這篇文章主要介紹了Go語言實現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解,圖文代碼聲情并茂,有感興趣的可以學習下
    2021-03-03
  • golang中值類型/指針類型的變量區(qū)別總結(jié)

    golang中值類型/指針類型的變量區(qū)別總結(jié)

    golang的值類型和指針類型receiver一直是大家比較混淆的地方,下面這篇文章主要給大家總結(jié)介紹了關于golang中值類型/指針類型的變量區(qū)別的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下。
    2017-12-12
  • Go語言中map使用和并發(fā)安全詳解

    Go語言中map使用和并發(fā)安全詳解

    golang?自帶的map不是并發(fā)安全的,并發(fā)讀寫會報錯,所以下面這篇文章主要給大家介紹了關于Go語言中map使用和并發(fā)安全的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-07-07
  • golang如何通過viper讀取config.yaml文件

    golang如何通過viper讀取config.yaml文件

    這篇文章主要介紹了golang通過viper讀取config.yaml文件,圍繞golang讀取config.yaml文件的相關資料展開詳細內(nèi)容,需要的小伙伴可以參考一下
    2022-03-03
  • golang使用信號量熱更新的實現(xiàn)示例

    golang使用信號量熱更新的實現(xiàn)示例

    這篇文章主要介紹了golang使用信號量熱更新的實現(xiàn)示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-04-04
  • golang結(jié)構(gòu)化日志log/slog包之slog.Record的用法簡介

    golang結(jié)構(gòu)化日志log/slog包之slog.Record的用法簡介

    這篇文章主要為大家詳細介紹了golang結(jié)構(gòu)化日志log/slog包中slog.Record結(jié)構(gòu)體的使用方法和需要注意的點,文中的示例代碼講解詳細,需要的可以學習一下
    2023-10-10
  • Go中defer使用場景及注意事項

    Go中defer使用場景及注意事項

    defer 會在當前函數(shù)返回前執(zhí)行傳入的函數(shù),它會經(jīng)常被用于關閉文件描述符、關閉數(shù)據(jù)庫連接以及解鎖資源。這篇文章主要介紹了Go中defer使用注意事項,需要的朋友可以參考下
    2021-12-12
  • Go語言實現(xiàn)猜謎小游戲

    Go語言實現(xiàn)猜謎小游戲

    這篇文章主要為大家介紹了Go語言實現(xiàn)猜謎小游戲示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01

最新評論