[go]tchannel

97 阅读6分钟

简介

在 Go 语言中,`channel`是一个很重要的部分,经常在 Go 中的`并发`中使用。
`channel`类似 [unix]() 中的管道(pipe)。

`channel`里的数据遵循`先进先出`的原则,且线程安全,多个`goroutine`访问时,不需要加锁,自己就可以实现线程安全。
channel本身就是线程安全的。

`channel`是有类型的,一个`string`类型的`channel`只能存放`string`类型的数据。
一个int类型的channel只能存放int类型的数据。


你可以通过`make`函数来创建一个`channel`,第二个参数为`channel`的容量,及在同一时间,channel中最多只能存放n个数据,等数据被取出之后,然后可以继续存放数据。

向`channel`写入数据时,不能超过其容量。
当从`channel`读取数据时,如果`channel`中没有数据,则会阻塞当前协程,直到`channel`中有数据可读。

tchannel的特点

-   go语言里的管道(channel)类似unix中的管道(pipe)。
-   channel里的数据遵循先进先出的原则。
-   不加锁多个goroutine同时操作管道里的数据时不会出现数据冲突。
-   channel是有类型的。

channel一般用在什么场景中

  1. 并发通信:在多个 goroutine 之间进行数据传递和通信。

  2. 任务分配:将任务分配给不同的 goroutine 进行处理。

  3. 数据共享:允许不同的 goroutine 访问和修改共享的数据。

  4. 生产者-消费者模式:一个 goroutine 生产数据,另一个 goroutine 消费数据。

  5. 异步操作:进行异步的操作,提高程序的性能和响应性。

  6. 信号传递:用于传递特定的信号或事件。

  7. 限制并发数:控制同时执行某个操作的 goroutine 数量。

  8. 线程安全的数据结构:构建线程安全的数据结构

  9. 管道式编程:类似于管道的方式处理数据。

  10. 任务协调:协调多个 goroutine 之间的工作流程。

    例如,在生产者-消费者模式中,生产者将数据发送到 channel,而消费者从 channel 中接收数据进行处理。
    channel 提供了一种安全的方式在不同的 goroutine 之间进行通信和协调,不需要加锁,是安全的。
    

如何定义一个channel对象

int类型的管道定义、初始化与写和读数据

package main

import "fmt"

func main() {
    var intChan chan int              // 定义int类型的管道,chan int为管道的类型
    intChan = make(chan int, 4)       // 初始化管道的长度
    
    
    intChan <- 10             // 往管道里写入数据
    num := <-intChan          // 从管道里读取数据
    fmt.Println(num)          // 结果为:10
}

字符串类型的管道定义、初始化与写和读操作

package main

import "fmt"

func main() {
    var strChan chan string               // 定义字符串类型的管道
    strChan = make(chan string, 4)        // 初始化管道长度
    
    intChan <- "is string"          // 往管道里写入数据
    str := <-intChan                // 从管道里读取数据
    fmt.Println(str)                // 结果为:is string
}

map类型的管道定义、初始化与写和读操作

package main

import "fmt"

func main() {
    var mapChan chan map[string]string
    mapChan = make(chan map[string]string, 4)
    
    // 一个map对象
    m := make(map[string]string)
    m["stu01"] = "001"
    m["stu02"] = "002"
    mapChan <- m         // 将数据写入管道

    outm := <-mapChan   // 从管道获取数据
    fmt.Println(outm)
}

自定义结构体类型的管道定义、初始化与写和读操作

package main

import "fmt"

type Student struct {
    Name string
}


func main() {
    var stuChan chan *Student
    stuChan = make(chan *Student, 10)
    stu := Student{Name: "stu01"}
    stuChan <- &stu          // 写入变量的地址

    outstu := <-stuChan      // 读取内容(读取的也是变量的地址)
    fmt.Println(*outstu)     // 打印时需要加*号获取值,否则获取到的是地址
}

interface类型的管道定义、初始化、写、读操作与类型转换

