文章目錄
- 寫在前面
- 筆試
- 一面
- 線程和協程有什麼差別?各自有什麼優缺點?
- 程序之間如何進行通信?
- 什麼是信号,信号量是如何實作的?
- 講講Go裡面的GMP模型?
- Go的GMP模型
- map用過吧?怎麼對map進行排序?
- 講講map底層?為什麼是無序的?
- 知道TCP連接配接吧?三次握手的目的是什麼?
- 那四次揮手有了解過嗎?為什麼是四次?
- 什麼是粘包和拆包?為什麼會出現?
- 怎麼解決?
- 什麼是事務?
- 那 redis 支援事務嗎?
- 算法:手撕連結清單,要求遞歸實作。
- 算法:忘記了。。好像是中等題,反正不難,也不簡單。
- 二面
寫在前面
米哈遊 面試下來感覺還行吧,挺注重基礎的,面試官水準也很高。但是感覺不是在招人的樣子,我有好多同學都是履歷挂(
筆試
無筆試,很奇怪(
一面
線程和協程有什麼差別?各自有什麼優缺點?
程序 | 線程 |
系統中正在運作的一個應用程式 | 系統配置設定處理器時間資源的基本單元 |
程式一旦運作就是程序 | 程序之内獨立執行的一個單元執行流 |
資源配置設定的最小機關 | 程式執行的最小機關 |
程序有
獨立的位址空間
,一個程序崩潰後,在
保護模式下不會對其它程序産生影響
,而線程隻是一個程序中的不同執行路徑。
線程有自己的堆棧和局部變量,但線程
沒有單獨的位址空間
,一個線程死掉就等于整個程序死掉,是以多程序的程式要比多線程的程式健壯,但在
程序切換
時,耗費資源較大,效率要差一些。但對于一些
要求同時進行并且又要共享某些變量的并發操作,隻能用線程,不能用程序
程序之間如何進行通信?
管道、消息隊列、共享記憶體、信号量、信号、socket
什麼是信号,信号量是如何實作的?
信号:
在Linux中,為了響應各種事件,提供了幾十種信号,可以通過
kill -l
指令檢視。
如果是運作在 shell終端 的程序,可以通過鍵盤組合鍵來給程序發送信号,例如使用Ctrl+C 産生
SIGINT 信号
,表示終止程序。
如果是運作在背景的程序,可以通過指令來給程序發送信号,例如使用
kill -9 PID
産生
SIGKILL
信号,表示立即結束程序。
信号量:
當使用共享記憶體的通信方式,如果有多個程序同時往共享記憶體寫入資料,有可能先寫的程序的内容被其他程序覆寫了。
是以需要一種保護機制,信号量本質上是一個整型的計數器,用于實作
程序間的互斥和同步
。
信号量代表着資源的數量,操作信号量的方式有兩種:
- P操作:這個操作會将信号量減一,相減後信号量如果小于0,則表示資源已經被占用了,程序需要阻塞等待;如果大于等于0,則說明還有資源可用,程序可以正常執行。
- V操作:這個操作會将信号量加一,相加後信号量如果小于等于0,則表明目前有程序阻塞,于是會将該程序喚醒;如果大于0,則表示目前沒有阻塞的程序。
講講Go裡面的GMP模型?
Go的GMP模型
G:表示goroutine,存儲了goroutine的執行stack資訊、goroutine狀态以及goroutine的任務函數等;另外G對象是
可以重用
的。
P:表示邏輯processor,P 的數量決定了系統内最大可并行的 G 的數量(前提:系統的實體cpu核數 >= P的數量);P的最大作用還是其擁有的各種G對象隊列、連結清單、一些cache和狀态。
M:M 代表着真正的執行計算資源,實體 Processor。
G 如果想運作起來必須依賴 P,因為 P 是它的,但是 P 要想真正的運作,他也需要與 M 綁定,這樣才能真正的運作起來,
邏輯處理單元
。
P 和 M 的這種關系就相當于 Linux 系統中的使用者層面的線程和核心的線程是一樣的
map用過吧?怎麼對map進行排序?
go的map不保證有序性,是以按key排序需要取出key,對key排序,再周遊輸出value
講講map底層?為什麼是無序的?
map 的底層是一個結構體
// Go map 的底層結構體表示
type hmap struct {
count int // map中鍵值對的個數,使用len()可以擷取
flags uint8
B uint8 // 哈希桶的數量的log2,比如有8個桶,那麼B=3
noverflow uint16 // 溢出桶的數量
hash0 uint32 // 哈希種子
buckets unsafe.Pointer // 指向哈希桶數組的指針,數量為 2^B
oldbuckets unsafe.Pointer // 擴容時指向舊桶的指針,當擴容時不為nil
nevacuate uintptr
extra *mapextra // 可選字段
}
const (
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits // 桶數量 1 << 3 = 8
)
// Go map 的一個哈希桶,一個桶最多存放8個鍵值對
type bmap struct {
// tophash存放了哈希值的最高位元組
tophash [bucketCnt]uint8
// 在這裡有幾個其它的字段沒有顯示出來,因為k-v的數量類型是不确定的,編譯的時候才會确定
// keys: 是一個數組,大小為bucketCnt=8,存放Key
// elems: 是一個數組,大小為bucketCnt=8,存放Value
// 你可能會想到為什麼不用空接口,空接口可以儲存任意類型。但是空接口底層也是個結構體,中間隔了一層。是以在這裡沒有使用空接口。
// 注意:之是以将所有key存放在一個數組,将value存放在一個數組,而不是鍵值對的形式,是為了消除例如map[int64]所需的填充整數8(記憶體對齊)
// overflow: 是一個指針,指向溢出桶,當該桶不夠用時,就會使用溢出桶
}
當向 map 中存儲一個 kv 時,通過
k 的 hash 值與 buckets 長度取餘
,定位到 key 在哪一個bucket中,hash 值的高8位存儲在 bucket 的 tophash[i] 中,用來快速判斷 key是否存在。當一個 bucket 滿時,通過 overflow 指針連結到下一個 bucket。
在源碼
runtime.mapiterinit
中
func mapiterinit(t *maptype, h *hmap, it *hiter) {
...
it.t = t
it.h = h
it.B = h.B
it.buckets = h.buckets
if t.bucket.kind&kindNoPointers != 0 {
h.createOverflow()
it.overflow = h.extra.overflow
it.oldoverflow = h.extra.oldoverflow
}
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
it.bucket = it.startBucket
...
mapiternext(it)
}
通過對
mapiterinit 方法
閱讀,可得知其主要用途是在 map 進行周遊疊代時進行初始化動作。共有三個形參,用于讀取目前
哈希表的類型資訊、目前哈希表的存儲資訊和目前周遊疊代的資料
源碼中
fastrand
...
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
// iterator state
it.bucket = it.startBucket
在這段代碼中,它生成了随機數。用于決定從哪裡開始循環疊代。更具體的話就是
根據随機數,選擇一個桶位置作為起始點進行周遊疊代。
是以每次重新
for range map
,你見到的結果都是不一樣的。那是因為它的起始位置根本就不固定!
知道TCP連接配接吧?三次握手的目的是什麼?
因為通信的前提是確定雙方都是
接收和發送資訊是正常
的。
三次握手是為了建立可靠的資料傳輸通道,
第一次握手就是
讓 接收方 知道 發送方 有發送資訊的能力
第二次握手就是
讓 發送方 知道 接收方 有發送和接受資訊的能力
第三次握手就是
讓 接收方 知道 發送方 有接受資訊的能力
那四次揮手有了解過嗎?為什麼是四次?
四次揮手則是為了保證等資料完成的被接收完再關閉連接配接。
既然提到需要保證資料完整的傳輸完,那就需要保證雙方
都達到關閉連接配接的條件才能斷開。
第一次揮手:用戶端
發起關閉連接配接
的請求給服務端;
第二次揮手:服務端收到關閉請求的時候可能這個時候資料還沒發送完,是以服務端會先回複一個
确認封包
,表示
自己知道用戶端想要關閉連接配接了
,但是因為資料還沒傳輸完,是以還需要等待;
第三次揮手:當資料傳輸完了,服務端會主動發送一個
FIN 封包
,告訴用戶端,表示資料
已經發送完了
,服務端這邊
準備關閉連接配接了
。
第四次揮手:當用戶端收到服務端的 FIN 封包過後,會回複一個
ACK 封包
,告訴服務端自己知道了,再等待一會就關閉連接配接。
什麼是粘包和拆包?為什麼會出現?
粘包就是兩個或者多個以上的包粘在一起,拆包就是為什麼解決粘包問題。
出現粘包原因有兩個,一個是發送方發送不及時,導緻多個包在發送端粘黏。另一個是接收方接受不及時,導緻包在接收端堆疊導緻。
怎麼解決?
設計一個帶標頭的應用層封包結構就能解決
。標頭定長,以特定标志開頭,裡帶着負載長度,這樣接收側隻要以定長嘗試讀取標頭,再按照標頭裡的負載長度讀取負載就行了,多出來的資料都留在緩沖區裡即可。其實ip封包和tcp封包都是這麼幹的。
什麼是事務?
事務具有
原子性、一緻性、隔離性、持久性
特性,并且 指将一系列資料操作捆綁成為一個整體進行統一管理,如果某一事務執行成功,則在該事物中進行的所有資料更改均會送出,成為資料庫中的永久組成部分。
那 redis 支援事務嗎?
redis 是不支援事務的,因為 他不支援原子性,但是支援一緻性,是最終一緻性。