Go全棧面試題(2) -Go進階面試題

title: Go全棧面試題(2) -Go進階面試題
tags: go
author: Clown95


Golang進階面試題

Go的堆棧使用

  • 每個goroutine維護着一個棧空間,默認最大爲4KB.
  • 當goroutine的棧空間不足時,golang會調用runtime.morestack(彙編實現:asm_xxx.s)來進行動態擴容.
  • 連續棧:當棧空間不足的時候申請一個2倍於當前大小的新棧,並把所有數據拷貝到新棧, 接下來的所有調用執行都發生在新棧上.
  • 每個function維護着各自的棧幀(stack frame),當function退出時會釋放棧幀.

Go運行時內存分配的策略

對於小對象(<=32kb),go runtime首先從,Cache開始,然後是Cental,最後Heap。
對於大對象(>32KB),直接從堆中獲取。

  • heap: 全局根對象。負責向操作系統申請內存,管理由垃圾回收器收回的空閒 span 內存塊。
  • central: 從 heap 獲取空閒 span,並按需要將其切分成 object 塊。heap 管理着多個central 對象,每個 central 負責處理一一種等級的內存分配需求。
  • cache: 運行行期,每個 cache 都與某個具體線程相綁定,實現無無鎖內存分配操作。其內部有個以等級爲序號的數組,持有多個切分好的 span 對象。缺少空間時,向等級對應的 central 獲取新的 span 即可。

運行時何時從堆中分配內存,何時從堆棧中分配內存?

  • 對於生命週期僅在堆棧幀內的小對象,將分配棧內存。
  • 對於將通過堆棧幀傳遞的小對象,堆內存。
  • 對於大對象(>32KB),堆內存。
  • 對於可以轉義到堆但實際上是內聯的小對象,棧內存。

Go運行時是否維護Map元素的遍歷順序?

不維護,Go白皮書明確提到映射元素的迭代順序是未定義的。 所以對於同一個映射值,它的一個遍歷過程和下一個遍歷過程中的元素呈現次序不保證是相同的。 對於標準編譯器,映射元素的遍歷順序是隨機的。 如果你需要固定的映射元素遍歷順序,那麼你就需要自己來維護這個順序。

但是請注意:從Go 1.12開始,標準庫包中的各個打印函數的結果中,映射條目總是排了序的。

函數返回局部變量的指針是否安全?

是的,在Go中這是絕對安全的。支持棧的Go編譯器將會對每個局部變量進行逃逸分析。 如果編譯器發現某個局部變量開闢在棧上不是絕對安全的,則此局部變量將被開闢在堆上。

goroutine是怎麼樣調度的?

Golang中調度器的主要有4個重要部分,分別是G、P、M、Sched:

  • G (goroutine) G代表一個goroutine對象,每次go調用的時候,都會創建一個G對象,包括了調用棧,重要的調度信息,例如channel等。
  • P (processor) 銜接M和G的調度上下文,它負責將等待執行的G與M對接。P的數量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的併發度,即有多少個goroutine可以同時運行。
  • M (work thread) 代表一個線程,每次創建一個M的時候,都會有一個底層線程創建;所有的G任務,最終還是在M上執行
  • Sched是調度實現中使用的數據結構,大多數需要的信息都已放在了結構體M、G和P中,Sched結構體只是一個殼。Sched結構體中的Lock是非常必須的,如果M或P等做一些非局部的操作,它們一般需要先鎖住調度器。
    每次go調用的時候,都會:
  1. 創建一個G對象,加入到本地隊列或者全局隊列
  2. 如果還有空閒的P,則創建一個M
  3. M會啓動一個底層線程,循環執行能找到的G任務
  4. G任務的執行順序是,先從本地隊列找,本地沒有則從全局隊列找(一次性轉移(全局G個數/P個數)個,再去其它P中找(一次性轉移一半),
  5. 以上的G任務執行是按照隊列順序(也就是go調用的順序)執行的。

G是順序執行的是不是有點奇怪,跟我們實際瞭解到情況不一樣。原因是,啓動的時候,會專門創建一個線程sysmon,用來監控和管理,在內部是一個循環:

  1. 記錄所有P的G任務計數schedtick,(schedtick會在每執行一個G任務後遞增)
  2. 如果檢查到 schedtick一直沒有遞增,說明這個P一直在執行同一個G任務,如果超過一定的時間(10ms),就在這個G任務的棧信息裏面加一個標記。
  3. 然後這個G任務在執行的時候,如果遇到非內聯函數調用,就會檢查一次這個標記,然後中斷自己,把自己加到隊列末尾,執行下一個G 。
  4. 如果沒有遇到非內聯函數(有時候正常的小函數會被優化成內聯函數)調用的話,那就慘了,會一直執行這個G任務,直到它自己結束;

如何等待所有goroutine的退出?

Go中的goroutines和channel提供了一種優雅而獨特的結構化併發軟件的方法,我們可以利用通道(channel)的特性,來實現當前等待goroutine的操作。但是channel並不是當前這個場景的最佳方案,用它來實現的方式是稍顯笨拙的,需要知道確定個數的goroutine,同時稍不注意就極易產生死鎖,但事實上比較優雅的方式是使用go標準庫sync,其中提供了專門的解決方案sync.WaitGroup用於等待一個goroutines集合的結束。

怎麼限制Goroutine的數量?

在Golang中,Goroutine雖然很好,但是數量太多了,往往會帶來很多麻煩,比如耗盡系統資源導致程序崩潰,或者CPU使用率過高導致系統忙不過來。所以我們可以限制下Goroutine的數量,這樣就需要在每一次執行go之前判斷goroutine的數量,如果數量超了,就要阻塞go的執行。第一時間想到的就是使用通道。每次執行的go之前向通道寫入值,直到通道滿的時候就阻塞了,

package main

import "fmt"

var ch chan  int

func elegance(){
	<-ch
	fmt.Println("the ch value receive",ch)
}

func main(){
	ch = make(chan int,5)
	for i:=0;i<10;i++{
		ch <-1
		fmt.Println("the ch value send",ch)
		go elegance()
		fmt.Println("the result i",i)
	}

}

運行:

> go run goroutine.go 
the ch value send 0xc00009c000
the result i 0
the ch value send 0xc00009c000
the result i 1
the ch value send 0xc00009c000
the result i 2
the ch value send 0xc00009c000
the result i 3
the ch value send 0xc00009c000
the result i 4
the ch value send 0xc00009c000
the result i 5
the ch value send 0xc00009c000
the ch value receive 0xc00009c000
the result i 6
the ch value receive 0xc00009c000
the ch value send 0xc00009c000
the result i 7
the ch value send 0xc00009c000
the result i 8
the ch value send 0xc00009c000
the result i 9
the ch value send 0xc00009c000
the ch value receive 0xc00009c000
the ch value receive 0xc00009c000
the ch value receive 0xc00009c000
the result i 10
the ch value send 0xc00009c000
the result i 11
the ch value send 0xc00009c000
the result i 12
the ch value send 0xc00009c000
the result i 13
the ch value send 0xc00009c000
the ch value receive 0xc00009c000
the ch value receive 0xc00009c000
the ch value receive 0xc00009c000
the ch value receive 0xc00009c000
the result i 14
the ch value receive 0xc00009c000
> go run goroutine.go 
the ch value send 0xc00007e000
the result i 0
the ch value send 0xc00007e000
the result i 1
the ch value send 0xc00007e000
the result i 2
the ch value send 0xc00007e000
the result i 3
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the result i 4
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the result i 5
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the result i 6
the ch value send 0xc00007e000
the result i 7
the ch value send 0xc00007e000
the ch value receive 0xc00007e000
the ch value receive 0xc00007e000
the ch value receive 0xc00007e000
the result i 8
the ch value send 0xc00007e000
the result i 9

這樣每次同時運行的goroutine就被限制爲5個了。但是新的問題於是就出現了,因爲並不是所有的goroutine都執行完了,在main函數退出之後,還有一些goroutine沒有執行完就被強制結束了。這個時候我們就需要用到sync.WaitGroup。使用WaitGroup等待所有的goroutine退出。

package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)
// Pool Goroutine Pool
type Pool struct {
	queue chan int
	wg *sync.WaitGroup
}
// New 新建一個協程池
func NewPool(size int) *Pool{
	if size <=0{
		size = 1
	}
	return &Pool{
		queue:make(chan int,size),
		wg:&sync.WaitGroup{},
	}
}
// Add 新增一個執行
func (p *Pool)Add(delta int){
	// delta爲正數就添加
	for i :=0;i<delta;i++{
		p.queue <-1
	}
	// delta爲負數就減少
	for i:=0;i>delta;i--{
		<-p.queue
	}
	p.wg.Add(delta)
}
// Done 執行完成減一
func (p *Pool) Done(){
	<-p.queue
	p.wg.Done()
}
// Wait 等待Goroutine執行完畢
func (p *Pool) Wait(){
	p.wg.Wait()
}

