一次非正式面試(go)

雖然這不是一次正經(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)用了):

  1. 有g(shù)任務(wù)待運(yùn)行, 會(huì)檢查有沒(méi)有spin的m,沒(méi)有的話(huà),會(huì)去wakeup. spin的m獲取任務(wù)后, 也可能會(huì)去newm.
  2. sysmon檢測(cè)到 m和g一直在系統(tǒng)調(diào)用, 也會(huì)需要m.
  3. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容