Java转Go初步学习(并发)

本文介绍了作者从Java转向Go语言学习的经历,重点讲解了Go中的goroutine、信道(包括无缓冲和带缓冲通道)、select语句、sync.Mutex互斥锁以及如何使用WaitGroup和Context来控制并发。内容详细且实用,适合Go初学者参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近面试成功了腾讯教育子公司。以后得从事Go语言开发相关工作啦,特此在这记录下Go基础学习。
参考链接: https://tour.go-zh.org/


goroutine

goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部实现了goroutine间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。goroutine比thread更易用、更高效、更轻便。

Go程

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

// 会启动一个新的 Go 程并执行 f(x, y, z)
go f(x, y, z)

信道 (<-)

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。
这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

无缓冲通道

  • make(chan int),开辟的通道是一种无缓冲通道,所以当对这个缓冲通道写的时候,会一直阻塞等到某个协程对这个缓冲通道读

无缓存通道要先接受,后发送 (或者是用Go程发送)

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	// 将 sum  发送至信道 c。
	// 当对这个缓冲通道写的时候,会一直阻塞等到某个协程对这个缓冲通道读
	c <- sum // 将和送入 c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	// 从 c 接收值并赋予 x y。
	x, y := <-c, <-c // 从 c 中接收

	fmt.Println(x, y, x+y)
}

带缓冲的信道

信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道。
当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

// 前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。
ch := make(chan bool, 4)

range循环信道需要配合close关闭

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

之后 ok 会被设置为 false。

循环 for i := range c 会不断从信道接收值,直到它被关闭。
应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic

	queue := make(chan string, 2)
	queue <- "one"
	queue <- "two"
	// 在生产者的地方关闭channel
	close(queue)
	for elem := range queue {
		fmt.Println(elem)
	}

注意:*向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}
func main() {
	c := make(chan int, 10)
	// cap()函数返回的是数组切片分配的空间大小
	go fibonacci(cap(c), c)
	// range c 会不断从信道接收值,直到它被关闭。
	for i := range c {
		fmt.Println(i)
	}
}

select 语句

select 语句使一个 Go 程可以等待多个通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
当 select 中的其它分支都没有准备好时,default 分支就会执行。

  • 为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:
  • 利用select还可以设置超时时间
func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		// 只有当监听的channel中有发送或接收可以进行时才会运行
		select {
		case c <- x:
		    // 这里赋值结束才会传入信道
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		// 利用select来设置超时
		case <-time.After(5 * time.Second):
			println("timeout")
			return
		}
	}
}
func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

Go语言的定时器实质是单向通道,time.Timer结构体类型中有一个time.Time类型的单向chan。
当 select 中的其它分支都没有准备好时,default 分支就会执行。
为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。

func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	for {
		select {
		// 进行阻塞监听信息
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}

sync.Mutex 互斥

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

Lock
Unlock
  • 在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。
  • 用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	c.v[key]++
	c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	defer c.mux.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}
	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}

控制并发

控制并发有两种经典的方式,一种是WaitGroup,另外一种就是Context

WaitGroup (控制多个goroutine同时完成)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("1号完成")
        wg.Done()
    }()
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("2号完成")
        wg.Done()
    }()
    wg.Wait()
    // 先做好的就要等着其他未完成的,所有的goroutine要都全部完成才可以。
    fmt.Println("好了,大家都干完了,放工")
}

Context

chan通知 (chan + select )

在后台goroutine中,使用select判断stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行default里的监控逻辑,继续监控,只到收到stop的通知。

func main() {
    stop := make(chan bool)
    go func() {
        for {
            select {
            case <-stop:
                fmt.Println("监控退出,停止了...")
                return
            default:
                fmt.Println("goroutine监控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }()
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    stop<- true
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

Context上下文

我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,它就是goroutine的上下文。

不要把Context放在结构体中,要以参数的方式传递
以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
Context是县城安全的,可以放心的在多个goroutine中传递

func main() {
	// 这个空的Context一般用于整个Context树的根节点;创建一个可取消的子Context,然后当作参数传给goroutine使用,
	ctx, cancel := context.WithCancel(context.Background())
	//Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
	//Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。
	//Err方法返回取消的错误原因,因为什么Context被取消。
	//Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
	ctx = context.WithValue(ctx,"test","hahaha")
	go watch(ctx,"【监控1】")
	go watch(ctx,"【监控2】")
	go watch(ctx,"【监控3】")
	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	// 它是我们调用context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
	for {
		select {
		// 如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求
		case <-ctx.Done():
			fmt.Println(name,"监控退出,停止了...")
			return
		default:
			fmt.Println(name,"goroutine监控中...")
			fmt.Println(ctx.Value("test"))
			time.Sleep(2 * time.Second)
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值