func main(){
	// 這裏限制5個併發
	pool := NewPool(5)
	fmt.Println("the NumGoroutine begin is:",runtime.NumGoroutine())
	for i:=0;i<20;i++{
		pool.Add(1)
		go func(i int) {
			time.Sleep(time.Second)
			fmt.Println("the NumGoroutine continue is:",runtime.NumGoroutine())
			pool.Done()
		}(i)
	}
	pool.Wait()
	fmt.Println("the NumGoroutine done is:",runtime.NumGoroutine())
}

運行:

the NumGoroutine begin is: 1
the NumGoroutine continue is: 6
the NumGoroutine continue is: 7
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 6
the NumGoroutine continue is: 3
the NumGoroutine continue is: 2
the NumGoroutine done is: 1

其中,Go的GOMAXPROCS默認值已經設置爲CPU的核數, 這裏允許我們的Go程序充分使用機器的每一個CPU,最大程度的提高我們程序的併發性能。runtime.NumGoroutine函數在被調用後,會返回系統中的處於特定狀態的Goroutine的數量。這裏的特指是指Grunnable\Gruning\Gsyscall\Gwaition。處於這些狀態的Groutine即被看做是活躍的或者說正在被調度。

這裏需要注意下:垃圾回收所在Groutine的狀態也處於這個範圍內的話,也會被納入該計數器。

Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量?

Golang中Goroutine 可以通過 Channel 進行安全讀寫共享變量。

Golang 中常用的併發模型?

Golang 中常用的併發模型有三種:

  • 通過channel通知實現併發控制

無緩衝的通道指的是通道的大小爲0,也就是說,這種類型的通道在接收前沒有能力保存任何值,它要求發送 goroutine 和接收 goroutine 同時準備好,纔可以完成發送和接收操作。

從上面無緩衝的通道定義來看,發送 goroutine 和接收 gouroutine 必須是同步的,同時準備後,如果沒有同時準備好的話,先執行的操作就會阻塞等待,直到另一個相對應的操作準備好爲止。這種無緩衝的通道我們也稱之爲同步通道。

func main() {
    ch := make(chan struct{})
    go func() {
        fmt.Println("start working")
        time.Sleep(time.Second * 1)
        ch <- struct{}{}
    }()

    <-ch

    fmt.Println("finished")
}

當主 goroutine 運行到 <-ch 接受 channel 的值的時候,如果該 channel 中沒有數據,就會一直阻塞等待,直到有值。 這樣就可以簡單實現併發控制

  • 通過sync包中的WaitGroup實現併發控制

Goroutine是異步執行的,有的時候爲了防止在結束mian函數的時候結束掉Goroutine,所以需要同步等待,這個時候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它會等待它收集的所有 goroutine 任務全部完成。在WaitGroup裏主要有三個方法:

  • Add, 可以添加或減少 goroutine的數量.
  • Done, 相當於Add(-1).
  • Wait, 執行後會堵塞主線程,直到WaitGroup 裏的值減至0.

在主 goroutine 中 Add(delta int) 索要等待goroutine 的數量。
在每一個 goroutine 完成後 Done() 表示這一個goroutine 已經完成,當所有的 goroutine 都完成後,在主 goroutine 中 WaitGroup 返回返回。

