跳至主要內容

高级

LiCheng大约 5 分钟

高级

介绍

  • 23/2/28
  • 理解go某些高级操作。

Select

  • select 用于在多线程计算数据
  • 下面示例附带了超时机制,但没有default语句.
  • 当存在default语句,如果渠道还未存在数据,则直接执行default语句
  • 超时机制的作用在于延迟兜底操作。
  • default就是渠道都没准备好,就直接执行
package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan string)
	c2 := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)
		c1 <- "one"
	}()

	go func() {
		time.Sleep(3 * time.Second)
		c2 <- "two"
	}()

	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-c1:
			fmt.Println("received", msg1)
		case msg2 := <-c2:
			fmt.Println("received", msg2)
		case <-time.After(5 * time.Second):
			fmt.Println("timeout") //超时
			return
		}
	}
	// 输出
	// received one
	// received two
}

常见用法

  • 1:我们需要一些其他线程的结果聚合就可以使用他,例如1个线程计算从1+至50,另1个线程从51+至100。
  • 就可以使用上面的方式进行处理提高效率
  • 如下示例
package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan int)
	c2 := make(chan int)
	go func() {
		var sum int
		for i := 1; i <= 50; i++ {
			sum += i
		}
		c1 <- sum
	}()
	go func() {
		var sum int
		for i := 51; i <= 100; i++ {
			sum += i
		}
		c2 <- sum
	}()
	var sum int
	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-c1:
			sum += msg1
		case msg2 := <-c2:
			sum += msg2
		case <-time.After(1 * time.Second):
			//每次只等待1秒超时,如果注释这个case那么就会一直等待,知道渠道发送数据
			fmt.Println("timeout")
			return
		}
	}
	fmt.Println("结果:", sum)
}

使用注意

  • select什么情况下会阻塞?

  • 当select语句没有任何case语句准备好,也没有default语句时,它就会阻塞,直到至少有一个case语句可以执行1。
  • 这意味着select语句会等待所有的channel操作,直到有一个channel可以收到或发送数据2。
  • 如果你想避免select语句阻塞,你可以使用default语句或者超时机制。

线程

轻量级线程变量如何处理的?

  • Go 语言支持并发,可以通过 go 关键字来开启 goroutine,也就是轻量级线程。goroutine 的调度是由 Go 运行时进行管理的。
  • 当启动一个 goroutine 时,它会继承当前函数的局部变量的值,并在自己的栈上创建一个副本。如果局部变量是指针或引用类型,
  • 那么 goroutine 可能会访问或修改共享的数据。为了避免数据竞争或不一致的问题,
  • Go 语言提供了 sync 包中的互斥锁 Mutex 和读写锁 RWMutex 来限制多个 goroutine 对同一个变量的访问3。
  • 你可以使用 sync.Mutex 的 Lock 和 Unlock 方法来保护临界区域,
  • 或者使用 sync.RWMutex 的 RLock 和 RUnlock 方法来允许多个读操作同时进行,但只允许一个写操作进行。

goroutine和线程区别?

  • goroutine 是 Go 语言运行时管理的轻量级线程,而线程是操作系统管理的执行单元。
  • goroutine 的切换开销比线程小得多,因为 goroutine 的切换只涉及三个寄存器的值修改,而线程的切换涉及模式切换、寄存器刷新等操作。
  • goroutine 的栈空间比线程动态灵活,goroutine 一般只需要 2KB 的栈内存,而线程通常需要 2MB。goroutine 的栈大小会根据需要动态地伸缩,而线程的栈大小是固定不变的。
  • goroutine 可以轻松创建成千上万个,并发执行,而线程的数量受到操作系统和硬件资源的限制。

goroutine能创建多少个?

  • goroutine 的数量并不受核心数的限制,而是受到内存和调度器的限制。
  • 理论上,goroutine 的数量可以达到几百万个,但实际上这样会导致内存消耗过大和调度开销过高。
  • 因此,在实际开发中,需要根据具体的场景和需求来控制 goroutine 的并发数量。

排序

  • 可能不经常用到

基本排序

package main

import (
	"fmt"
	"sort"
)

type StudentTasks []StudentTask

func (s StudentTasks) Len() int {
	return len(s)
}
func (s StudentTasks) Less(i, j int) bool {
	return s[i].Score > s[j].Score
}
func (s StudentTasks) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

type StudentTask struct {
	Score int
}

