一、并发概念
并发是指程序同时执行多个任务的能力。在计算机领域,可以理解为一个处理器同时处理多个任务的能力。
多线程是实现并发的一种方式,它是指在一个进程中开启多个线程,每个线程执行不同的任务。这些线程可以并发执行,提高程序的效率。但同时多个线程还需要协调配合,在访问共享资源时要保证互斥,否则会出现数据竞争问题。
Go语言中goroutine是实现并发的一个重要机制,本文将重点介绍goroutine之间如何进行通信,以及其中的一种通信机制channel(管道)。
二、goroutine及其创建
goroutine是一种轻量级的线程,由Go语言的运行时(runtime)管理。使用goroutine可以方便地实现并发操作。
使用关键字go来创建goroutine:
func main() {
go func() {
fmt.Println("Hello, goroutine!")
}()
fmt.Println("Hello, main!")
}
上述代码中,通过调用go语句,创建一个新的goroutine,用于执行匿名函数。同时main函数也会照常执行。
三、channel介绍
channel是goroutine之间通信的一种方式,是一种类型安全的、同步的、阻塞式的数据传输机制。channel可以用来传输数据,或者用来同步goroutine的执行。
使用关键字make来创建channel:
// 声明一个int类型的channel
var ch chan int
// 创建一个int类型的channel
ch = make(chan int)
上述代码中,通过make函数创建了一个int类型的channel。channel是可以设置缓冲区大小的,也就是说可以先把数据存储在缓冲区中。
创建带缓冲区的channel:
// 声明一个带缓冲区的string类型channel
var ch chan string
// 创建一个带缓冲区大小为10的string类型channel
ch = make(chan string, 10)
以上代码创建了一个带缓冲区大小为10的string类型channel。
四、channel的读写操作
4.1 向channel写入数据
使用<-符号来向channel写入数据:
ch := make(chan int)
go func() {
// 向channel写入数据
ch <- 10
}()
// 从channel中读取数据
data := <-ch
fmt.Println(data) // 10
上述代码中,首先创建一个int类型的channel,然后在一个goroutine中向channel中写入数据10,在主函数中通过<-符号读取数据。
4.2 从channel中读取数据
使用<-符号来从channel中读取数据:
ch := make(chan int)
go func() {
// 向channel写入数据
ch <- 10
}()
// 从channel中读取数据
data := <-ch
fmt.Println(data) // 10
上述代码中,在goroutine中向channel中写入数据10,然后在主函数中通过<-符号读取数据。
如果channel中没有数据,读取操作会被阻塞,直到有数据可读。
五、channel的阻塞与非阻塞
5.1 阻塞式读取
当从channel中读取数据时,如果channel中没有数据可读,读取操作会被阻塞,直到有数据可读。
ch := make(chan int, 1)
go func() {
ch <- 10
}()
data := <-ch
fmt.Println(data) // 10
以上代码中,首先创建一个带缓冲区大小为1的int类型channel,向channel中写入数据10,然后立刻读取数据,因为缓冲区里有数据,所以读取成功,输出结果为10。
如果去掉make(chan int, 1)中的1,变成make(chan int),则会出现deadlock(死锁)的问题,因为主函数一直等待读取数据,而goroutine中没有向channel中写入数据,因为channel没有缓冲区,所以写入操作会被阻塞,从而导致死锁。
5.2 非阻塞式读取
可以使用select语句配合default子句来实现非阻塞式读取。
ch := make(chan int)
go func() {
time.Sleep(1 * time.Second)
ch <- 10
}()
select {
case data := <-ch:
fmt.Println(data) // 10
default:
fmt.Println("no data received")
}
以上代码中,在goroutine中向channel中写入数据,但是需要等待1秒钟才会完成。使用select语句来读取channel中的数据,如果channel中有数据可读,则读取数据并输出,如果channel中没有数据可读,则执行default子句。
六、关闭channel
关闭channel表示不能再向channel中写入数据,但是仍然可以从channel中读取数据。通常情况下,关闭channel是为了告诉接收方已经不会再有新的数据了。
在读取channel时,可以判断channel是否关闭:
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
for {
data, ok := <-ch
if !ok {
fmt.Println("channel closed")
break
}
fmt.Println(data)
}
以上代码中,创建一个int类型的channel,向其中写入数据,然后关闭channel。在for循环中不断读取channel中的数据,如果channel被关闭,则ok的值为false,跳出循环。
七、select语句
select语句可以同时对多个channel进行等待操作,一旦有任意一个channel可读,就执行相应的操作。
ch1 := make(chan int)
ch2 := make(chan string)
go func() {
ch1 <- 10
}()
go func() {
ch2 <- "hello"
}()
select {
case data := <-ch1:
fmt.Println("channel 1:", data)
case data := <-ch2:
fmt.Println("channel 2:", data)
}
以上代码中,使用select语句同时等待ch1和ch2两个channel中的数据,当任意一个channel中有数据可读时,就执行相应的操作。
八、总结
本文介绍了goroutine、channel以及channel的读写、阻塞、非阻塞、关闭和select等机制,通过实例代码详细介绍channel的应用场景和实现方法。
使用channel是Go语言中实现并发的重要手段,掌握它可以方便地进行并发编程,提高程序的效率和质量。