Golang 并发 通道 select

本文介绍了Go语言中goroutine的基本使用、sync包的WaitGroup和Once功能,以及如何通过channel进行并发控制。通过实例演示了如何利用这些工具实现高效并发和同步,包括GOMAXPROCS设置和Channel在并发中的关键作用。

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

使用goroutine 

启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。

func hello() {
	time.Sleep(1)
	fmt.Println("Hello Goroutine!")
}

func main() {
	go hello()
	fmt.Println("main goroutine done!")
}

这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。为什么呢?
在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。

func main() {
	// 合起来写
	go func() {
		i := 0
		for {
			i++
			fmt.Printf("new goroutine: i = %d\n", i)
			time.Sleep(time.Second)
		}
	}()
	i := 0
	for {
		i++
		fmt.Printf("main goroutine: i = %d\n", i)
		time.Sleep(time.Second)
		if i == 2 {
			break
		}
	}
}

上面再次证明

func hello() {
	fmt.Println("Hello Goroutine!")
}

func main() {
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	time.Sleep(time.Second)
}

执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。首先为什么会先打印main goroutine done!是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。但是对于实际生活的大多数场景来说,1秒是不够的,并且大部分时候我们都无法预知for循环内代码运行时间的长短。这时候就不能使用time.Sleep() 来完成等待操作了。

sync.WaitGroup

在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。让我们再来一个例子: (这里使用了sync.WaitGroup来实现goroutine的同步)它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器地值减为0。

var wg sync.WaitGroup

func hello(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("Hello Goroutine!", i)
}

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hello(i)
	}
	wg.Wait() // 等待所有登记的goroutine都结束
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

sync.Once

sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等

runtime.Gosched

这个函数的作用是让当前goroutine让出CPU,好让其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")
    // 主协程
    for i := 0; i < 2; i++ {
        // 切一下,再次分配任务
        runtime.Gosched()
        fmt.Println("hello")
    }
}

runtime.GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}

Channel

Golang 中对Channel 的支持添加了select关键字,通过select可以监听channel上的数据流动Golang中基于Channel select的实现由监控、定时器等示例,用于处理异步IO操作。执行流程有点类似switch case,case 后只能接channel输出,可以用变量接收:num:=<-ch

若所有的case后的channel都没有输出,则继续循坏等待,若所有的case后的channel均有输出,则随机选择一个case执行。

select功能

在多个通道上进行读或写操作,让函数可以处理多个事情,但1次只处理1个。以下特性也都必须熟记于心:

  1. 每次执行select,都会只执行其中1个case或者执行default语句。
  2. 当没有case或者default可以执行时,select则阻塞,等待直到有1个case可以执行
  3. 当有多个case可以执行时,则随机选择1个case执行。
  4. case后面跟的必须是读或者写通道的操作,否则编译出错。

select长下面这个样子,由selectcase组成,default不是必须的,如果没其他事可做,可以省略default

func main() {
    readCh := make(chan int, 1)
    writeCh := make(chan int, 1)
 
    y := 1
    select {
    case x := <-readCh:
        fmt.Printf("Read %d\n", x)
    case writeCh <- y:
        fmt.Printf("Write %d\n", y)
    default:
        fmt.Println("Do what you want")
    }
}

我们创建了readChwriteCh2个通道:

  1. readCh中没有数据,所以case x := <-readCh读不到数据,所以这个case不能执行。
  2. writeCh是带缓冲区的通道,它里面是空的,可以写入1个数据,所以case writeCh <- y可以执行。
  3. case可以执行,所以default不会执行。

这个测试的结果是

Write 1

eat()函数会启动1个协程,该协程先睡几秒,事件不定,然后喊你吃饭,main()函数中的sleep是个定时器,每3秒喊你吃1次饭,select则处理3种情况:

  1. eatCh中读到数据,代表有人喊我吃饭,我要吃饭了。
  2. sleep.C中读到数据,代表闹钟时间到了,我要睡觉。
  3. default是,没人喊我吃饭,也不到时间睡觉,我就打豆豆
import (
    "fmt"
    "time"
    "math/rand"
)
 
func eat() chan string {
    out := make(chan string)
    go func (){
        rand.Seed(time.Now().UnixNano())
        time.Sleep(time.Duration(rand.Intn(5)) * time.Second)
        out <- "Mom call you eating"
        close(out)
    }()
    return out
}
 
 
func main() {
    eatCh := eat()
    sleep := time.NewTimer(time.Second * 3)
    select {
    case s := <-eatCh:
        fmt.Println(s)
    case <- sleep.C:
        fmt.Println("Time to sleep")
    default:
        fmt.Println("Beat DouDou")
    }
}

由于前2个case都要等待一会,所以都不能执行,所以执行default。default和下面的打印注释掉,多运行几次,有时候会吃饭,有时候会睡觉

select很简单但功能很强大,它让golang的并发功能变的更强大。

控制并发数

有时需要定时执行几百个任务,例如每天定时按城市来执行一些离线计算的任务。但是并发数又不能太高, 因为任务执行过程依赖第三方的一些资源,对请求的速率有限制。这时就可以通过 channel 来控制并发数。

package main

import (
	"fmt"
	"time"
)

var limit = make(chan int, 3)

func main() {
	// 通过channel控制最大并发数量
	tasks := [...]int{11, 22, 33, 44, 55, 66, 77, 88, 99, 100}
	for i, v := range tasks {
		// 为每一个任务开启一个goroutine
		go func(i, v int) {
			// 通过channel控制goroutine最大并发数量
			limit <- -1
			fmt.Println(i, v)
			time.Sleep(time.Second)
			<-limit
		}(i, v)
	}
	time.Sleep(time.Second * 4)
}