func main(){
    var wg sync.WaitGroup
    var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
    }
    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            defer wg.Done()
            http.Get(url)
        }(url)
    }
    wg.Wait()
}

在Golang官網中對於WaitGroup介紹是A WaitGroup must not be copied after first use,在 WaitGroup 第一次使用後,不能被拷貝

應用示例:

func main(){
 wg := sync.WaitGroup{}
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(wg sync.WaitGroup, i int) {
            fmt.Printf("i:%d", i)
            wg.Done()
        }(wg, i)
    }
    wg.Wait()
    fmt.Println("exit")
}

運行:

i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000094018)
        /home/keke/soft/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc000094010)
        /home/keke/soft/go/src/sync/waitgroup.go:130 +0x64
main.main()
        /home/keke/go/Test/wait.go:17 +0xab
exit status 2

它提示所有的 goroutine 都已經睡眠了,出現了死鎖。這是因爲 wg 給拷貝傳遞到了 goroutine 中,導致只有 Add 操作,其實 Done操作是在 wg 的副本執行的。

因此 Wait 就死鎖了。

這個第一個修改方式:將匿名函數中 wg 的傳入類型改爲 *sync.WaitGrou,這樣就能引用到正確的WaitGroup了。
這個第二個修改方式:將匿名函數中的 wg 的傳入參數去掉,因爲Go支持閉包類型,在匿名函數中可以直接使用外面的 wg 變量

  • 在Go 1.7 以後引進的強大的Context上下文,實現併發控制

通常,在一些簡單場景下使用 channel 和 WaitGroup 已經足夠了,但是當面臨一些複雜多變的網絡併發場景下 channel 和 WaitGroup 顯得有些力不從心了。
比如一個網絡請求 Request,每個 Request 都需要開啓一個 goroutine 做一些事情,這些 goroutine 又可能會開啓其他的 goroutine,比如數據庫和RPC服務。
所以我們需要一種可以跟蹤 goroutine 的方案,纔可以達到控制他們的目的,這就是Golang爲我們提供的 Context,稱之爲上下文非常貼切,它就是goroutine 的上下文。
它是包括一個程序的運行環境、現場和快照等。每個程序要運行時,都需要知道當前程序的運行狀態,通常Go 將這些封裝在一個 Context 裏,再將它傳給要執行的 goroutine 。

context 包主要是用來處理多個 goroutine 之間共享數據,及多個 goroutine 的管理。

context 包的核心是 struct Context,接口聲明如下:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this `Context` is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this Context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Done() 返回一個只能接受數據的channel類型,當該context關閉或者超時時間到了的時候,該channel就會有一個取消信號

Err() 在Done() 之後,返回context 取消的原因。

Deadline() 設置該context cancel的時間點

Value() 方法允許 Context 對象攜帶request作用域的數據,該數據必須是線程安全的。

Context 對象是線程安全的,你可以把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行 取消 操作時,所有 goroutine 都會接收到取消信號。

一個 Context 不能擁有 Cancel 方法,同時我們也只能 Done channel 接收數據。
其中的原因是一致的:接收取消信號的函數和發送信號的函數通常不是一個。
典型的場景是:父操作爲子操作操作啓動 goroutine,子操作也就不能取消父操作。

Goroutine和Channel的作用分別是什麼?

進程是內存資源管理和cpu調度的執行單元。爲了有效利用多核處理器的優勢,將進程進一步細分,允許一個進程裏存在多個線程,這多個線程還是共享同一片內存空間,但cpu調度的最小單元變成了線程。

那協程又是什麼呢,以及與線程的差異性??

協程,可以看作是輕量級的線程。但與線程不同的是,線程的切換是由操作系統控制的,而協程的切換則是由用戶控制的。

最早支持協程的程序語言應該是lisp方言scheme裏的continuation(續延),續延允許scheme保存任意函數調用的現場,保存起來並重新執行。Lua,C#,python等語言也有自己的協程實現。

Go中的goroutinue就是協程,可以實現並行,多個協程可以在多個處理器同時跑。而協程同一時刻只能在一個處理器上跑(可以把宿主語言想象成單線程的就好了)。
然而,多個goroutine之間的通信是通過channel,而協程的通信是通過yield和resume()操作。

goroutine非常簡單,只需要在函數的調用前面加關鍵字go即可,例如:

go elegance()

我們也可以啓動5個goroutines分別打印索引。

func main() {
	for i:=1;i<5;i++ {
		go func(i int) {
			fmt.Println(i)
		}(i)
	}
	// 停歇5s,保證打印全部結束
	time.Sleep(5*time.Second)
}

在分析goroutine執行的隨機性和併發性,啓動了5個goroutine,再加上main函數的主goroutine,總共有6個goroutines。由於goroutine類似於”守護線程“,異步執行的,如果主goroutine不等待片刻,可能程序就沒有輸出打印了。

在Golang中channel則是goroutinues之間進行通信的渠道。

可以把channel形象比喻爲工廠裏的傳送帶,一頭的生產者goroutine往傳輸帶放東西,另一頭的消費者goroutinue則從輸送帶取東西。channel實際上是一個有類型的消息隊列,遵循先進先出的特點。

  1. channel的操作符號

ch <- data 表示data被髮送給channel ch;

data <- ch 表示從channel ch取一個值,然後賦給data。

  1. 阻塞式channel

channel默認是沒有緩衝區的,也就是說,通信是阻塞的。send操作必須等到有消費者accept纔算完成。

應用示例:

func main() {
	ch1 := make(chan int)
	go pump(ch1) // pump hangs
	fmt.Println(<-ch1) // prints only 1
}

func pump(ch chan int) {
	for i:= 1; ; i++ {
		ch <- i
	}
}

在函數pump()裏的channel在接受到第一個元素後就被阻塞了,直到主goroutinue取走了數據。最終channel阻塞在接受第二個元素,程序只打印 1。

沒有緩衝(buffer)的channel只能容納一個元素,而帶有緩衝(buffer)channel則可以非阻塞容納N個元素。發送數據到緩衝(buffer) channel不會被阻塞,除非channel已滿;同樣的,從緩衝(buffer) channel取數據也不會被阻塞,除非channel空了。

