可以避免对结构体进行复制,从而提高性能和减少内存占用;
但是要注意使用pointer receiver时不要出现空指针异常,在方法中要对指针接受者进行判断
因为mutex作为互斥锁,其能够保证多个goroutine同步访问共享资源的原因是,**多个goroutine使用的是同一份互斥锁的实例,**一个goroutine抢占了这份实例,其他goroutine必须等待这份锁实例被释放。
如果使用value receiver的话,这个结构体实例本身会在每个goroutine中都被复制一份,那么实例中的mutex变量也相应地会被复制了,这样每个goroutine对应的是不同的互斥锁实例了,此时就起不到互斥的作用了。
Go中使用**pre-emptive scheduling(抢占式调度)**来解决这个问题,就是定期地中断某些正在运行的goroutine,然后把控制权转交给已经等待了很久的其他的goroutine。在这个过程中,并不需要正在运行的这些goroutine的合作。
go是一个由buffer和lock组成的结构体。当向通道发送消息时:先获取锁,等待其他线程接收通道里已经有点消息后再获取CPU资源,接着就传递消息;当从通道中接收消息时:同样也是先获取锁,然后等待有发送者往通道里面写入消息后,才能进行读取操作。
可以使用sync.Mutex和sync.Cond来实现自己的通道。
Go通道只能在单个程序上来使用,通道不能跨程序或者跨电脑来进行通信,能实现这个功能的不是通道机制,而是Go的RPC包:https://golang.org/pkg/net/rpc/
由指向数组的指针、起始下标和结束下标 这三个参数来定义一个slice,不同的切片可以指向共同的底层数组,所以切片比数组灵活得多。
waitGroup是用于特殊场景的,也就是当前协程等待所有其他的goroutine都完成;而channel是用于一般的交换传递值这样的场景。
channel是可以实现waitGroup的功能的,也就是可以使用channel来等待多个go协程的完成,但是需要更多行的代码. 这个我用chatGPT找到满足条件的代码是:
package main
import (
"fmt"
"time"
)
func worker(id int, done chan<- bool) {
// 模拟工作耗时
time.Sleep(time.Second)
fmt.Printf("Worker %d 完成工作\n", id)
done <- true
}
func main() {
numWorkers := 5
done := make(chan bool, numWorkers)
// 启动多个协程
for i := 1; i <= numWorkers; i++ {
go worker(i, done)
}
// 等待所有工作完成
for i := 1; i <= numWorkers; i++ {
<-done
}
close(done)
fmt.Println("所有工作已完成")
}
上面这个例子中构建了一个缓冲大小与goroutine数目一样大的通道done,每个go协程在运行结束之后都会往done通道写入一个dummy bool,那么当所有协程都运行完,那么done通道内的协程数刚好达到buffer size。但是由于
for i := 1; i <= numWorkers; i++ {
<-done
}
这段代码会要求从通道中读取buffer size个数据,很明显只有当所有协程都结束,这段代码才会结束,只要有一个协程没有结束,也就是只要还有一个协程往通道里面还未写入数据,这段代码就会阻塞在这里,不会继续往下执行。
使用周期性的定时的goroutine来实现: select {} 是一个空的选择语句,它在没有任何可执行的 case 时会阻塞当前的goroutine。它的作用是让主goroutine保持运行状态,以防止程序提前退出。
package main
import (
"fmt"
"time"
)
func periodicTask() {
for {
// 执行任务逻辑
fmt.Println("执行任务...")
// 暂停一秒钟
time.Sleep(time.Second)
}
}
func main() {
// 启动goroutine执行周期性任务
go periodicTask()
// 主goroutine继续执行其他逻辑
fmt.Println("主程序继续执行...")
// 防止主goroutine退出
select {}
}
在实现中观察到的结果是:随着协程数目的增加,throughput或者说吞吐量也会相应提高,但是再增加,吞吐量就会开始下降了。
比如如果有16 core的计算机,如果使用CPU平行,那么最多可以有16个协程并发,但是通常采取10个:比如0.1s内可以爬取一个网页,那么1个协程1s内就能爬取10个网页,10个并发的协程在1s内就能爬取100个网页了…
使用条件变量而不是使用通道。
因为通道本身就是用于同步的通信的,如果不确定是否会有一个goroutine恰好等待着接收通道中发送进去的变量,那么就会阻塞;而条件变量,则会提醒那些可能正在等待消息的goroutine,某些不需要等待消息的goroutine不会受到影响,如果没有任何一个goroutine在等待消息,也没有关系。
第一种解决方案:
当你有多个不同通道的输入需要处理时,你可以为每个通道创建一个单独的goroutine。
假设你有三个通道:channelA、channelB和channelC。你可以分别创建三个goroutine,每个goroutine负责监听和处理一个通道。例如,一个goroutine可以通过在channelA上接收数据来等待来自channelA的输入,另一个goroutine可以通过在channelB上接收数据来等待来自channelB的输入,以此类推:
go func() {
data := <-channelA
// 处理从channelA接收到的数据
}()
go func() {
data := <-channelB
// 处理从channelB接收到的数据
}()
go func() {
data := <-channelC
// 处理从channelC接收到的数据
}()
每个goroutine都会在对应的通道上阻塞,直到有数据可读。这种方式能够同时监听并处理多个通道的输入,因为当一个通道上有数据可读时,对应的goroutine会被唤醒并开始处理数据。
请注意,这种方法的适用性取决于你的具体需求和情况。如果你有大量的通道需要处理,或者通道之间的处理逻辑非常复杂,可能会变得不太实际
第二种解决方案
使用select语句同时监视多个通道
package main
import (
"fmt"
"time"
)
func main() {
channelA := make(chan int)
channelB := make(chan int)
channelC := make(chan int)
go func() {
time.Sleep(2 * time.Second)
channelA <- 1
}()
go func() {
time.Sleep(3 * time.Second)
channelB <- 2
}()
go func() {
time.Sleep(4 * time.Second)
channelC <- 3
}()
for i := 0; i < 3; i++ {
select {
case data := <-channelA:
fmt.Println("Received from channelA:", data)
case data := <-channelB:
fmt.Println("Received from channelB:", data)
case data := <-channelC:
fmt.Println("Received from channelC:", data)
}
}
}
在这个例子中,我们创建了三个通道:channelA、channelB和channelC。然后,我们启动了三个goroutine,每个goroutine在不同的时间点往对应的通道中发送数据。主函数中的select语句用于同时监视这三个通道。当任意一个通道有数据可读时,对应的case分支会被执行。在每次循环中,我们使用select语句选择一个可用的通道,并打印接收到的数据。运行以上代码,你会看到输出的顺序可能会不同,这取决于各个goroutine发送数据的时间点。这个例子演示了如何使用select语句同时监视多个通道,以实现并发处理多个通道的输入。