golang中创建协程以及协程间的相互通信

golang中创建协程以及协程间的相互通信。

在golang中创建协程

在 Go 语言中,使用协程来实现并发模型。

协程是 Go 语言的并发执行单元,它比传统的线程更轻量级,允许我们并发执行多个任务。

Go 会在内部使用一组线程来运行创建的协程,并在这些线程之间高效地分配协程执行,这样可以在不增加太多操作系统线程的情况下执行大量的协程。

在golang中,我们可以方便的使用go func() {}() 语句用于启动一个新的协程(goroutine)。

协程是 Go 语言的并发执行单元,它比传统的线程更轻量级,允许我们并发执行多个任务。

当在函数调用前加上 go 关键字时,这个函数就会在新的协程中异步执行。

下面是一个简单的在golang主函数中创建协程的例子:

func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
        time.Sleep(time.Second)
    }
}

func main() {
    // 启动一个新的协程来执行 printNumbers 函数
    go printNumbers()

    // 主协程中执行其他工作
    for i := 'a'; i <= 'e'; i++ {
        fmt.Printf("%c", i)
        time.Sleep(time.Second)
    }

    // 等待足够的时间以确保 printNumbers 协程可以完成执行
    time.Sleep(6 * time.Second)
    fmt.Println("main function finished")
}

我们在新的协程中,启动了一个printNumbers()函数,运行结果如下:a12bc3d4e5

这说明我们成功创建了一个协程,这个协程和主程序并发运行。上述代码也可以写成下面的形式:

func main() {
    // 启动一个新的协程来执行 printNumbers 函数
    go func(){    for i := 1; i <= 5; i++ {
        fmt.Println(i)
        time.Sleep(time.Second)
    }}()

    // 主协程中执行其他工作
    for i := 'a'; i <= 'e'; i++ {
        fmt.Printf("%c", i)
        time.Sleep(time.Second)
    }

    // 等待足够的时间以确保 printNumbers 协程可以完成执行
    time.Sleep(6 * time.Second)
    fmt.Println("main function finished")
}

我们再次运行发现,运行结果如下:a1b23c45e,可以看到虽然代码是一样的,但是打印出字母和数字的顺序却不同。

这是为什么呢?因为我们只开启了一个协程,并没有控制协程和主程序的执行顺序,所以每次运行,有时协程运行的快些,有时主程序运行的快些。

那么如果我们想要打印一个字母,再打印一个数字,要怎么办呢?这就需要用到golang提供的,协程间的通信机制。

golang中进程间的通信

在gonglang中,可以使用管道来进行协程间的通信。

管道的创建和使用

通过 make(chan Type)可以创建一个管道,其中 Type是管道将传递的数据类型。我们通过通道对上述代码进行改造:

func main() {
	ch := make(chan bool)   // 用于从 printNumbers 协程到主协程的同步
	done := make(chan bool) // 用于从主协程到 printNumbers 协程的同步

	// 启动一个新的协程来执行 printNumbers 函数
	go func() {
		for i := 1; i <= 5; i++ {
			fmt.Printf("%d", i)
			ch <- true // 发送数字到主协程
			<-done     // 等待主协程的信号
		}
		close(ch) // 关闭通道
	}()

	// 主协程中执行其他工作
	for i := 'a'; i <= 'e'; i++ {
		<-ch // 等待来自 printNumbers 协程的数字
		fmt.Printf("%c", i)
		done <- true // 发送信号到 printNumbers 协程
	}

	fmt.Println("main function finished")
}

在上述代码中,我们通过通道chdone进行进程间的消息传递,主程序通过<-ch收到ch通道传来的信号后,才打印出数字。

同样,协程收到来自主程序通道传来的信号后,再继续打印字母。

这样就可以保证打印的顺序始终为1a2b3c4d5

管道通信中传递的值

在我们的代码中使用了<-ch,即没有关心通道传来的具体值,如果需要用到通道传递过来的值,可以使用a<-ch,这样通道传来的值就会被存储在变量a中。

有缓冲的管道通信

此外管道可以是无缓冲的,也可以是有缓冲的,我们上述使用的是无缓冲的通道。

无缓冲管道在发送和接收之间是同步的:发送操作会阻塞,直到另一端的协程执行接收操作,而接收操作也会阻塞,直到有数据被发送。这种同步通信机制确保了协程间的同步执行。

有缓冲的管道则允许在协程间异步传输数据。当你创建一个有缓冲的管道时,你需要指定缓冲区的大小。有缓冲的管道可以存储多个元素,直到达到其缓冲区的容量为止。发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区空时才会阻塞。

创建有缓冲的管道的示例:ch := make(chan int, 5)

在这个例子中,ch 是一个可以存储最多 5 个整数的有缓冲管道。这意味着我们可以在没有接收方的情况下向管道发送最多 5 个整数,而不会发生阻塞。

golang中进程间的同步控制

golang中当协程的父程序即创建协程的程序结束时,父程序中的协程会全部终止

在文章最开始的代码中,我们通过使用 time.Sleep(6 * time.Second)来确保在协程结束之前,主程序不会停止,这其实是非常不好的做法。

同步原语sync.WaitGroup

实际上,在gonglang中,一般使用WaitGroup来进行进程间的同步控制,确保在子协程全部结束时,父协程才会终止。

sync.WaitGroup 是 Go 语言中的一个同步原语,用于等待一组协程(goroutines)的完成。它主要用于确保所有协程都已经完成它们的工作。

WaitGroup中常用的三个函数:Add()、Done()和Wait()

在使用sync.WaitGroup之前我们首先要学习其Add()、Done()和Wait()函数。

wg.Add(delta int) 方法用于设置等待组中的协程数量。每次调用 wg.Add() 时,传递的参数(通常是 1)将会被加到内部计数器上。这个方法通常在启动协程之前调用,以表明有新的协程需要等待。

wg.Done() 方法用于表示一个协程完成了它的工作。每当协程完成它的执行时,它应该调用 wg.Done(),这将从内部计数器减去 1。

当内部计数器减到 0 时,任何阻塞在 wg.Wait() 调用上的协程都会停止阻塞,继续执行。

wg使用实例

通过下面这个函数我们可以确保所有子协程全部完成之后,主程序才会终止:


func main() {
	// 启动一个新的协程来执行 printNumbers 函数
	var wg sync.WaitGroup
	// 等待协程数增加一
	wg.Add(1)
	go func() {
		for i := 1; i <= 5; i++ {
			fmt.Println(i)
		}
		//协程完成等待协程数减少1
		wg.Done()
	}()

	wg.Add(1)
	go func() {
		for i := 'a'; i <= 'e'; i++ {
			fmt.Printf("%c", i)
		}
		wg.Done()
	}()

	// 等待所有的协程完成
	wg.Wait()
	fmt.Println("main function finished")
}

通过这种方法,我们发现,只有当两个子程序都运行完成后,才会打印出main function finished。

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