package main

import "fmt"

type Student struct {
    Name string
}

func main() {
    var stuChan chan interface{}
    stuChan = make(chan interface{}, 10)

    stu := Student{Name: "interstu"}
    stuChan <- &stu

    var stu01 interface{}
    stu01 = <-stuChan
    fmt.Printf("stu01: type[%T],data[%s]\n", stu01, stu01)

    // 类型转换(将interface类型的变量stu01转换为Student类型的变量stu02)
    var stu02 *Student
    // 类型断言判断是否可以转换
    stu02, ok := stu01.(*Student)
    if !ok {        // 如果不可转换打印错误信息
        fmt.Println("con not convert", ok)
        return
    }
    fmt.Printf("stu02: type[%T],data[%s]\n", stu02, stu02)     // 打印转换结果
}

channel的遍历

channel 支持 for–range 的方式进行遍历,请注意两个细节

1.  在遍历时,如果 channel 没有关闭,则会出现 deadlock 的错误, 因此遍历前需要关闭channel
2.  在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
   func main() {
        
        // 定义一个int类型的通道
	var intChan chan int
	intChan = make(chan int,100)
         
        // 循环往里写数据
	for i := 0; i < 100; i++ {
		intChan <- i
	}
 
	// 如果不关闭则会deadlock
        // 关闭了,就不会deadlock
	close(intChan)
	
	for v := range intChan{
		fmt.Println(v)
	}
}

channel的使用注意事项

channel 中只能存放指定的数据类型
channle 的数据放满后,就不能再放入了
如果从 channel 取出数据后,可以继续放入在没有使用协程的情况下,如果 channel 数据取完了,再取,就     会报 dead lock
当管道的类型为任意类型时,从管道获取出来的值必须经过类型断言才能操作或者参与运算

使用select 可以解决从管道取数据的阻塞问题

package main
import (
	"fmt"
	"time"
)
 
func main() {
    // 使用select可以解决从管道取数据的阻塞问题

    // 1.定义一个管道 10个数据int
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
        intChan <- i
    }
    // 2.定义一个管道 5个数据string
    stringChan := make(chan string, 5)
    for i := 0; i < 5; i++ {
        stringChan <- "hello" + fmt.Sprintf("%d", i)
    }

    //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock

    //问题,在实际开发中,可能我们不好确定什么关闭该管道.
    //可以使用select 方式可以解决
    //label:
    for {
            select {
                    //注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
                    //,会自动到下一个case匹配
                    case v := <-intChan : 
                            fmt.Printf("从intChan读取的数据%d\n", v)
                            time.Sleep(time.Second)
                    case v := <-stringChan :
                            fmt.Printf("从stringChan读取的数据%s\n", v)
                            time.Sleep(time.Second)
                    default :
                            fmt.Printf("都取不到了,不玩了, 可以加入逻辑\n")
                            time.Sleep(time.Second)
                            return 
                            //break label
            }
    }
}

如何设置通道的大小

make(chan data_type, buffer_size)

其中,`data_type``channel`中要传递的数据类型,
`buffer_size`表示`channel`的缓冲区大小,如果不指定缓冲区大小,则表示`channel`是无缓冲的。

无缓冲的`channel`可以用于同步操作,例如两个`Go`程之间的阻塞和等待。
有缓冲的`channel`可以用于异步操作,这意味着发送和接收操作不会阻塞程序的执行。



ch := make(chan string,10)

在这里,创建了一个传递字符串类型的`channel`,设置缓冲区大小为10。
有缓冲的`channel`可以用于异步操作,这意味着发送和接收操作不会阻塞程序的执行。

需要注意的是,`channel`的缓冲区大小需要根据实际情况进行设置。
如果缓冲区大小设置得过小,可能会导致频繁的阻塞和等待,从而影响程序的性能。
如果缓冲区大小设置得过大,则会浪费内存资源。


因此,在设置`channel`的缓冲区大小时,需要综合考虑程序的性能和内存使用情况。