怎麼查看Goroutine的數量?

在Golang中,GOMAXPROCS中控制的是未被阻塞的所有Goroutine,可以被Multiplex到多少個線程上運行,通過GOMAXPROCS可以查看Goroutine的數量。

如何測試代碼是否有goroutine泄漏的?

使用runtime.Stack在測試代碼運行前後計算goroutine數量,當然我理解測試代碼運行完成之後是會觸發gc的。如果觸發gc之後,發現還有goroutine沒有被回收,那麼這個goroutine很有可能是被泄漏的。

堆棧將調用goroutine的堆棧跟蹤格式化爲buf 並返回寫入buf的字節數。如果全部爲真,則在當前goroutine的跟蹤之後,Stack格式化所有其他goroutine的跟蹤到buf中。
func Stack(buf []byte, all bool) int {
	if all {
		stopTheWorld("stack trace")
	}

	n := 0
	if len(buf) > 0 {
		gp := getg()
		sp := getcallersp()
		pc := getcallerpc()
		systemstack(func() {
			g0 := getg()
			// Force traceback=1 to override GOTRACEBACK setting,
			// so that Stack's results are consistent.
			// GOTRACEBACK is only about crash dumps.
			g0.m.traceback = 1
			g0.writebuf = buf[0:0:len(buf)]
			goroutineheader(gp)
			traceback(pc, sp, 0, gp)
			if all {
				tracebackothers(gp)
			}
			g0.m.traceback = 0
			n = len(g0.writebuf)
			g0.writebuf = nil
		})
	}

	if all {
		startTheWorld()
	}
	return n
}

如何使map能夠安全的併發?

map併發讀寫是不安全的。map屬於引用類型,併發讀寫時多個協程見是通過指針訪問同一個地址,即訪問共享變量,此時同時讀寫資源存在競爭關係。利用讀寫鎖可實現對map的安全訪問。

實現阻塞讀且併發安全的map

GO裏面MAP如何實現key不存在 get操作等待 直到key存在或者超時,保證併發安全,且需要實現以下接口:

type sp interface {
    Out(key string, val interface{})  //存入key /val,如果該key讀取的goroutine掛起,則喚醒。此方法不會阻塞,時刻都可以立即執行並返回
    Rd(key string, timeout time.Duration) interface{}  //讀取一個key,如果key不存在阻塞,等待key存在或者超時
}

解析:

看到阻塞協程第一個想到的就是channel,題目中要求併發安全,那麼必須用鎖,還要實現多個goroutine讀的時候如果值不存在則阻塞,直到寫入值,那麼每個鍵值需要有一個阻塞goroutine 的 channel。

實現如下:

type Map struct {
    c   map[string]*entry
    rmx *sync.RWMutex
}
type entry struct {
    ch      chan struct{}
    value   interface{}
    isExist bool
}

func (m *Map) Out(key string, val interface{}) {
    m.rmx.Lock()
    defer m.rmx.Unlock()
    if e, ok := m.c[key]; ok {
        e.value = val
        e.isExist = true
        close(e.ch)
    } else {
        e = &entry{ch: make(chan struct{}), isExist: true,value:val}
        m.c[key] = e
        close(e.ch)
    }
}

func (m *Map) Rd(key string, timeout time.Duration) interface{} {
    m.rmx.Lock()
    if e, ok := m.c[key]; ok && e.isExist {
        m.rmx.Unlock()
        return e.value
    } else if !ok {
        e = &entry{ch: make(chan struct{}), isExist: false}
        m.c[key] = e
        m.rmx.Unlock()
        fmt.Println("協程阻塞 -> ", key)
        select {
        case <-e.ch:
            return e.value
        case <-time.After(timeout):
            fmt.Println("協程超時 -> ", key)
            return nil
        }
    } else {
        m.rmx.Unlock()
        fmt.Println("協程阻塞 -> ", key)
        select {
        case <-e.ch:
            return e.value
        case <-time.After(timeout):
            fmt.Println("協程超時 -> ", key)
            return nil
        }
    }
}

主協程如何等其餘協程完再操作?

  • 使用管道實現同步,協程結束後發送信號給主協程。
  • 最理想的方案是使用sync.WaitGroup,WaitGroup內部實現了一個計數器,用來記錄未完成的操作個數,它提供了三個方法,Add()用來添加計數。Done()用來在操作結束時調用,使計數減一。Wait()用來等待所有的操作結束,即計數變爲0,該函數會在計數不爲0時等待,在計數爲0時立即返回。

簡述go語言的CSP併發模型

Actor模型是多線程通過共享內存來通信,它是"通過內存共享來進行通訊",CSP不同於Actor模型,它講究的是“以通信的方式來共享內存”。GSP用於描述兩個獨立的併發實體通過共享的通訊 channel(管道)進行通信的併發模型。
Golang中使用 CSP中 channel 這個概念。channel 是被單獨創建並且可以在進程之間傳遞,它的通信模式類似於 boss-worker 模式的,一個實體通過將消息發送到channel 中,然後又監聽這個 channel 的實體處理,兩個實體之間是匿名的,這個就實現實體中間的解耦,其中 channel 是同步的一個消息被髮送到 channel 中,最終是一定要被另外的實體消費掉的,在實現原理上其實是一個阻塞的消息隊列。
Golang的CSP併發模型,是通過Goroutine和Channel來實現的。Goroutine 是Golang中併發的執行單位。有點抽象,其實就是和傳統概念上的”線程“類似,可以理解爲”線程“。 Channel是Golang中各個併發結構體(Goroutine)之前的通信機制。通常Channel,是各個Goroutine之間通信的”管道“,有點類似於Linux中的管道。
通信機制channel也很方便,傳數據用channel <- data,取數據用<-channel。
在通信過程中,傳數據channel <- data和取數據<-channel必然會成對出現,因爲這邊傳,那邊取,兩個goroutine之間纔會實現通信。
而且不管傳還是取,必阻塞,直到另外的goroutine傳或者取爲止。

