雖然這不是一次正經(jīng)的面試僅僅是和大佬聊了聊但作為第一次go的面試我覺(jué)得還是有必要記錄一下,問(wèn)到的問(wèn)題不會(huì)全寫(xiě)只會(huì)羅列出我認(rèn)為比較重要的內(nèi)容也算是一種復(fù)習(xí)吧。
一.channel的幾種關(guān)閉方式?
這篇文章完美的回答了這個(gè)問(wèn)題 http://www.itdecent.cn/p/d24dfbb33781 (膜拜大佬了 ~)
簡(jiǎn)單總結(jié)一下(一定要遵循的原則:1.不要在關(guān)閉已經(jīng)關(guān)閉的通道2.不要給已經(jīng)關(guān)閉的通道繼續(xù)發(fā)送)
1.最簡(jiǎn)單的一種方式實(shí)現(xiàn)一個(gè)函數(shù)用來(lái)檢查通道關(guān)閉了沒(méi)有
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan T)
fmt.Println(IsClosed(c)) // false
close(c)
fmt.Println(IsClosed(c)) // true
}
這種函數(shù)靠select,default實(shí)現(xiàn)。我們可以將通道傳入函數(shù)通過(guò)case來(lái)判斷是否是接收通道是就說(shuō)明通道已經(jīng)關(guān)閉返回true如果不是就返回false說(shuō)明通道沒(méi)有關(guān)閉。但是這種方法只能檢查一時(shí)的狀態(tài)如果你的代碼中調(diào)用了類(lèi)似功能的方法修改了通道的狀態(tài),那么你相信這次結(jié)果必然會(huì)產(chǎn)生,關(guān)閉已經(jīng)關(guān)閉的通道,給已經(jīng)關(guān)閉的通道發(fā)送值的錯(cuò)誤。
2.使用defer和recover來(lái)使你的數(shù)據(jù)安全的發(fā)送到通道
func SafeSend(ch chan T, value T) (closed bool) {
defer func() {
if recover() != nil {
// the return result can be altered
// in a defer function call
closed = true
}
}()
ch <- value // panic if ch is closed
return false // <=> closed = false; return
}
首先這個(gè)方式符合不在接受端關(guān)閉的原則。所以我們實(shí)現(xiàn)的是一個(gè)安全的發(fā)送端。如果通道沒(méi)有關(guān)閉數(shù)據(jù)會(huì)安全的發(fā)送進(jìn)去,并返回false。如果通道關(guān)閉那么引發(fā)一次panic,recover恢復(fù)panic并且返回true告訴你通道關(guān)閉了。
3.安全的關(guān)閉通道
func SafeClose(ch chan T) (justClosed bool) {
defer func() {
if recover() != nil {
justClosed = false
}
}()
// assume ch != nil here.
close(ch) // panic if ch is closed
return true
}
可以使用這個(gè)方法關(guān)閉通道,如果通道沒(méi)有關(guān)閉就關(guān)閉通道并且返回true,如果通道關(guān)閉了那么就會(huì)引發(fā)panic,觸發(fā)recover返回false。
4.使用sync.Once來(lái)關(guān)閉channel
type MyChannel struct {
C chan T
once sync.Once
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan T)}
}
func (mc *MyChannel) SafeClose() {
mc.once.Do(func(){
close(mc.C)
})
}
因?yàn)镺nce的關(guān)系,通道只能被關(guān)閉一次,這樣就避免了多次關(guān)閉的問(wèn)題。
5.使用Mutex避免多次關(guān)閉channel
type MyChannel struct {
C chan T
closed bool
mutex sync.Mutex
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan T)}
}
func (mc *MyChannel) SafeClose() {
mc.mutex.Lock()
if !mc.closed {
close(mc.C)
mc.closed = true
}
mc.mutex.Unlock()
}
func (mc *MyChannel) IsClosed() bool {
mc.mutex.Lock()
defer mc.mutex.Unlock()
return mc.closed
}
通關(guān)變量closed來(lái)控制鎖的獲取權(quán)限。
6.在一個(gè)發(fā)送端多個(gè)接收端的情況下,先用Waitgroup的Add數(shù)來(lái)控制通道的大小,然后一個(gè)發(fā)送端開(kāi)始發(fā)送數(shù)據(jù),并且設(shè)置好停止發(fā)送的條件,并在不想發(fā)送的時(shí)候停止關(guān)閉通道。多個(gè)接收端開(kāi)始接收,并且每一次接收完畢就waitgroup.Done。這樣做就可以確保全部接受,并且符合不在接收端關(guān)閉通道的原則。
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
// ...
const MaxRandomNumber = 100000
const NumReceivers = 100
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)
// ...
dataCh := make(chan int, 100)
// the sender
go func() {
for {
if value := rand.Intn(MaxRandomNumber); value == 0 {
// the only sender can close the channel safely.
close(dataCh)
return
} else {
dataCh <- value
}
}
}()
// receivers
for i := 0; i < NumReceivers; i++ {
go func() {
defer wgReceivers.Done()
// receive values until dataCh is closed and
// the value buffer queue of dataCh is empty.
for value := range dataCh {
log.Println(value)
}
}()
}
wgReceivers.Wait()
}
7.這種是多個(gè)發(fā)送端一個(gè)接收端的情況,用一個(gè)信號(hào)通道來(lái)控制停止發(fā)送請(qǐng)求,由于我們不能讓接收者來(lái)關(guān)閉通道,所以我們可以關(guān)閉一個(gè)額外的通道來(lái)達(dá)到停止發(fā)送的目的。由于發(fā)送者有多個(gè)所以不應(yīng)該在發(fā)送端直接關(guān)閉data通道,而是在接收端接收完畢通過(guò)信號(hào)通道通知發(fā)送端停止發(fā)送。
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
// ...
const MaxRandomNumber = 100000
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(1)
// ...
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh is an additional signal channel.
// Its sender is the receiver of channel dataCh.
// Its reveivers are the senders of channel dataCh.
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
value := rand.Intn(MaxRandomNumber)
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}()
}
// the receiver
go func() {
defer wgReceivers.Done()
for value := range dataCh {
if value == MaxRandomNumber-1 {
// the receiver of the dataCh channel is
// also the sender of the stopCh cahnnel.
// It is safe to close the stop channel here.
close(stopCh)
return
}
log.Println(value)
}
}()
// ...
wgReceivers.Wait()
}
8.多個(gè)發(fā)送者多個(gè)接收者的情況
package main
import (
"time"
"math/rand"
"sync"
"log"
"strconv"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
// ...
const MaxRandomNumber = 100000
const NumReceivers = 10
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)
// ...
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh is an additional signal channel.
// Its sender is the moderator goroutine shown below.
// Its reveivers are all senders and receivers of dataCh.
toStop := make(chan string, 1)
// the channel toStop is used to notify the moderator
// to close the additional signal channel (stopCh).
// Its senders are any senders and receivers of dataCh.
// Its reveiver is the moderator goroutine shown below.
var stoppedBy string
// moderator
go func() {
stoppedBy = <- toStop // part of the trick used to notify the moderator
// to close the additional signal channel.
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(MaxRandomNumber)
if value == 0 {
// here, a trick is used to notify the moderator
// to close the additional signal channel.
select {
case toStop <- "sender#" + id:
default:
}
return
}
// the first select here is to try to exit the
// goroutine as early as possible.
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
defer wgReceivers.Done()
for {
// same as senders, the first select here is to
// try to exit the goroutine as early as possible.
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case value := <-dataCh:
if value == MaxRandomNumber-1 {
// the same trick is used to notify the moderator
// to close the additional signal channel.
select {
case toStop <- "receiver#" + id:
default:
}
return
}
log.Println(value)
}
}
}(strconv.Itoa(i))
}
// ...
wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}
請(qǐng)注意channel toStop的緩沖大小是1.這是為了避免當(dāng)mederator goroutine 準(zhǔn)備好之前第一個(gè)通知就已經(jīng)發(fā)送了,導(dǎo)致丟失。
(因?yàn)檫@個(gè)問(wèn)題我當(dāng)時(shí)確實(shí)不知道怎么回答所以寫(xiě)的詳細(xì)點(diǎn),當(dāng)時(shí)我的想法就是關(guān)閉通道不就是close()還能有其他的?/(ㄒoㄒ)/~~)
二.go垃圾回收
三.go內(nèi)存管理
1.內(nèi)存分配的大致策略
申請(qǐng)一塊較大的地址空間(虛擬內(nèi)存),用于內(nèi)存分配及管理(golang:spans+bitmap+arena->512M+16G+512G) 當(dāng)空間不足時(shí),向系統(tǒng)申請(qǐng)一塊較大的內(nèi)存,如100KB或者1MB 申請(qǐng)到的內(nèi)存塊按特定的size,被分割成多種小塊內(nèi)存(golang:_NumSizeClasses = 67),并用鏈表管理起來(lái) 創(chuàng)建對(duì)象時(shí),按照對(duì)象大小,從空閑鏈表中查找到最適合的內(nèi)存塊 銷(xiāo)毀對(duì)象時(shí),將對(duì)應(yīng)的內(nèi)存塊返還空閑鏈表中以復(fù)用 空閑內(nèi)存達(dá)到閾值時(shí),返還操作系統(tǒng) Go內(nèi)存管理基于tcmalloc,使用連續(xù)虛擬地址,以頁(yè)(8k)為單位、多級(jí)緩存進(jìn)行管理; 在分配內(nèi)存時(shí),需要對(duì)size進(jìn)行對(duì)齊處理,根據(jù)best-fit找到合適的mspan,對(duì)未用完的內(nèi)存還會(huì)拆分成其他大小的mspan繼續(xù)使用 在new一個(gè)object時(shí)(忽略逃逸分析),根據(jù)object的size做不同的分配策略: 極小對(duì)象(size<16byte)直接在當(dāng)前P的mcache上的tiny緩存上分配; 小對(duì)象(16byte <= size <= 32k)在當(dāng)前P的mcache上對(duì)應(yīng)slot的空閑列表中分配,無(wú)空閑列表則會(huì)繼續(xù)向mcentral申請(qǐng)(還是沒(méi)有則向mheap申請(qǐng)); 大對(duì)象(size>32k)直接通過(guò)mheap申請(qǐng),如果mheap也沒(méi)有了就去操作系統(tǒng)申請(qǐng)。
span是內(nèi)存管理的基本單位由多個(gè)頁(yè)組成,它使得內(nèi)存分配更加細(xì)致。
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
nelems uintptr // number of object in the span.
allocBits *gcBits
gcmarkBits *gcBits
allocCount uint16 // number of allocated objects
spanclass spanClass // size class and noscan (uint8)
elemsize uintptr // computed from sizeclass or from npages
}
現(xiàn)在我們了解到go用span來(lái)分配內(nèi)存,那么在哪里用span?我們都知道每個(gè)p都有mcache,通過(guò)mcache管理每個(gè)g需要的內(nèi)存。
type mcache struct {
tiny uintptr
tinyoffset uintptr
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
}
numSpanClasses = _NumSizeClasses << 1
_NumSizeClasses = 67
從結(jié)構(gòu)體來(lái)看,前兩個(gè)字段用于極小對(duì)象的分配。alloc是一個(gè)mspan數(shù)組,長(zhǎng)度是1>>67,說(shuō)明每種size class有2組元素。第一組span對(duì)象中包含了指針,叫做scan,表示需要gc scan;第二組沒(méi)有指針,叫做noscan。提高gc scan性能。mcache初始化沒(méi)有span,g先從central動(dòng)態(tài)申請(qǐng),并緩存在cache。
central
type mcentral struct {
lock mutex
spanclass spanClass
nonempty mSpanList // list of spans with a free object, ie a nonempty free list
empty mSpanList // list of spans with no free objects (or cached in an mcache)
// nmalloc is the cumulative count of objects allocated from
// this mcentral, assuming all spans in mcaches are
// fully-allocated. Written atomically, read under STW.
nmalloc uint64
}
lock:多個(gè)g并發(fā)從central申請(qǐng)span,所以需要lock,保證一致性。
spanclass:每個(gè)mcentral管理著一組有相同size class的span列表。
empty:沒(méi)有內(nèi)存可用的span列表。
nmalloc:累計(jì)分配的對(duì)象個(gè)數(shù)。
線(xiàn)程從central獲取span的步驟
1.加鎖。
2.從nonemptylie列表獲取一個(gè)可用的span,并將其從鏈表中刪除。
3.將取出的sapn放入empty鏈表。
4.將span返回給線(xiàn)程。
5.解鎖。
6.線(xiàn)程將該span緩存進(jìn)cache。
線(xiàn)程將span歸還步驟
1.加鎖
2.將span從empty列表中刪除
3.將span加入nonempty列表
4.解鎖
heap
central只管理特定的size class span,所以必然有一個(gè)人更上層的數(shù)據(jù)結(jié)構(gòu),管理所有的sizeclass central,這就是heap。
type mheap struct {
lock mutex
spans []*mspan
// Malloc stats.
largealloc uint64 // bytes allocated for large objects
nlargealloc uint64 // number of large object allocations
largefree uint64 // bytes freed for large objects (>maxsmallsize)
nlargefree uint64 // number of frees for large objects (>maxsmallsize)
// range of addresses we might see in the heap
bitmap uintptr // Points to one byte past the end of the bitmap
bitmap_mapped uintptr
arena_start uintptr
arena_used uintptr // Set with setArenaUsed.
arena_alloc uintptr
arena_end uintptr
arena_reserved bool
central [numSpanClasses]struct {
mcentral mcentral
pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
}
spans:映射span->page
large:大對(duì)象>32k
bitmap:gc
arena:arena區(qū)相關(guān)信息,pages,堆區(qū)
central:通過(guò)size class管理span,每種size class對(duì)應(yīng)兩個(gè)centarl.
四.go調(diào)度器
pmg(也就是系統(tǒng)線(xiàn)程到goroutine的映射關(guān)系,當(dāng)時(shí)一慌居然忘了可惡)
推薦這篇文章
https://segmentfault.com/a/1190000016611742
簡(jiǎn)單聊一下m
創(chuàng)建m:
時(shí)機(jī)很多, 不容易理清楚..
一般是有g(shù)待運(yùn)行, 且有空閑的p, 那么就需要newm, 這種一般是新生成了g, 或者start the world.
還有就是系統(tǒng)調(diào)用太久, handoffp后, 也需要newm (其實(shí)也是有g(shù)待運(yùn)行, 且有空閑的p的一種).
newm的邏輯是有idle m就用idle的, 沒(méi)有就創(chuàng)建一個(gè)
具體很多地方(可以看看runtime.newm哪些地方調(diào)用了):
- 有g(shù)任務(wù)待運(yùn)行, 會(huì)檢查有沒(méi)有spin的m,沒(méi)有的話(huà),會(huì)去wakeup. spin的m獲取任務(wù)后, 也可能會(huì)去newm.
- sysmon檢測(cè)到 m和g一直在系統(tǒng)調(diào)用, 也會(huì)需要m.
- stw后, restart the world里, 可能也需要newm.
....
系統(tǒng)限制:
linux是沒(méi)有限制協(xié)程數(shù)量噢, go的運(yùn)行時(shí)目前限制了1萬(wàn).
釋放和回收:
locked m和g, 在g運(yùn)行完了, 會(huì)釋放m, 真正的exit(一般業(yè)務(wù)代碼里很少用LockOSThread)
大多數(shù)情況m是不會(huì)exit的. 只會(huì)變成free狀態(tài), 以便復(fù)用.
那一般就是運(yùn)行的g運(yùn)行完了, 調(diào)度找不到任務(wù), 就變成freem了.
為什么不exit線(xiàn)程, 不是為了復(fù)用, 是目前還不好搞.
commit里說(shuō)主要還是g0棧不好clean up.
runtime: make it possible to exit Go-created threads
https://go-review.googlesource.com/c/go/+/46037/
五.閉包
不知道說(shuō)成回調(diào)函數(shù)會(huì)不會(huì)挨打。emmm