nil channels

下面我们看下nil 通道有什么特点,空通道对操作的反应如下:

  • 给一个 nil channel 发送数据,造成永远阻塞
  • 从一个 nil channel 接收数据,造成永远阻塞
  • 给一个已经关闭的 channel 发送数据,引起 panic
  • 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  • 无缓冲的channel是同步的,而有缓冲的channel是非同步的

15字口诀:“空读写阻塞,写关闭异常,读关闭空零”,往已经关闭的 channel 写入数据会 panic。

func main() {
	var ch chan int
	select {
	case v, ok := <-ch:
		println(v, ok)
	default:
		println("default")//ch为nil,读写都会阻塞,输出default
	}
}

在 select 语句中禁用一个 case

你的任务是编写一个函数, 给定两个 channels a 和 b 返回一个相同类型的 channel c. a 或 b 中收到的每个元素都将发送给 c, 并且一旦 a 和 b 都关闭, c 也将被关闭.

package main

import (
	"fmt"
	"log"
	"time"
)

func insert(vs ...int) <-chan int {
	c := make(chan int)
	go func() {
		for _, v := range vs {
			c <- v
			time.Sleep(1 * time.Second)
		}
		close(c)
	}()
	return c
}

func merge(a, b <-chan int) <-chan int {
	c := make(chan int)
	go func() {
		defer close(c)
		if a != nil || b != nil {
			for {
				select {
				case v, ok := <-a:
					if !ok {
						a = nil
						log.Printf("a chan is done")
						continue
					}
					c <- v
				case v, ok := <-b:
					if !ok {
						b = nil
						log.Printf("b chan is done")
						continue
					}
					c <- v
				}
			}
		}
	}()
	return c
}

func main() {
	a := insert(1, 2, 3, 4)
	b := insert(5, 6, 7, 8)
	c := merge(a, b)
	for v := range c {
		fmt.Println(v)
	}
}

### Golang 并发编程练习示例代码 #### 使用 `sync.WaitGroup` 实现并发任务等待完成 下面展示了一个使用 `sync.WaitGroup` 的简单例子,该程序启动多个 Goroutine 来模拟并发任务并等待它们全部完成。 ```go package main import ( "fmt" "sync" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("Worker %d starting\n", id) // Simulate some work with a sleep. for i := 0; i < 3; i++ { fmt.Printf("Worker %d is working...\n", id) } fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup numWorkers := 5 for i := 1; i <= numWorkers; i++ { wg.Add(1) go worker(i, &wg) } wg.Wait() fmt.Println("All workers have finished their tasks.") } ``` 此代码展示了如何通过 `WaitGroup` 控制多个 Goroutine 的生命周期[^4]。 --- #### 使用 Channel 进行消息传递的任务队列改造 基于引用中的计时器退出逻辑[^3],可以将其改造成一个简单的任务队列模型。以下是一个改进版本: ```go package main import ( "fmt" "time" ) type Task struct { Name string Duration time.Duration } var taskQueue chan Task var exitChan chan bool func processTasks(ticker *time.Ticker) { for { select { case t := <-taskQueue: fmt.Printf("Processing task: %s\n", t.Name) time.Sleep(t.Duration) fmt.Printf("Task completed: %s\n", t.Name) case <-ticker.C: fmt.Println("Checking for new tasks...") case <-exitChan: fmt.Println("Exiting the processor goroutine.") return } } } func main() { taskQueue = make(chan Task, 10) exitChan = make(chan bool, 1) ticker := time.NewTicker(time.Second * 2) go processTasks(ticker) // Add tasks to the queue for i := 1; i <= 5; i++ { task := Task{Name: fmt.Sprintf("Task-%d", i), Duration: time.Second} taskQueue <- task if i == 3 { close(taskQueue) // Close channel after adding all tasks (optional). } } // Exit signal after processing all tasks time.Sleep(time.Second * 8) exitChan <- true } ``` 上述代码创建了一个任务队列,并利用通道来管理任务的分发和处理过程。 --- #### 使用 Select 和 Timeout 处理超时场景 以下代码演示了如何在 Go 中使用 `select` 和 `time.After` 函数实现超时控制[^2]。 ```go package main import ( "fmt" "time" ) func fetchData(ch chan<- string) { time.Sleep(3 * time.Second) // Simulating network delay or computation ch <- "Data fetched successfully!" } func main() { dataCh := make(chan string, 1) timeoutCh := time.After(2 * time.Second) go fetchData(dataCh) select { case data := <-dataCh: fmt.Println("Received:", data) case <-timeoutCh: fmt.Println("Request timed out!") } } ``` 在此示例中,如果数据获取超过指定的时间,则会触发超时机制。 --- #### 使用 Mutex 和 Condition 变量同步访问共享资源 以下代码展示了如何使用 `sync.Mutex` 或 `sync.Cond` 同步对共享变量的访问。 ```go package main import ( "fmt" "sync" ) func main() { var mu sync.Mutex count := 0 increment := func(wg *sync.WaitGroup) { mu.Lock() defer mu.Unlock() count++ fmt.Println("Count incremented:", count) wg.Done() } var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go increment(&wg) } wg.Wait() fmt.Println("Final Count:", count) } ``` 这段代码说明了如何防止竞争条件的发生,从而确保线程安全的操作。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值