context包的用途是什麼?

context包的主要用途是用於控制併發,可以有效的避免goroutine泄露。在實際的業務種,我們可能會有這麼一種場景:需要我們主動的通知某一個goroutine結束。比如我們開啓一個後臺goroutine一直做事情,比如監控,現在不需要了,就需要通知這個監控goroutine結束,不然它會一直跑,就泄漏了。所以我們需要一種可以跟蹤goroutine的方案,纔可以達到控制他們的目的,這就是Golang爲我們提供的Context,稱之爲上下文非常貼切,它就是goroutine的上下文。

Context 使用原則

  • 不要把 Context 放在結構體中,要以參數的方式傳遞
  • 以 Context 作爲參數的函數方法,應該把 Context 作爲第一個參數,放在第一位。
  • 給一個函數方法傳遞 Context 的時候,不要傳遞 nil,如果不知道傳遞什麼,就使用 context.TODO
  • Context 的 Value 相關方法應該傳遞必須的數據,不要什麼數據都使用這個傳遞
  • Context 是線程安全的,可以放心的在多個 goroutine 中傳遞

互斥鎖,讀寫鎖,死鎖問題是怎麼解決。

  • 互斥鎖

互斥鎖就是互斥變量mutex,用來鎖住臨界區的.

條件鎖就是條件變量,當進程的某些資源要求不滿足時就進入休眠,也就是鎖住了。當資源被分配到了,條件鎖打開,進程繼續運行;讀寫鎖,也類似,用於緩衝區等臨界資源能互斥訪問的。

  • 讀寫鎖

通常有些公共數據修改的機會很少,但其讀的機會很多。並且在讀的過程中會伴隨着查找,給這種代碼加鎖會降低我們的程序效率。讀寫鎖可以解決這個問題。

注意:寫獨佔,讀共享,寫鎖優先級高

  • 死鎖

一般情況下,如果同一個線程先後兩次調用lock,在第二次調用時,由於鎖已經被佔用,該線程會掛起等待別的線程釋放鎖,然而鎖正是被自己佔用着的,該線程又被掛起而沒有機會釋放鎖,因此就永遠處於掛起等待狀態了,這叫做死鎖(Deadlock)。
另外一種情況是:若線程A獲得了鎖1,線程B獲得了鎖2,這時線程A調用lock試圖獲得鎖2,結果是需要掛起等待線程B釋放鎖2,而這時線程B也調用lock試圖獲得鎖1,結果是需要掛起等待線程A釋放鎖1,於是線程A和B都永遠處於掛起狀態了。

死鎖產生的四個必要條件:

  1. 互斥條件:一個資源每次只能被一個進程使用
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
  4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。
    這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。

預防死鎖

可以把資源一次性分配:(破壞請求和保持條件)

然後剝奪資源:即當某進程新的資源未滿足時,釋放已佔有的資源(破壞不可剝奪條件)

資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)

避免死鎖

預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統性能。由於在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全狀態,則將資源分配給進程;否則,進程等待。其中最具有代表性的避免死鎖算法是銀行家算法。

檢測死鎖

首先爲每個進程和每個資源指定一個唯一的號碼,然後建立資源分配表和進程等待表.

解除死鎖

當發現有進程死鎖後,便應立即把它從死鎖狀態中解脫出來,常採用的方法有.

  • 剝奪資源
    從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態.
  • 撤消進程
    可以直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除爲止.所謂代價是指優先級、運行代價、進程的重要性和價值等。

說下Go中的鎖有哪些?

Go中的三種鎖包括:互斥鎖,讀寫鎖,sync.Map的安全的鎖.

  • 互斥鎖

Go併發程序對共享資源進行訪問控制的主要手段,由標準庫代碼包中sync中的Mutex結構體表示。

//Mutex 是互斥鎖, 零值是解鎖的互斥鎖, 首次使用後不得複製互斥鎖。
type Mutex struct {
   state int32
   sema  uint32
}

sync.Mutex包中的類型只有兩個公開的指針方法Lock和Unlock。

//Locker表示可以鎖定和解鎖的對象。
type Locker interface {
   Lock()
   Unlock()
}

//鎖定當前的互斥量
//如果鎖已被使用,則調用goroutine
//阻塞直到互斥鎖可用。
func (m *Mutex) Lock() 

//對當前互斥量進行解鎖
//如果在進入解鎖時未鎖定m,則爲運行時錯誤。
//鎖定的互斥鎖與特定的goroutine無關。
//允許一個goroutine鎖定Mutex然後安排另一個goroutine來解鎖它。
func (m *Mutex) Unlock()

聲明一個互斥鎖:

var mutex sync.Mutex

不像C或Java的鎖類工具,我們可能會犯一個錯誤:忘記及時解開已被鎖住的鎖,從而導致流程異常。但Go由於存在defer,所以此類問題出現的概率極低。關於defer解鎖的方式如下:

var mutex sync.Mutex
func Write()  {
   mutex.Lock()
   defer mutex.Unlock()
}

如果對一個已經上鎖的對象再次上鎖,那麼就會導致該鎖定操作被阻塞,直到該互斥鎖回到被解鎖狀態.

fpackage main

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

func main() {

	var mutex sync.Mutex
	fmt.Println("begin lock")
	mutex.Lock()
	fmt.Println("get locked")
	for i := 1; i <= 3; i++ {
		go func(i int) {
			fmt.Println("begin lock ", i)
			mutex.Lock()
			fmt.Println("get locked ", i)
		}(i)
	}

	time.Sleep(time.Second)
	fmt.Println("Unlock the lock")
	mutex.Unlock()
	fmt.Println("get unlocked")
	time.Sleep(time.Second)
}

我們在for循環之前開始加鎖,然後在每一次循環中創建一個協程,並對其加鎖,但是由於之前已經加鎖了,所以這個for循環中的加鎖會陷入阻塞直到main中的鎖被解鎖, time.Sleep(time.Second) 是爲了能讓系統有足夠的時間運行for循環,輸出結果如下:

> go run mutex.go 
begin lock
get locked
begin lock  3
begin lock  1
begin lock  2
Unlock the lock
get unlocked
get locked  3

