• go的slice学习


    并发访问slice

    线上出现一粒多协程并发append全局slice的情况,导致内存不断翻倍,因此对slice的使用需要重新考虑。
    并发读写的情况下, 可以利用锁、channel等避免竞态

    问题

    func TestDemo32(t *testing.T) {
    	var wg sync.WaitGroup
    	var n = 100
    	s := make([]int, 0, 200)
    	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
    	fmt.Printf("Data addr: %d\n", hdr.Data)
    	fmt.Printf("Data cap: %d\n", hdr.Cap)
    	fmt.Printf("Data len: %d\n", hdr.Len)
    	wg.Add(n)
    	for i := 1; i <= n; i++ {
    		go func(v int) {
    			defer wg.Done()
    			s = append(s, v)
    		}(i)
    	}
    	wg.Wait()
    	hdr = (*reflect.SliceHeader)(unsafe.Pointer(&s))
    	fmt.Printf("Data addr: %d\n", hdr.Data)
    	fmt.Printf("Data cap: %d\n", hdr.Cap)
    	fmt.Printf("Data len: %d\n", hdr.Len)
    	fmt.Println(jsonx.ToString(s))
    
    	// Data addr: 824645965056
    	// Data cap: 200
    	// Data len: 0
    	// Data addr: 824645965056
    	// Data cap: 200
    	// Data len: 96
    }
    
    func TestDemo33(t *testing.T) {
    	var wg sync.WaitGroup
    	var n = 500
    	s := make([]int, 0, 10)
    	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
    	fmt.Printf("Data addr: %d\n", hdr.Data)
    	fmt.Printf("Data cap: %d\n", hdr.Cap)
    	fmt.Printf("Data len: %d\n", hdr.Len)
    	wg.Add(n)
    	for i := 1; i <= n; i++ {
    		go func(v int) {
    			defer wg.Done()
    			s = append(s, v)
    		}(i)
    	}
    	wg.Wait()
    	hdr = (*reflect.SliceHeader)(unsafe.Pointer(&s))
    	fmt.Printf("Data addr: %d\n", hdr.Data)
    	fmt.Printf("Data cap: %d\n", hdr.Cap)
    	fmt.Printf("Data len: %d\n", hdr.Len)
    	fmt.Println(jsonx.ToString(s))
    	
    	// Data addr: 824635459136
    	// Data cap: 10
    	// Data len: 0
    	// Data addr: 824665328128
    	// Data cap: 672
    	// Data len: 453
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    Go语言中的slice是一种引用类型,它本身不保存任何元素,只是对数组的引用。
    append操作的主要功能是向slice添加元素。
    如果slice的底层数组容量足够,就直接添加;如果不够,就会先分配新的底层数组,然后将原有的元素和新添加的元素一起拷贝到新数组,在这个过程中,原有的底层数组会被垃圾回收。

    当底层数组的容量不足以容纳新的元素时,就会产生新的底层数组
    此时原有的slice和返回的新的slice,其底层数组是不同的。
    这也是为什么在使用append时,总是习惯性地将结果再次赋值给原slice。

    方案一 channel

    func TestDemo(t *testing.T) {
    	// 无缓冲,发送侧有数据,接收侧才执行
    	// 用于做同步
    	c := make(chan struct{})
    
    	// new 了该 job 后,该 job 就开始准备从 channel 接收数据
    	s := NewScheduleJob(n, func() { c <- struct{}{} })
    
    	// 并发发送数据到channel
    	var wg sync.WaitGroup
    	var n  = 1000
    	wg.Add(n)
    	for i := 0; i < n; i++ {
    		go func(v int) {
    			defer wg.Done()
    			s.AddData(v)
    		}(i)
    	}
    
    	// 等待上述多个协程将数据存入slice
    	wg.Wait()
    
    	// 发送完之后关闭channel
    	s.Close()
    
    	// 阻塞在这里是等待NewScheduleJob执行结束
    	<-c
    	
    	// 最终实现读写一致
    	fmt.Println(len(s.data))
    }
    
    type ServiceData struct {
    	ch   chan int // 用来同步的channel
    	data []int    // 存储数据的slice
    }
    
    // Schedule 从 channel 接收数据串行存入slice,直到ch关闭
    func (s *ServiceData) Schedule() {
    	for i := range s.ch {
    		s.data = append(s.data, i)
    	}
    }
    
    // Close 关闭channel
    func (s *ServiceData) Close() {
    	close(s.ch)
    }
    
    // AddData 发送数据到 channel
    func (s *ServiceData) AddData(v int) {
    	s.ch <- v
    }
    
    func NewScheduleJob(size int, done func()) *ServiceData {
    	s := &ServiceData{
    		ch:   make(chan int, size),
    		data: make([]int, 0),
    	}
    
    	go func() {
    		// 并发地 append 数据到 slice
    		// Schedule 从 channel 接收数据串行存入slice,直到ch关闭
    		s.Schedule()
    		// 通知主协程继续执行
    		done()
    	}()
    
    	return s
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    优点:逻辑复杂,适用于高并发场景

    方案二 - 锁

    func main() {
    	slc := make([]int, 0, 1000)
    	var wg sync.WaitGroup
    	var lock sync.Mutex
    
    	for i := 0; i < 1000; i++ {
    		wg.Add(1)
    		go func(a int) {
    			defer wg.Done()
    			lock.Lock()
    			defer lock.Unlock()
    			slc = append(slc, a)
    		}(i)
    	}
       wg.Wait()
    	fmt.Println(len(slc))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    优点:锁的逻辑重,适用于对性能要求不高的场景

    学习文档

    https://juejin.cn/post/6844904134592692231

  • 相关阅读:
    Matlab数值计算(多项式插值)
    万字长文详解开源流式湖仓服务Arctic
    Android/Automotive 多声道录音支持
    主流开源OLAP对比分析
    适配器模式
    2022杭电多校9(总结+补题)
    珂学送分
    .NET餐厅管理系统菜品添加页面前端
    CEF 桌面软件开发实战
    notion database 必知必会
  • 原文地址:https://blog.csdn.net/wangkai6666/article/details/136724052