func main() {
	// 创建一个学生切片
	students := StudentTasks{
		{90},
		{60},
		{80},
		{76},
	}
	sort.Sort(students)
	fmt.Println(students) // [{90} {80} {76} {60}]
}

Chan(缓冲区)

  • 多线程单Chan Vs 多线程多Chan
  • 由多线程多Chan处理性能更快(都设置了缓冲区)
  • 未设置缓存区的Chan 对比 设置缓存区的 Chan 所花费的时间要在2亿循环中,设置缓存区比未设置快大约5倍
package main

import (
	"fmt"
	"testing"
	"time"
)

func TestMultipleWritersToSingleChannel(t *testing.T) {
	// 创建一个带有缓冲区的通道
	ch := make(chan int, 100000)
	// 未设置缓存区 55.9750587s
	// 设置缓存区 11.7164381s

	// 启动多个协程写入数据
	for i := 0; i < 2; i++ {
		go func() {
			for j := 0; j < 100000000; j++ {
				ch <- j
			}
		}()
	}
	// 读取所有数据
	var count int
	start := time.Now()
	for range ch {
		count++
		if count == 200000000 {
			break
		}
	}
	elapsed := time.Since(start)
	// 检查是否读取了所有数据
	fmt.Println("处理数量 -> ", count)
	fmt.Printf("处理时间: %s\n", elapsed)
}

func TestMultipleWritersToMultipleChannels(t *testing.T) {
	// 创建多个带有缓冲区的通道
	ch1 := make(chan int, 100000)
	ch2 := make(chan int, 100000)
	ch3 := make(chan int, 2)

	go func() {
		for j := 0; j < 100000000; j++ {
			ch1 <- j
		}
		close(ch1)
	}()
	go func() {
		for j := 0; j < 100000000; j++ {
			ch2 <- j
		}
		close(ch2)
	}()
	// count 加锁
	var count int
	go func() {
		for {
			select {
			case _, ok := <-ch1:
				if ok {
					count++
				} else {
					ch3 <- 1
				}
			}
		}
	}()
	go func() {
		for {
			select {
			case _, ok := <-ch2:
				if ok {
					count++
				} else {
					ch3 <- 1
				}
			}
		}
	}()

	start := time.Now()
	for i := 0; i < 2; i++ {
		select {
		case <-ch3:
		}
	}
	fmt.Println("处理数量 -> ", count)
	elapsed := time.Since(start)

	fmt.Printf("处理时间: %s\n", elapsed)
}

协程上下文

简易版

package main

import (
	"bytes"
	"context"
	"fmt"
	"runtime"
	"strconv"
	"time"
)

// 定义一个上下文,测试是否线程安全
var ctx = NewLocalContext()

func main() {
	fmt.Println("main goroutine id:", ctx.GetGID())
	Go(1)
	Go(2)
	time.Sleep(1 * time.Second) // 等待1秒加入上下文
	fmt.Println(len(ctx.ctx))
	time.Sleep(5 * time.Second)
	fmt.Println(len(ctx.ctx))
}

func Go(num int) {
	go func() {
		for i := 0; i < 3; i++ {
			ctx.SaveCtx(num)
			// 打印
			time.Sleep(time.Second)
			// 打印协程id
			fmt.Println("上下文数据:", ctx.GetCtx(), "协程id:", ctx.GetGID())
		}
		ctx.ClearCtx()
	}()
}

func (*LocalContext) GetGID() uint64 {
	b := make([]byte, 64)
	b = b[:runtime.Stack(b, false)]
	b = bytes.TrimPrefix(b, []byte("goroutine "))
	b = b[:bytes.IndexByte(b, ' ')]
	n, _ := strconv.ParseUint(string(b), 10, 64)
	return n
}

// SaveCtx 保存上下文
func (l *LocalContext) SaveCtx(data interface{}) {
	background := context.Background() // TODO 暂时不可用
	background = context.WithValue(background, l.key, data)
	l.ctx[l.GetGID()] = background
}

// GetCtx 获取上下文
func (l *LocalContext) GetCtx() interface{} {
	return l.ctx[l.GetGID()].Value(l.key) // TODO 暂时不可用
}

// ClearCtx 清理上下文
func (l *LocalContext) ClearCtx() {
	delete(l.ctx, l.GetGID())
}

// LocalContext 本地上下文
type LocalContext struct {
	ctx map[uint64]context.Context
	key string
}

// NewLocalContext 创建本地上下文
func NewLocalContext() *LocalContext {
	return &LocalContext{key: "userIdKey", ctx: make(map[uint64]context.Context)}
}