這裏可以看到解鎖後,三個協程會重新搶奪互斥鎖權,最終協程3獲勝。

互斥鎖鎖定操作的逆操作並不會導致協程阻塞,但是有可能導致引發一個無法恢復的運行時的panic,比如對一個未鎖定的互斥鎖進行解鎖時就會發生panic。避免這種情況的最有效方式就是使用defer。

我們知道如果遇到panic,可以使用recover方法進行恢復,但是如果對重複解鎖互斥鎖引發的panic卻是無用的(Go 1.8及以後)。

package main

import (
	"fmt"
	"sync"
)

func main() {
	defer func() {
		fmt.Println("Try to recover the panic")
		if p := recover(); p != nil {
			fmt.Println("recover the panic : ", p)
		}
	}()
	var mutex sync.Mutex
	fmt.Println("begin lock")
	mutex.Lock()
	fmt.Println("get locked")
	fmt.Println("unlock lock")
	mutex.Unlock()
	fmt.Println("lock is unlocked")
	fmt.Println("unlock lock again")
	mutex.Unlock()
}

運行:

> go run mutex.go 
begin lock
get locked
unlock lock
lock is unlocked
unlock lock again
fatal error: sync: unlock of unlocked mutex

goroutine 1 [running]:
runtime.throw(0x4bc1a8, 0x1e)
        /home/keke/soft/go/src/runtime/panic.go:617 +0x72 fp=0xc000084ea8 sp=0xc000084e78 pc=0x427ba2
sync.throw(0x4bc1a8, 0x1e)
        /home/keke/soft/go/src/runtime/panic.go:603 +0x35 fp=0xc000084ec8 sp=0xc000084ea8 pc=0x427b25
sync.(*Mutex).Unlock(0xc00001a0c8)
        /home/keke/soft/go/src/sync/mutex.go:184 +0xc1 fp=0xc000084ef0 sp=0xc000084ec8 pc=0x45f821
main.main()
        /home/keke/go/Test/mutex.go:25 +0x25f fp=0xc000084f98 sp=0xc000084ef0 pc=0x486c1f
runtime.main()
        /home/keke/soft/go/src/runtime/proc.go:200 +0x20c fp=0xc000084fe0 sp=0xc000084f98 pc=0x4294ec
runtime.goexit()
        /home/keke/soft/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc000084fe8 sp=0xc000084fe0 pc=0x450ad1
exit status 2

這裏試圖對重複解鎖引發的panic進行recover,但是我們發現操作失敗,雖然互斥鎖可以被多個協程共享,但還是建議將對同一個互斥鎖的加鎖解鎖操作放在同一個層次的代碼中。

  • 讀寫鎖

讀寫鎖是針對讀寫操作的互斥鎖,可以分別針對讀操作與寫操作進行鎖定和解鎖操作 。

讀寫鎖的訪問控制規則如下:

① 多個寫操作之間是互斥的
② 寫操作與讀操作之間也是互斥的
③ 多個讀操作之間不是互斥的

在這樣的控制規則下,讀寫鎖可以大大降低性能損耗。

在Go的標準庫代碼包中sync中的RWMutex結構體表示爲:

// RWMutex是一個讀/寫互斥鎖,可以由任意數量的讀操作或單個寫操作持有。
// RWMutex的零值是未鎖定的互斥鎖。
//首次使用後,不得複製RWMutex。
//如果goroutine持有RWMutex進行讀取而另一個goroutine可能會調用Lock,那麼在釋放初始讀鎖之前,goroutine不應該期望能夠獲取讀鎖定。 
//特別是,這種禁止遞歸讀鎖定。 這是爲了確保鎖最終變得可用; 阻止的鎖定會阻止新讀操作獲取鎖定。
type RWMutex struct {
   w           Mutex  //如果有待處理的寫操作就持有
   writerSem   uint32 // 寫操作等待讀操作完成的信號量
   readerSem   uint32 //讀操作等待寫操作完成的信號量
   readerCount int32  // 待處理的讀操作數量
   readerWait  int32  // number of departing readers
}

sync中的RWMutex有以下幾種方法:

//對讀操作的鎖定
func (rw *RWMutex) RLock()
//對讀操作的解鎖
func (rw *RWMutex) RUnlock()
//對寫操作的鎖定
func (rw *RWMutex) Lock()
//對寫操作的解鎖
func (rw *RWMutex) Unlock()

//返回一個實現了sync.Locker接口類型的值,實際上是回調rw.RLock and rw.RUnlock.
func (rw *RWMutex) RLocker() Locker

Unlock方法會試圖喚醒所有想進行讀鎖定而被阻塞的協程,而 RUnlock方法只會在已無任何讀鎖定的情況下,試圖喚醒一個因欲進行寫鎖定而被阻塞的協程。若對一個未被寫鎖定的讀寫鎖進行寫解鎖,就會引發一個不可恢復的panic,同理對一個未被讀鎖定的讀寫鎖進行讀寫鎖也會如此。

由於讀寫鎖控制下的多個讀操作之間不是互斥的,因此對於讀解鎖更容易被忽視。對於同一個讀寫鎖,添加多少個讀鎖定,就必要有等量的讀解鎖,這樣才能其他協程有機會進行操作。

package main

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

func main() {
	var rwm sync.RWMutex
	for i := 0; i < 5; i++ {
		go func(i int) {
			fmt.Println("try to lock read ", i)
			rwm.RLock()
			fmt.Println("get locked ", i)
			time.Sleep(time.Second * 2)
			fmt.Println("try to unlock for reading ", i)
			rwm.RUnlock()
			fmt.Println("unlocked for reading ", i)
		}(i)
	}
	time.Sleep(time.Millisecond * 1000)
	fmt.Println("try to lock for writing")
	rwm.Lock()
	fmt.Println("locked for writing")
}

運行:

