Golang 09 Goroutine

36

2025/7/11 15:00 - 2025/7/11 23:00

《Go 入门指南》 | Go 技术论坛

第十四章

协程与线程

在 Go 中,应用程序并发处理的部分被称作 <font style="background-color:rgb(249, 250, 250);">goroutines(协程)</font>,它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的,协程是轻量的,比线程更轻。

Go中并发不建议共享变量,而是通过Channel传递数据。

开启一个协程:

go func() 

协程的通道(Channel)

概念

通道是一种FIFO的队列,引用类型,其内部只能储存同一类型的数据,另外

  • 无缓冲通道,一次只能接收一个数据,直到这个数据被取出后发送者才能继续发送,否则阻塞,同样的,直到有数据可取接收者才能继续取出数据,否则阻塞。
  • 有缓冲通道,遵循FIFO,同样有阻塞。
var identifier chan datatype
//创建一个字符串通道,默认无缓冲
var ch1 chan string
ch1 = make(chan string)

var ch2 chan string
ch2 =make(chan string,10) //带10个单位缓冲的通道

通信操作符 <-

通道的数据根据箭头方向传输流动,流出通道的数据从通道中移除:

ch <- "hello" // 1流入通道
var s1
s1 <-ch //ch的第一个数据"hello"流向s1,此时通道内无数据了
<-ch //流出一个数据,即使无变量接收:
if <-ch !="world" {...} //合法

通道阻塞与死锁

无缓冲通道,发送数据前必须要有准备接收数据的协程 <-ch,否则会造成发送阻塞,从而造成接收阻塞形成死锁,因此一定要使用go func 同时进行发送和接收(main中没有go是因为它自己就是一个协程)。

以下程序就是先发送再开启接收协程导致死锁:

package main

import (
    "fmt"
)

func f1(in chan int) {
    fmt.Println(<-in)
}

func main() {
    out := make(chan int)
    out <- 2
    go f1(out)
}

将其调换位置并等待1s:

package main

import (
	"fmt"
	"time"
)

func f1(in chan int) {
	fmt.Println(<-in)
}

func main() {
	out := make(chan int)
	go f1(out)
	out <- 2
	time.Sleep(1000 * time.Millisecond)
}

有缓冲通道在缓冲区未满时不会出现阻塞。

基于无缓冲通道阻塞的性质,可以写以下代码:

package main

import (
	"fmt"
)

func main() {

	f2()
}

func f2() {
	ch := make(chan int)
	bigArray := make([]int, 100)
	for i := 0; i < 100; i++ {
		bigArray[i] = i
	}
	go sum(bigArray, ch) //计算结果流入通道
	// 其他代码
	sum := <-ch // 阻塞等到
	fmt.Println("sum:", sum)
}

func sum(bigArray []int, in chan int) {
	sum := 0
	for _, value := range bigArray {
		sum += value
	}
	in <- sum
}

这里 go sum(...) 是先开启发送数据协程,而非先发送数据,不会造成死锁。

通道关闭

一般而言不会去关闭通道,如果实在需要,也只能发送者去关闭,接收者不管:

package main

import (
	"fmt"
)


func main() {
	f2()
}

func f2() {
	ch := make(chan int)
	bigArray := make([]int, 100)
	for i := 0; i < 100; i++ {
		bigArray[i] = i
	}
	go sum(bigArray, ch) //计算结果流入通道
	// 其他代码
	// 1.检查通道是否关闭
	for {
		if data, ok := <-ch; ok {
			fmt.Println(data)
		} else {
			fmt.Println("Channel is closed")
			break
		}
	}
}

func sum(bigArray []int, in chan int) {
	defer close(in) // 2.计算结束后关闭通道
	sum := 0
	for _, value := range bigArray {
		sum += value
	}
	in <- sum

}

多个协程通道,select切换协程

准确意义是切换协程的通道,但是资料上都写的是切换协程。哪个管道先有数据了就切换到哪个管道接收数据让该管道的发送者协程继续发送数据,称为切换。

语法与switch类似,使用for循环监听多个协程的通道,一般不写default:

select {
    case v := <-ch1:
        fmt.Printf("Received on channel 1: %d\n", v)
    case v := <-ch2:
        fmt.Printf("Received on channel 2: %d\n", v)
    defalut:
        ...
}
package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go pump1(ch1)
	go pump2(ch2)
	go suck(ch1, ch2)

	time.Sleep(1e9)
}

