【Golang 源码】sync.Map 源码详解
sync.Map
不安全的 map
go 中原生的 map 不是并發(fā)安全的,多個(gè) goroutine 并發(fā)地去操作一個(gè) map 會(huì)拋出一個(gè) panic
package main import "fmt" func main() {m := map[string]int {"1": 1, "2": 2,}// 并發(fā)寫for i := 0; i < 100; i ++ {go func(i int) {m[fmt.Sprintf("%d", i)] = i}(i)}// 讀for i := 0; i < 100; i ++ {fmt.Println(i, m[fmt.Sprintf("%d", i)])} }PS E:\test\gol\main> go run .\01.go fatal error: concurrent map writes fatal error: concurrent map writes解決的辦法是互斥地去讀寫,如:
type SafeMap struct {data map[interface{}]interface{}sync.RWMutex }func (sm *SafeMap) Set(key interface{}, val interface{}) {sm.Lock()defer sm.Unlock()sm.data[key] = val }func (sm *SafeMap) Get(key interface{}) (val interface{}){sm.Lock()defer sm.Unlock()val, ok := sm.data[key]if !ok {val = ""}return }而另一個(gè)常用的辦法就是使用 sync 包提供的 Map.
sync.Map 概覽
sync.Map 包的核心是 Map 結(jié)構(gòu)體,其向外暴露了四個(gè)方法:
// 從 Map 中取出一個(gè) value func (m *Map) Load(key interface{}) (value interface{}, ok bool)// 向 Map 中 存入一個(gè) KV 對(duì) func (m *Map) Store(key, value interface{})// 如果 Map 中存在 key,覆蓋并返回 (舊值, true), 否則返回 (新值, false) func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)// 從 Map 中刪除一個(gè) KV 對(duì) func (m *Map) Delete(key interface{})// 對(duì) Map 中的所有 KV 執(zhí)行 f, 直到 f 返回 false func (m *Map) Range(f func(key, value interface{}) bool)源碼分析
數(shù)據(jù)結(jié)構(gòu)和設(shè)計(jì)思想
通過上面直接對(duì)所有讀寫操作加鎖的方式類似于Java中的 HashTable, 效率并不高,所以參考 ConcurrentHashMap, orcaman 提出了 concurrent_map
通過對(duì)內(nèi)部map進(jìn)行分片,降低鎖粒度,從而達(dá)到最少的鎖等待時(shí)間(鎖沖突).
但這樣只是降低了鎖粒度,sync.Map 的思路是盡可能使用原子操作而不是鎖,因?yàn)樵硬僮髦苯佑捎布С?#xff0c;在多核 CPU 環(huán)境下有更好的拓展性和性能。
如何對(duì) map 使用原子操作呢?,之所以出現(xiàn)不安全的現(xiàn)象,是由于多個(gè) goroutine 對(duì)同一個(gè)公有變量(map)操作引起的,如果我們將這個(gè)map 存儲(chǔ)在 atomic.Value 中,讀的時(shí)候使用 Load原子地獲取到 map, 再返回 map[key]不就可以避免讀時(shí)鎖競(jìng)爭(zhēng)了嗎?
type SafeMap struct {read atomic.Value }type readOnly struct {m map[interface{}]interface{} }func (m *SafeMap) Load(key interface{}) interface{}{read := m.read.Load().(readOnly)return read.m[key] }類似于上面地偽代碼,將 map 包裝成 readOnly 后,使用 Value 存儲(chǔ),在需要 Load 的時(shí)候,原子地取出 readOnly, 由于 read 變量不是公有的,所以在拿出 readOnly 后,再從其中查找 key 對(duì)應(yīng)的 value 就不存在線程安全的問題了。
這樣看起來很完美,但問題在于僅僅使用 Value 無法安全的存儲(chǔ)鍵值對(duì):
func (m *SafeMap) Store(k, v interface{}) {read := m.read.Load().(readOnly)read.m[key] = vm.read.Store(rea) }上面三條語句操作的其實(shí)是同一個(gè) map ,可能出現(xiàn)在 store 之前已經(jīng)有別人 store 的情況,不對(duì)這三條語句加鎖可能導(dǎo)致覆蓋別人的數(shù)據(jù),所以其并不是安全的,要想實(shí)現(xiàn)安全存儲(chǔ),必須加鎖:
type SafeMap struct {mu sync.Mutexread atomic.Value }func (m *SafeMap) Store(k, v interface{}) {m.mu.Lock()read := m.read.Load().(readOnly)read.m[key] = vm.read.Store(rea)m.mu.UnLock() }但這就退化到了最初的情況,每次 Store 都需要競(jìng)爭(zhēng)鎖,為了提高Store 的效率,sync.Map 使用了一個(gè)冗余的字段 dirty, 如果是往 Map 中插入新值,就加鎖插入到 dirty 中, 如果是要修改已經(jīng)存在的 key 對(duì)應(yīng)的 value ,就可以直接修改 read ,當(dāng)達(dá)到某種條件時(shí),會(huì)把 dirty 轉(zhuǎn)換為 read, 這樣設(shè)計(jì)能夠盡可能避免使用 Mutex而改用性能和拓展性更好的 原子操作來實(shí)現(xiàn)安全并發(fā)。
Map struct
type Map struct {mu sync.Mutexread atomic.Valuedirty map[interface{}]*entrymisses int }- mu: 用于對(duì) dirty 操作時(shí)保障并發(fā)安全的鎖
- read: 與上面?zhèn)未a中的 read 相同,存儲(chǔ)一個(gè)只讀的量 readOnly, 對(duì)它的操作是原子的,所以對(duì) Map 的操作會(huì)優(yōu)先在 read 上嘗試。
- dirty: 這里存儲(chǔ)的是最新的 KV 對(duì),一個(gè)新的鍵值對(duì)會(huì)被存儲(chǔ)在這,等時(shí)機(jī)成熟,dirty 會(huì)被轉(zhuǎn)換為 read, 然后該字段會(huì)被置為空,由于 dirty 中的數(shù)據(jù)總是比 read 中的更新,所以在查詢修改等操作中,read 中如果找不到還需要回到 dirty 中找。
- misses: 控制什么時(shí)候 dirty 轉(zhuǎn)換為 read, 每次從 read 中沒找到回到 dirty 中查詢都會(huì)導(dǎo)致 misses 自增一,等 misses > len(dirty) 時(shí), 就會(huì)觸發(fā)轉(zhuǎn)換。
readOnly
type readOnly struct {// m 和 dirty 中的 value 是同一塊內(nèi)存m map[interface{}]*entry// 如果 dirty 和 read 中的數(shù)據(jù)不一致時(shí),amended 為 trueamended bool }readOnly 同樣類似于上面?zhèn)未a中的 readOnly, Map.read中存放的就是它,其中 m 便是車存儲(chǔ)鍵值對(duì)的地方,由于 read 中的數(shù)據(jù)可能滯后于 dirty, 所以需要使用 amended 來標(biāo)識(shí), read 中沒有讀到且 amended == true 時(shí),要回 dirty 中查詢。
entry
type entry struct {p unsafe.Pointer // *interface{} }從上面可以看到,readOnly 和 dirty 中存儲(chǔ)的 Value 都是 entry 的指針,這樣做的好處在于:
Load
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {read, _ := m.read.Load().(readOnly)// 嘗試從 read 中獲取e, ok := read.m[key]// 如果 read 中沒找到并且 read 和 dirty 不一致,需要從 dirty 中找if !ok && read.amended {m.mu.Lock()// double-checking, 避免在加鎖過程中 dirty 被提升為 readread, _ = m.read.Load().(readOnly)e, ok = read.m[key]// 雙重檢查沒有得到,去 dirty 中找if !ok && read.amended {e, ok = m.dirty[key]// 修改 misses,嘗試提升 dirtym.missLocked()}m.mu.Unlock()}if !ok {return nil, false}return e.load() }Load 的邏輯很簡(jiǎn)單,就是先從 read 中找,找不到就去 dirty 中找,并執(zhí)行 missLocked() 修改 misses 判斷是否需要提升 dirty 到 read. 唯一需要注意的是這里的 double-checking:
由于可能存在一個(gè) goroutine 在執(zhí)行完 if !ok && read.amended 但還沒有加鎖完成時(shí),另一個(gè) goroutine 將 dirty 提升成了 read 的情況,所以在加鎖之后還需要再從 read 中檢查一遍,這與 Java 安全單例中的雙重檢查是一樣的,雙重檢查會(huì)在 Map 中多次使用到。
從 read 或 dirty 中得到 key 對(duì)應(yīng)的 value 后,并不是最終的結(jié)果,而是一個(gè)指向 entry 的指針,我們需要根據(jù)其指向的 entry 中的 p 拿到真實(shí)的 value:
func (e *entry) load() (value interface{}, ok bool) {p := atomic.LoadPointer(&e.p)if p == nil || p == expunged {return nil, false}return *(*interface{})(p), true }entry.p 有三種可能的值:
前兩種的出現(xiàn)是由于 Map 的延時(shí)刪除策略,到刪除時(shí)再說,所以在這個(gè),如果 p 等于前兩種值,就說明 key 不存在或已經(jīng)被刪除,所以返回 nil, false
missLocked 的邏輯也很簡(jiǎn)單,每當(dāng)調(diào)用,就將 misses自增 1 ,當(dāng) m.misses >= len(m.dirty) 時(shí),會(huì)進(jìn)行提升,提升的過程也很簡(jiǎn)單,提升結(jié)束后,會(huì)對(duì) dirty 和 misses 初始化。
func (m *Map) missLocked() {m.misses++if m.misses < len(m.dirty) {return}// 將 dirty 提升為 readm.read.Store(readOnly{m: m.dirty})// 重置相關(guān)字段m.dirty = nilm.misses = 0 }Delete
func (m *Map) Delete(key interface{}) {read, _ := m.read.Load().(readOnly)e, ok := read.m[key]if !ok && read.amended {m.mu.Lock()read, _ = m.read.Load().(readOnly)e, ok = read.m[key]if !ok && read.amended {// read 中沒有,從 dirty 中刪除delete(m.dirty, key)}m.mu.Unlock()}if ok {e.delete()} }Delete 的邏輯類似于 Load() ,通過雙重檢查判斷鍵值對(duì)是否在 read 中,不在的話直接從 dirty 中刪除,否則調(diào)用 entry 的 delete 方法從read 中刪除。
func (e *entry) delete() (hadValue bool) {for {p := atomic.LoadPointer(&e.p)// 不存在或被刪除if p == nil || p == expunged {return false}// CAS 將 enter.p 指向 nilif atomic.CompareAndSwapPointer(&e.p, p, nil) {return true}} }在 enter.delete() 中,并沒有真的刪除 value, 只是通過 CAS 把 enter.p 標(biāo)記為了 nil,但這時(shí)這個(gè)鍵值對(duì)并沒有被從 read 中刪除,僅僅是吧它的值指向了 nil, 在之后的 Store 操作中,這個(gè)鍵可能還會(huì)被復(fù)用到,否則,直到下一次 dirty 升級(jí)這個(gè)鍵值才會(huì)被真正刪除,這就是延時(shí)刪除。
Store
func (m *Map) Store(key, value interface{}) {read, _ := m.read.Load().(readOnly)// kv 在 read 中能找到,更新 read key 對(duì)應(yīng)的 entryif e, ok := read.m[key]; ok && e.tryStore(&value) {return}m.mu.Lock()read, _ = m.read.Load().(readOnly)if e, ok := read.m[key]; ok {if e.unexpungeLocked() {m.dirty[key] = e}e.storeLocked(&value)} else if e, ok := m.dirty[key]; ok {e.storeLocked(&value)} else {if !read.amended {m.dirtyLocked()m.read.Store(readOnly{m: read.m, amended: true})}m.dirty[key] = newEntry(value)}m.mu.Unlock() }更新值
更新值對(duì)應(yīng)有兩種情況:
鍵值對(duì)在 read 中能找到,這時(shí)直接通過 tryStore 修改 enter.p 。
read, _ := m.read.Load().(readOnly)// kv 在 read 中能找到,更新 read key 對(duì)應(yīng)的 entryif e, ok := read.m[key]; ok && e.tryStore(&value) {return} func (e *entry) tryStore(i *interface{}) bool {for {p := atomic.LoadPointer(&e.p)// 被刪除if p == expunged {return false}// 比較 e.p 與 p, 相等賦新值,否則自旋比較if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {return true}} }tryStore 中使用 CAS 實(shí)現(xiàn)輕量級(jí)鎖實(shí)現(xiàn)了并發(fā)安全的更新操作。
在 read 中找不到,在 dirty 中:在持鎖狀態(tài)下通過 storeLocked 修改 dirty 中 entry.p
// m.mu.Lock() else if e, ok := m.dirty[key]; ok {e.storeLocked(&value) } func (e *entry) storeLocked(i *interface{}) {atomic.StorePointer(&e.p, unsafe.Pointer(i)) }插入新值
新值會(huì)被直接加鎖寫入到 dirty 中.
else {if !read.amended {m.dirtyLocked()m.read.Store(readOnly{m: read.m, amended: true})}m.dirty[key] = newEntry(value) }需要注意的是,如果 read.amended == false 時(shí),即 dirty 中沒有新數(shù)據(jù)時(shí),會(huì)執(zhí)行 if 塊中的那兩條語句,這在兩種情況下會(huì)發(fā)生:
第一次往 Map 中插入數(shù)據(jù)時(shí),amended == false, dirty 是一個(gè)空 map , 這時(shí) dirtyLocked 會(huì)直接返回什么也不做,然后第二條語句會(huì)給 read 分配一個(gè)空 map, 并標(biāo)記 dirty 中有新數(shù)據(jù)。
dirty 剛被提升為了 read, 這時(shí) amended == false, dirty == nil, dirtyLocked 會(huì)將 read 中沒有被刪除的字段復(fù)制到 dirty 中, 當(dāng)下一次提升 dirty 時(shí),那些被標(biāo)記的鍵值對(duì)才會(huì)被真正刪除。
func (m *Map) dirtyLocked() {// 對(duì)應(yīng)情況 1if m.dirty != nil {return}// 情況 2read, _ := m.read.Load().(readOnly)m.dirty = make(map[interface{}]*entry, len(read.m))for k, e := range read.m {// 沒有被刪除,復(fù)制到 dirty 中if !e.tryExpungeLocked() {m.dirty[k] = e}} }tryExpungeLocked 用來判斷 entry 是否被刪除,當(dāng) entry.p == nil 時(shí),說明這個(gè) value 被標(biāo)記為刪除,這時(shí)會(huì)把它重新標(biāo)記為 expunged 返回 true, 否則返回 false
這里的并發(fā)安全同樣使用 CAS 輕量級(jí)鎖實(shí)現(xiàn)
func (e *entry) tryExpungeLocked() (isExpunged bool) {p := atomic.LoadPointer(&e.p)for p == nil {if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {return true}p = atomic.LoadPointer(&e.p)}return p == expunged }修改已刪除的值
從上面知道,當(dāng)對(duì)已經(jīng)存在于 read 中的鍵值對(duì)執(zhí)行刪除操作時(shí),而是會(huì)把其暫時(shí)標(biāo)記為 nil, 等 dirty 升級(jí)為 read 后再插入新值時(shí)會(huì)把 read 中標(biāo)記為 nil 的值標(biāo)記為 expunged, 而其他的值會(huì)被重新復(fù)制到 dirty 中,當(dāng)這時(shí)插入剛被刪除的鍵后,就會(huì)直接把之前標(biāo)記為 expunged 的鍵的值賦為新值,如:
sMap := Map{}sMap.Store(1, 2) sMap.Store(2, 3) sMap.Store(5, 5) fmt.Println("[*] ", len(sMap.dirty)) // 3 sMap.Load(10) sMap.Load(10) sMap.Load(10) // 到這會(huì)執(zhí)行 dirty 的提升 sMap.Load(10) fmt.Println("[*] ", len(sMap.dirty)) // 0, 提升后 dirty == nil sMap.Delete(1) // 此時(shí) 1 在 read 中,刪除會(huì)將其標(biāo)記為 nil sMap.Store(4, 4) // 觸發(fā)復(fù)制, sMap.Store(1, 5) // 不會(huì)把 1 當(dāng)作一個(gè)新值插入,而是直接存儲(chǔ)在剛刪除的 1 的位置 fmt.Println("[*] ", len(sMap.dirty)) // 4, 新值會(huì)先存儲(chǔ)在 dirty 中,同時(shí)會(huì)修改 read 中對(duì)應(yīng)的 value上面的代碼是我將 Map 源碼整體復(fù)制出來后測(cè)試的,Map 中的所有字段都是私有的,直接訪問不到
這種情況對(duì)應(yīng)源碼中加鎖后的第一次判斷:
read, _ = m.read.Load().(readOnly) if e, ok := read.m[key]; ok {if e.unexpungeLocked() {m.dirty[key] = e}e.storeLocked(&value) } func (e *entry) unexpungeLocked() (wasExpunged bool) {return atomic.CompareAndSwapPointer(&e.p, expunged, nil) }加鎖后就老朋友 double-checking ,然后如果 key 在 read 中時(shí),會(huì)調(diào)用 storeLocked() 將 value 的指針存儲(chǔ)在 e.p 中,并且當(dāng)value 被標(biāo)記為 expunged時(shí)(通過 e.unexpungeLocked()判斷),意味著該鍵值對(duì)在之前已經(jīng)被刪除,但由于它還是新加入的,所以必須存放在 dirty 中,否則下一次提升 dirty 就會(huì)丟失這個(gè)值.
這與第一種更新值的不同點(diǎn)在于更新值只會(huì)從 read 中更新,不會(huì)去操作 dirty, 這是因?yàn)樵诟轮禃r(shí),dirty 與 read 是一致的,或則 dirty 比 read 更新,這是允許的,但在從 read 中復(fù)制值到 dirty 中時(shí),我們不能將已標(biāo)記的鍵值對(duì)也復(fù)制過去,這會(huì)導(dǎo)致這些鍵值無法被刪除,所以如果在插入已刪除的鍵值時(shí)還和更新值時(shí)一樣只改 read就會(huì)導(dǎo)致 read 比 dirty 新,這是不允許的。
LoadOrStore
LoadOrStore() 的作用是如果 key 存在,就 Load, 否則就 Store, 其邏輯與 Load 和 Store 基本一致,
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {// 命中 readread, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok {actual, loaded, ok := e.tryLoadOrStore(value)if ok {return actual, loaded}}// 未命中read 或 `expunged`m.mu.Lock()// ...m.mu.Unlock()return actual, loaded } func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {p := atomic.LoadPointer(&e.p)if p == expunged {return nil, false, false}if p != nil {return *(*interface{})(p), true, true}// p == nilic := ifor {// 賦新值if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {return i, false, true}// 已經(jīng)被別的協(xié)程修改,重新判斷p = atomic.LoadPointer(&e.p)if p == expunged {return nil, false, false}if p != nil {return *(*interface{})(p), true, true}} }如果 key 在 read 中, 會(huì)進(jìn)入 tryLoadOrStore:
read 沒有命中或 entry.p == expunged 時(shí),需要加鎖對(duì) dirty 進(jìn)行操作,流程與 Store 完全一樣,不再贅述。
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {// Avoid locking if it's a clean hit.read, _ := m.read.Load().(readOnly)if e, ok := read.m[key]; ok {actual, loaded, ok := e.tryLoadOrStore(value)if ok {return actual, loaded}}m.mu.Lock()read, _ = m.read.Load().(readOnly)if e, ok := read.m[key]; ok {if e.unexpungeLocked() {m.dirty[key] = e}actual, loaded, _ = e.tryLoadOrStore(value)} else if e, ok := m.dirty[key]; ok {actual, loaded, _ = e.tryLoadOrStore(value)m.missLocked()} else {if !read.amended {// We're adding the first new key to the dirty map.// Make sure it is allocated and mark the read-only map as incomplete.m.dirtyLocked()m.read.Store(readOnly{m: read.m, amended: true})}m.dirty[key] = newEntry(value)actual, loaded = value, false}m.mu.Unlock()return actual, loaded }Range
我們可以使用安全的 for-range 對(duì)一個(gè)原生的 map 進(jìn)行隨機(jī)遍歷,但 Map 使用不了這種簡(jiǎn)單的方法,好在其提供了 Map.Range,可以通過回調(diào)的方式隨機(jī)遍歷其中的鍵值。
Range 接受一個(gè)回調(diào)函數(shù),在調(diào)用時(shí),Range 會(huì)把當(dāng)前遍歷到的鍵值對(duì)傳給這個(gè)給回調(diào) f, 當(dāng) f 返回 false 時(shí),遍歷結(jié)束。
Range 的源碼很簡(jiǎn)單,為了保證遍歷完整進(jìn)行,在真正遍歷之前,他會(huì)通過 double-checking 提升 dirty.
func (m *Map) Range(f func(key, value interface{}) bool) {read, _ := m.read.Load().(readOnly)if read.amended {m.mu.Lock()read, _ = m.read.Load().(readOnly)if read.amended {read = readOnly{m: m.dirty}m.read.Store(read)m.dirty = nilm.misses = 0}m.mu.Unlock()}for k, e := range read.m {v, ok := e.load()if !ok {continue}if !f(k, v) {break}} }總結(jié)
原生的 map 并不是并發(fā)安全的,在并發(fā)環(huán)境下使用原生 map 會(huì)直接導(dǎo)致一個(gè) panic,為此,Go 官方從 1.7 之后添加了 sync.Map,用于支持并發(fā)環(huán)境下的鍵值對(duì)存取操作。
實(shí)現(xiàn)并發(fā)安全的兩個(gè)思路分別是 原子操作 和 加鎖, 原子操作由于是直接面向硬件的一組不可分割的指令,所以效率要比加鎖高很多,因此 Map 的基本思路就是盡可能多的使用原子操作,直到迫不得已才去使用鎖機(jī)制,Map 的做法是將數(shù)據(jù)冗余存儲(chǔ)了兩個(gè)數(shù)據(jù)結(jié)構(gòu)中,read 是一個(gè)只讀的 sync.Value 類型的結(jié)構(gòu),其上存儲(chǔ)的數(shù)據(jù)可以通過 Value.Load()和 Value.Store() 安全存取,另外,新的數(shù)據(jù)會(huì)被存儲(chǔ)在 dirty 中, 等實(shí)際成熟, dirty 會(huì)被升級(jí)為 read.所有的讀和修改操作都會(huì)優(yōu)先在 read 上進(jìn)行,以此盡量避免使用鎖。
Map 的優(yōu)勢(shì)主要集中于下面兩個(gè)場(chǎng)景:
(1) when the entry for a given key is only ever written once but read many times, as in caches that only grow,
(2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys.
即:
關(guān)于源碼
源碼中的一些核心思想:
關(guān)于 dirty 的提升
Map 中維持了一個(gè) int 類型的 misses 每當(dāng) Map 未命中 read 時(shí),會(huì)將該值自增 1, 當(dāng)該值大于 dirty 的長度后,dirty 就會(huì)被提升為 read,提升之后,dirty 和 misses 會(huì)被重置,等下一次插入新值時(shí),會(huì)將 read 中未刪除的數(shù)據(jù)復(fù)制到 dirty 中。
除此之外,執(zhí)行 Range 時(shí),也會(huì)先進(jìn)行一次提升。
關(guān)于延遲刪除
當(dāng)執(zhí)行 Delete 時(shí),如果 read 沒有擊中, 就會(huì)直接從 dirty 中刪除,否則如果鍵值在 read 中,會(huì)先將其 Value 的指針(enter.p)標(biāo)記為 nil, 等下一次執(zhí)行復(fù)制時(shí),這些被標(biāo)記為 nil 的鍵值會(huì)被重新標(biāo)記為 expunged,即 enter.p 有三種可能的值:
被刪除的數(shù)據(jù)直到下一次提升時(shí)才會(huì)被真正刪除
總結(jié)
以上是生活随笔為你收集整理的【Golang 源码】sync.Map 源码详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 让SlickEdit 自动编译Keil
- 下一篇: ORAN专题系列-5:5G O-RAN