MIT 6.824: Go并发相关的协程问题

1 为什么当结构体很大时,倾向于使用pointer receiver ?

可以避免对结构体进行复制,从而提高性能和减少内存占用;
但是要注意使用pointer receiver时不要出现空指针异常,在方法中要对指针接受者进行判断

2 当结构体中含有sync.Mutex类型的成员变量mutex时,为什么不能使用value receiver?

因为mutex作为互斥锁,其能够保证多个goroutine同步访问共享资源的原因是,**多个goroutine使用的是同一份互斥锁的实例,**一个goroutine抢占了这份实例,其他goroutine必须等待这份锁实例被释放。

如果使用value receiver的话,这个结构体实例本身会在每个goroutine中都被复制一份,那么实例中的mutex变量也相应地会被复制了,这样每个goroutine对应的是不同的互斥锁实例了,此时就起不到互斥的作用了。

3 goroutine并行运行时,如果协程的数目多于计算机的core数,那怎么办呢?

Go中使用**pre-emptive scheduling(抢占式调度)**来解决这个问题,就是定期地中断某些正在运行的goroutine,然后把控制权转交给已经等待了很久的其他的goroutine。在这个过程中,并不需要正在运行的这些goroutine的合作。

4 从high level的角度来说明一下,go的通道机制是如何工作的?go是如何确保如此多可能的goroutine之间的同步的?(后续不断补充)

go是一个由buffer和lock组成的结构体。当向通道发送消息时:先获取锁,等待其他线程接收通道里已经有点消息后再获取CPU资源,接着就传递消息;当从通道中接收消息时:同样也是先获取锁,然后等待有发送者往通道里面写入消息后,才能进行读取操作。

可以使用sync.Mutex和sync.Cond来实现自己的通道。

5 怎么创建一个通过互联网进行连接的Go通道?怎么来指定用于发送消息的协议?

Go通道只能在单个程序上来使用,通道不能跨程序或者跨电脑来进行通信,能实现这个功能的不是通道机制,而是Go的RPC包:https://golang.org/pkg/net/rpc/

6 Go中的slice/切片是怎么实现的?

由指向数组的指针、起始下标和结束下标 这三个参数来定义一个slice,不同的切片可以指向共同的底层数组,所以切片比数组灵活得多。

7 什么时候我们应该使用sync.WaitGroup而不是channel?什么时候我们使用channel而不是sync.WaitGroup呢?

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个数据,很明显只有当所有协程都结束,这段代码才会结束,只要有一个协程没有结束,也就是只要还有一个协程往通道里面还未写入数据,这段代码就会阻塞在这里,不会继续往下执行。

8 Go中如何最容易地来实现:每隔一秒来执行一次任务?

使用周期性的定时的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 {}
}

9 我们如何知道创建并发协程(spawing goroutine)的开销会超过我们从中收获的并发度(concurrency)?

在实现中观察到的结果是:随着协程数目的增加,throughput或者说吞吐量也会相应提高,但是再增加,吞吐量就会开始下降了。
比如如果有16 core的计算机,如果使用CPU平行,那么最多可以有16个协程并发,但是通常采取10个:比如0.1s内可以爬取一个网页,那么1个协程1s内就能爬取10个网页,10个并发的协程在1s内就能爬取100个网页了…

10 我正在使用通道来唤醒另外一个goroutine, 也就是给通道发送消息,但是这个goroutine已经在运行了,所以将无法接受我发送的消息,所以我这个发送goroutine将会被阻塞。我应该怎么做呢?

使用条件变量而不是使用通道。

因为通道本身就是用于同步的通信的,如果不确定是否会有一个goroutine恰好等待着接收通道中发送进去的变量,那么就会阻塞;而条件变量,则会提醒那些可能正在等待消息的goroutine,某些不需要等待消息的goroutine不会受到影响,如果没有任何一个goroutine在等待消息,也没有关系。

11 怎么可以让一个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语句同时监视多个通道,以实现并发处理多个通道的输入。

你可能感兴趣的:(golang,开发语言,后端)