func pump1(ch chan int) {
	for i := 0; i <= 10; i++ {
		ch <- i * 2
	}
}

func pump2(ch chan int) {
	for i := 0; i <= 10; i++ {
		ch <- i / 2
	}
}

func suck(ch1, ch2 chan int) {
	for {
		select {
		case v := <-ch1:
			fmt.Printf("Received on channel 1: %d\n", v)
		case v := <-ch2:
			fmt.Printf("Received on channel 2: %d\n", v)
		}
	}
}

补:2025/7/16 23:00

sync.Mutex和 sync.WaitGroup

sync.Mutex 互斥锁

sync.Mutex 用于控制并发对共享资源的访问,防止多个 goroutine 同时访问共享资源时发生数据竞争(data race)。它是一种互斥锁,保证在同一时间只有一个 goroutine 可以访问锁住的资源。

  • Lock():加锁,获取锁,防止其他 goroutine 同时访问被保护的资源。
  • Unlock():解锁,释放锁,让其他 goroutine 可以访问。

使用方式也简单,像Java的 ReentrantLock

var mutex sync.Mutex // 声明一个互斥锁
var counter int // 共享资源

func increment() {
    defer mutex.Unlock() // 释放锁
	mutex.Lock()   // 获取锁
	counter++      // 修改共享资源

}

注意事项

  1. 死锁:如果 goroutine 在加锁后没有解锁,或者加锁顺序不当,可能会导致程序死锁。确保每次加锁后都调用解锁。
  2. 解锁位置:解锁应尽量放在 defer 语句中,这样可以保证即使发生错误也会解锁。

sync.WaitGroup 等待组

sync.WaitGroup 用于等待一组 goroutine 完成。你可以使用它来等待多个并发任务结束,避免主 goroutine 提前退出。类似Java的 CountDownLatch

  • Add():设置等待组计数。
  • Done():每个 goroutine 执行完成时调用,表示任务已完成。
  • Wait():阻塞直到所有任务完成。
var wg sync.WaitGroup

// 启动多个 goroutine
for i := 0; i < 5; i++ {
	wg.Add(1) // 增加等待计数
	go func(i int) {
		defer wg.Done() // 完成任务时减少计数
		fmt.Println(i)
	}(i)
}

wg.Wait() // 等待所有 goroutine 完成

  • **wg.Add(1)** 在每个 goroutine 启动前调用,表示增加一个任务计数。
  • **defer wg.Done()** 确保每个 goroutine 完成时会减少计数。
  • **wg.Wait()** 会阻塞,直到计数为零,即所有 goroutine 完成。

组合使用

一个稍微具体的存款例子

package main

import (
	"fmt"
	"sync"
)

type BankAccount struct {
	balance int
	mutex   sync.Mutex // 用于保护余额的互斥锁
}

func (a *BankAccount) Deposit(amount int, wg *sync.WaitGroup) {
	defer wg.Done() // 每次操作完成后减少等待组计数
	a.mutex.Lock()  // 加锁,确保存款操作是线程安全的
	a.balance += amount
	a.mutex.Unlock() // 解锁
}

func (a *BankAccount) Withdraw(amount int, wg *sync.WaitGroup) {
	defer wg.Done() // 每次操作完成后减少等待组计数
	a.mutex.Lock()  // 加锁,确保取款操作是线程安全的
	if a.balance >= amount {
		a.balance -= amount
	} else {
		fmt.Println("Insufficient funds for withdrawal")
	}
	a.mutex.Unlock() // 解锁
}

func main() {
	// 初始化一个银行账户,余额为 1000
	account := &BankAccount{balance: 1000}

	// 等待组,用于等待所有 goroutine 完成
	var wg sync.WaitGroup

	// 启动多个存款和取款操作
	wg.Add(3) // 我们将启动 3 个存款操作和 2 个取款操作

	// 存款操作
	go account.Deposit(500, &wg)  // 存款 500
	go account.Deposit(300, &wg)  // 存款 300
	go account.Deposit(700, &wg)  // 存款 700

	// 取款操作
	wg.Add(2) // 再增加两个取款操作的等待计数
	go account.Withdraw(400, &wg) // 取款 400
	go account.Withdraw(1500, &wg) // 试图取款 1500(不足)

	// 等待所有的存款和取款操作完成
	wg.Wait()

	// 输出最终余额
	fmt.Println("Final Account Balance:", account.balance)
}

posted on 2025-07-16 23:20  依只  阅读(17)  评论(0)    收藏  举报

导航