golang并发编程基础-----channel

128 阅读4分钟

channel相关

1.数据类型

属于引用类型

2.声名方式

var 变量 chan 元素类型 || make(chan 元素类型, [缓冲大小])

3.常用操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。操作符号(<-)

①同步的channel

a.必须先存在接收方,然后才能存在发送方,这是个阻塞式的channel(同步)。

func main() {
   // 创建一个无缓冲区通道
   ch := make(chan string)
   // 接收通道的值
   go receive(ch)
   // 发送的数据
   ch <- "light"
   fmt.Println("发送成功")
   // 关闭通道
   close(ch)
   fmt.Println("通道已关闭")
}

// 接收通道的值
func receive(ch chan string) {
   // 接收的数据
   result := <-ch
   fmt.Println("接收的数据:" + result)
}

结果右图所示: image.png

b.如果先存在发送方,可以通过编译,但会报运行时异常---死锁(可以理解为生产者和消费者的关系,一定要先有生产者,才能有消费者,不然就单向阻塞)。

func main() {
   // 创建一个无缓冲区通道
   ch := make(chan string)
   // 发送的数据
   ch <- "light"
   fmt.Println("发送成功")
   // 关闭通道
   close(ch)
   fmt.Println("通道已关闭")
   // 接收通道的值
   go receive(ch)
}

// 接收通道的值
func receive(ch chan string) {
   // 接收的数据
   result := <-ch
   fmt.Println("接收的数据:" + result)
}

结果右图所示: image.png

②异步的channel

a.创建携带缓冲区的通道,且发送方数量不大于缓冲区数量(通道容量)。

func main() {
   // 创建一个容量为1的有缓冲区通道
   ch := make(chan string, 1)
   // 发送的数据
   ch <- "light"
   fmt.Println("发送成功")
   // 接收通道的值
   go receive(ch)
   // 关闭通道
   close(ch)
   time.Sleep(1)
   fmt.Println("通道已关闭")
}

// 接收通道的值
func receive(ch chan string) {
   // 接收的数据
   result := <-ch
   fmt.Println("接收的数据:" + result)
}

结果右图所示: image.png

b.若发送方数量大于缓冲区数量(通道容量),编译会通过,但运行会死锁,因为当发送方数量超过缓冲区数量的情况下,就不再属于异步执行了,而是同步阻塞,要等待缓冲区里的任务释放掉,才会执行后面的任务。

func main() {
   // 创建一个容量为1的有缓冲区通道
   ch := make(chan string, 1)
   // 发送的数据
   ch <- "light"
   ch <- "1"
   fmt.Println("发送成功")
   // 接收通道的值
   go receive(ch)
   // 关闭通道
   close(ch)
   time.Sleep(1)
   fmt.Println("通道已关闭")
}

// 接收通道的值
func receive(ch chan string) {
   // 接收的数据
   result := <-ch
   fmt.Println("接收的数据:" + result)
}

结果右图所示: image.png

③关闭通道

a.通道的特性:可以被自动回收,不需要每次都手动关闭,但如果手动关闭,必须注意以下几点:

channelnil已关闭的非空空的
关闭情况报panic报panic关闭成功,读取完数据后返回0值关闭成功,返回0值

4.channel存在的意义

channel存在的意义是为了解决在并发编程中数据交换的问题。一般在并发中实现数据交换常用的方式是使用共享内存,但这样有个缺陷就是内存中的数据不一定会准确复制到各个线程中,存在资源抢占的问题(即不能保证CAP原则中的C---数据一致性)。此种情况下,解决方案势必是使用互斥量对内存加锁来保证线程安全,这样就会存在性能上的问题。而channel就是天然可以解决这个问题,channel提倡通过通信来实现共享内存,而不是通过共享内存实现通信。channel本质上就是队列,遵循先进先出原则。各个goroutine(线程)之间都通过它来实现共享内存,可以发送/接收数据在同一个channel中。

5.综合演练

便于大家更好的理解和使用channel以及goroutine,自己做了一个简易版的ATM自动取款机,代码如下:

var group sync.WaitGroup

// Atm 取款机
type Atm struct {
   // users 用户信息
   users []User
   // balance 取款机余额
   balance int
}

// User 用户
type User struct {
   // Name 用户名称
   Name string
   // Money 取出的金额
   Money int
}

// TestAtm 简易版的ATM自动取款机
func TestAtm(t *testing.T) {
   atmChain := make(chan *Atm, 1)
   atmChain <- &Atm{balance: 100}
   fmt.Println("==========取款机已经准备就绪==========")
   userChain := make(chan *User, 4)
   for i := 0; i < 4; i++ {
      group.Add(1)
      id := i + 1
      go func() {
         defer group.Done()
         user := User{Name: "用户_" + strconv.Itoa(id), Money: 20 * id}
         userChain <- &user
      }()
   }
   group.Wait()
   close(userChain)
   fmt.Println("==========4个用户正在进行取款操作==========")
   group.Add(1)
   go WithdrawMoney(&atmChain, &userChain)
   fmt.Println("==========全部用户已经完成取款操作==========")
   group.Wait()
   // 打印取款机中的详情
   for atm := range atmChain {
      fmt.Printf("已成功取款的信息:%v\n", atm.users)
      fmt.Printf("取款机的余额:%d\n", atm.balance)
   }
}

// WithdrawMoney 取款操作
func WithdrawMoney(atm *chan *Atm, user *chan *User) {
   defer group.Done()
   ATM, ok := <-*atm
   if ok {
      balance := ATM.balance
      users := make([]User, 0)
      for u := range *user {
         balance -= u.Money
         if 0 <= balance {
            users = append(users, *u)
         } else {
            balance += u.Money
            break
         }
      }
      if 0 < len(users) {
         *atm <- &Atm{users: users, balance: balance}
         close(*atm)
      }
   }
}

结果右图所示: image.png