> go run rwmutex.go 
try to lock read  0
get locked  0
try to lock read  4
get locked  4
try to lock read  3
get locked  3
try to lock read  1
get locked  1
try to lock read  2
get locked  2
try to lock for writing
try to unlock for reading  0
unlocked for reading  0
try to unlock for reading  2
unlocked for reading  2
try to unlock for reading  1
unlocked for reading  1
try to unlock for reading  3
unlocked for reading  3
try to unlock for reading  4
unlocked for reading  4
locked for writing

這裏可以看到創建了五個協程用於對讀寫鎖的讀鎖定與讀解鎖操作。在 rwm.Lock()種會對main中協程進行寫鎖定,但是for循環中的讀解鎖尚未完成,因此會造成mian中的協程阻塞。當for循環中的讀解鎖操作都完成後就會試圖喚醒main中阻塞的協程,main中的寫鎖定纔會完成。

  • sync.Map安全鎖

golang中的sync.Map是併發安全的,其實也就是sync包中golang自定義的一個名叫Map的結構體。

應用示例:

package main
import (
    "sync"
    "fmt"
)

func main() {
    //開箱即用
    var sm sync.Map
    //store 方法,添加元素
    sm.Store(1,"a")
    //Load 方法,獲得value
    if v,ok:=sm.Load(1);ok{
        fmt.Println(v)
    }
    //LoadOrStore方法,獲取或者保存
    //參數是一對key:value,如果該key存在且沒有被標記刪除則返回原先的value(不更新)和true;不存在則store,返回該value 和false
    if vv,ok:=sm.LoadOrStore(1,"c");ok{
        fmt.Println(vv)
    }
    if vv,ok:=sm.LoadOrStore(2,"c");!ok{
        fmt.Println(vv)
    }
    //遍歷該map,參數是個函數,該函數參的兩個參數是遍歷獲得的key和value,返回一個bool值,當返回false時,遍歷立刻結束。
    sm.Range(func(k,v interface{})bool{
        fmt.Print(k)
        fmt.Print(":")
        fmt.Print(v)
        fmt.Println()
        return true
    })
}

運行 :

a
a
c
1:a
2:c

sync.Map的數據結構:

 type Map struct {
    // 該鎖用來保護dirty
    mu Mutex
    // 存讀的數據,因爲是atomic.value類型,只讀類型,所以它的讀是併發安全的
    read atomic.Value // readOnly
    //包含最新的寫入的數據,並且在寫的時候,會把read 中未被刪除的數據拷貝到該dirty中,因爲是普通的map存在併發安全問題,需要用到上面的mu字段。
    dirty map[interface{}]*entry
    // 從read讀數據的時候,會將該字段+1,當等於len(dirty)的時候,會將dirty拷貝到read中(從而提升讀的性能)。
    misses int
}

read的數據結構是:

type readOnly struct {
    m  map[interface{}]*entry
    // 如果Map.dirty的數據和m 中的數據不一樣是爲true
    amended bool 
}

entry的數據結構:

type entry struct {
    //可見value是個指針類型,雖然read和dirty存在冗餘情況(amended=false),但是由於是指針類型,存儲的空間應該不是問題
    p unsafe.Pointer // *interface{}
}

Delete 方法:

func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    //如果read中沒有,並且dirty中有新元素,那麼就去dirty中去找
    if !ok && read.amended {
        m.mu.Lock()
        //這是雙檢查(上面的if判斷和鎖不是一個原子性操作)
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            //直接刪除
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    if ok {
    //如果read中存在該key,則將該value 賦值nil(採用標記的方式刪除!)
        e.delete()
    }
}

func (e *entry) delete() (hadValue bool) {
    for {
        p := atomic.LoadPointer(&e.p)
        if p == nil || p == expunged {
            return false
        }
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return true
        }
    }
}

Store 方法:

func (m *Map) Store(key, value interface{}) {
    // 如果m.read存在這個key,並且沒有被標記刪除,則嘗試更新。
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    // 如果read不存在或者已經被標記刪除
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
    //如果entry被標記expunge,則表明dirty沒有key,可添加入dirty,並更新entry
        if e.unexpungeLocked() { 
            //加入dirty中
            m.dirty[key] = e
        }
        //更新value值
        e.storeLocked(&value) 
        //dirty 存在該key,更新
    } else if e, ok := m.dirty[key]; ok { 
        e.storeLocked(&value)
        //read 和dirty都沒有,新添加一條
    } else {
     //dirty中沒有新的數據,往dirty中增加第一個新鍵
        if !read.amended { 
            //將read中未刪除的數據加入到dirty中
            m.dirtyLocked() 
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value) 
    }
    m.mu.Unlock()
}

//將read中未刪除的數據加入到dirty中
func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }
    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[interface{}]*entry, len(read.m))
    //read如果較大的話,可能影響性能
    for k, e := range read.m {
    //通過此次操作,dirty中的元素都是未被刪除的,可見expunge的元素不在dirty中
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

//判斷entry是否被標記刪除,並且將標記爲nil的entry更新標記爲expunge
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        // 將已經刪除標記爲nil的數據標記爲expunged
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}

//對entry 嘗試更新
func (e *entry) tryStore(i *interface{}) bool {
    p := atomic.LoadPointer(&e.p)
    if p == expunged {
        return false
    }
    for {
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
        if p == expunged {
            return false
        }
    }
}

//read裏 將標記爲expunge的更新爲nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

//更新entry
func (e *entry) storeLocked(i *interface{}) {
    atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

因此,每次操作先檢查read,因爲read 併發安全,性能好些;read不滿足,則加鎖檢查dirty,一旦是新的鍵值,dirty會被read更新。

Load方法:

Load方法是一個加載方法,查找key。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    //因read只讀,線程安全,先查看是否滿足條件
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    //如果read沒有,並且dirty有新數據,那從dirty中查找,由於dirty是普通map,線程不安全,這個時候用到互斥鎖了
    if !ok && read.amended {
        m.mu.Lock()
        // 雙重檢查
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        // 如果read中還是不存在,並且dirty中有新數據
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // mssLocked()函數是性能是sync.Map 性能得以保證的重要函數,目的講有鎖的dirty數據,替換到只讀線程安全的read裏
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}

//dirty 提升至read 關鍵函數,當misses 經過多次因爲load之後,大小等於len(dirty)時候,講dirty替換到read裏,以此達到性能提升。
func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    //原子操作,耗時很小
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

sync.Map是通過冗餘的兩個數據結構(read、dirty),實現性能的提升。爲了提升性能,load、delete、store等操作儘量使用只讀的read;爲了提高read的key擊中概率,採用動態調整,將dirty數據提升爲read;對於數據的刪除,採用延遲標記刪除法,只有在提升dirty的時候才刪除。

死鎖條件,如何避免?

死鎖產生的四個必要條件:

  1. 互斥條件:一個資源每次只能被一個進程使用
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
  4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。
    這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
  • 預防死鎖

可以把資源一次性分配:(破壞請求和保持條件)

然後剝奪資源:即當某進程新的資源未滿足時,釋放已佔有的資源(破壞不可剝奪條件)

資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)

  • 避免死鎖
    預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統性能。由於在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全狀態,則將資源分配給進程;否則,進程等待。其中最具有代表性的避免死鎖算法是銀行家算法。

常見的GC模式

  • 引用計數(reference counting)每個對象維護一個引用計數器,當引用該對象的對象被銷燬或者更新的時候,被引用對象的引用計數器自動減 1,當被應用的對象被創建,或者賦值給其他對象時,引用 +1,引用爲 0 的時候回收,思路簡單,但是頻繁更新引用計數器降低性能,存在循環以引用(php,Python所使用的)

  • 標記清除(mark and sweep)就是 golang 所使用的,從根變量來時遍歷所有被引用對象,標記之後進行清除操作,對未標記對象進行回收,缺點:每次垃圾回收的時候都會暫停所有的正常運行的代碼,系統的響應能力會大大降低,各種 mark&swamp 變種(三色標記法),緩解性能問題。

  • 分代蒐集(generation)jvm 就使用的分代回收的思路。在面向對象編程語言中,絕大多數對象的生命週期都非常短。分代收集的基本思想是,將堆劃分爲兩個或多個稱爲代(generation)的空間。新創建的對象存放在稱爲新生代(young generation)中(一般來說,新生代的大小會比 老年代小很多),隨着垃圾回收的重複執行,生命週期較長的對象會被提升(promotion)到老年代中(這裏用到了一個分類的思路,這個是也是科學思考的一個基本思路)。

Go 觸發GC機制

  1. 在申請內存的時候,檢查當前當前已分配的內存是否大於上次GC後的內存的2倍,若是則觸發(主GC線程爲當前M)

  2. 監控線程發現上次GC的時間已經超過兩分鐘了,觸發;將一個G任務放到全局G隊列中去。(主GC線程爲執行這個G任務的M)

Golang GC 時會發生什麼?

Golang 1.5後,採取的是“非分代的、非移動的、併發的、三色的”標記清除垃圾回收算法。
golang 中的 gc 基本上是標記清除的過程:

gc的過程一共分爲四個階段:

  1. 棧掃描(開始時STW(Stop the world))
  2. 第一次標記(併發)
  3. 第二次標記(STW)
  4. 清除(併發)

整個進程空間裏申請每個對象佔據的內存可以視爲一個圖,初始狀態下每個內存對象都是白色標記。

  1. 先STW,做一些準備工作,比如 enable write barrier。然後取消STW,將掃描任務作爲多個併發的goroutine立即入隊給調度器,進而被CPU處理
  2. 第一輪先掃描root對象,包括全局指針和 goroutine 棧上的指針,標記爲灰色放入隊列
  3. 第二輪將第一步隊列中的對象引用的對象置爲灰色加入隊列,一個對象引用的所有對象都置灰並加入隊列後,這個對象才能置爲黑色並從隊列之中取出。循環往復,最後隊列爲空時,整個圖剩下的白色內存空間即不可到達的對象,即沒有被引用的對象;
  4. 第三輪再次STW,將第二輪過程中新增對象申請的內存進行標記(灰色),這裏使用了write barrier(寫屏障)去記錄

Golang gc 優化的核心就是儘量使得 STW(Stop The World) 的時間越來越短。

怎麼設計orm,讓你寫,你會怎麼寫?

對象關係映射(Object Relational Mapping,簡稱ORM),是一種程序技術,用於實現面向對象編程語言裏不同類型系統的數據之間的轉換。從效果上說,它其實是創建了一個可在編程語言裏使用的–“虛擬對象數據庫”。

通常來講,ORM就是將數據庫中的表映射成一個對象實體A,對A進行操作,就相對對數據庫進行操作,完成這個過程,其實只要你好好想想你是怎麼操作數據庫的,然後將類似的行爲換成對象即可。

要設計一個ORM,我們需要幾步操作:

  1. 先準備好一個對象A和數據庫中某張表對應T(A->T)

  2. 我們知道當你創建一個表時,一般使用create命令如下:

CREATE TABLE database_name.table_name( 
column1 datatype PRIMARY KEY(one or more columns), 
column2 datatype, 
column3 datatype,.. 
columnN datatype, 
);

從這裏我們可以看就對應着表名,表字段名,字段類型,是否是主鍵等,此時我們需要如何根據A中成員變量,而知道T的這些內容?如果你能根據A能夠轉化成T,那此時你就已經將A映射到了T了.

  1. 映射過程完成後,接下來就是要具備表的四種操作:增,刪,改,查.
SELECT * FROM T WHERE field1 >= ? OR field2 >=;

這個過程其實是構建where語句的過程,我們可以根據一些條件,構建where語句,然後映射到成一個sql語句,根據sql語句我們就可以查詢到一組符合條件的數據(cursor),然後就是將cursor數據轉化成A.

現在總結下第三步操作的兩個過程:condition ->sql語句;cursor reslut->A

完成這三部,基本上就完成了一個ORM的設計,如果後面需要對性能,細節進行優化,就可以慢慢來。畢竟主功能已具備。

注意:如果想線程安全進行數據庫操作可以考慮  db.enableWriteAheadLogging();

設計orm過程就是:

  • 根據A得到 T;
  • 根據condition構建where,拼接成sql;
  • 根據sql從T中查出cursors;
  • cursor轉化成A.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章