初识golang-数据类型_channel

查看上一篇结构体类型请点我

一.goroutine

说到channel,就不得不提golang的goroutine,这是golang原生支持高并发很重的一点。并发模型有5种:

1.单进(线)程·循环处理请求

        单进程和单线程其实没有区别,因为一个进程至少有一个线程。循环处理请求应该是最初级的做法。当大量请求进来时,单线程一个一个处理请求,请求很容易就积压起来,得不到响应。这是无并发的做法。

2.多进程

        主进程监听和管理连接,当有客户请求的时候,fork 一个子进程来处理连接,父进程继续等待其他客户的请求。但是进程占用服务器资源是比较多的,服务器负载会很高。这种架构的最大的好处是隔离性,子进程万一 crash 并不会影响到父进程。缺点就是对系统的负担过重。

3.多线程

        和多进程的方式类似,只不过是替换成线程。主线程负责监听、accept()连接,子线程(工作线程)负责处理业务逻辑和流的读取。子线程阻塞,同一进程内的其他线程不会被阻塞。缺点是会频繁地创建、销毁线程,这对系统也是个不小的开销。这个问题可以用线程池来解决。线程池是预先创建一部分线程,由线程池管理器来负责调度线程,达到线程复用的效果,避免了反复创建线程带来的性能开销,节省了系统的资源。同时还需要处理同步的问题,当多个线程请求同一个资源时,需要用锁之类的手段来保证线程安全。同步处理不好会影响数据的安全性,也会拉低性能。最重要的一点,一个线程的崩溃会导致整个进程的崩溃。

4.单线程·回调(callback)和事件轮询

        主进程(master 进程)首先通过 socket() 来创建一个 sock 文件描述符用来监听,然后fork生成子进程(workers 进程),子进程将继承父进程的 sockfd(socket 文件描述符),之后子进程 accept() 后将创建已连接描述符(connected descriptor)),然后通过已连接描述符来与客户端通信。采用此种方式最经典的就是Nginx。

5.协程

        协程基于用户空间的调度器,具体的调度算法由具体的编译器和开发者实现,相比多线程和事件回调的方式,更加灵活可控。不同语言协程的调度方式也不一样,python是在代码里显式地yield进行切换,golang 则是用go语法来开启 goroutine,具体的调度由语言层面提供的运行时执行。

        gorounte 的堆栈比较小,一般是几k,可以动态增长。线程的堆栈空间在 Windows 下默认 2M,Linux 下默认 8M。这也是goroutine 单机支持上万并发的原因,因为它更廉价。

        从堆栈的角度,进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(内核线程)。协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

        在使用 goroutine 的时候,可以把它当作轻量级的线程来用,和多进程、多线程方式一样,主 goroutine 监听,开启多个工作 goroutine 处理连接。比起多线程的方式,优势在于能开更多的 goroutine,来处理连接。

        goroutine 的底层实现,关键在于三个基本对象上,G(goroutine),M(machine),P (process)。M:与内核线程连接,代表内核线程;P:代表M运行G所需要的资源,可以把它看做一个局部的调度器,维护着一个goroutine队列;G:代表一个goroutine,有自己的栈。M 和 G 的映射,可以类比操作系统内核线程与用户线程的 m:n 模型。通过对 P 数量的控制,可以控制操作系统的并发度。

到此,可以解释golang为何能原生支持高并发了。

接下来看看golang的goroutine怎么用。golang是通过关键字go启动goroutine,废话少说,上例子。

package main

import (
	"fmt"
	"time"
)

func cal(a int , b int )  {
	c := a+b
	fmt.Printf("%d + %d = %d\n",a,b,c)
}

func main() {
	for i :=0 ; i<10 ;i++{
		go cal(i,i+1)  //启动10个goroutine 来计算
	}
	time.Sleep(time.Second * 2) // sleep作用是为了等待所有任务完成
}

运行结果:

4 + 5 = 9
7 + 8 = 15
5 + 6 = 11
6 + 7 = 13
1 + 2 = 3
8 + 9 = 17
9 + 10 = 19
2 + 3 = 5
0 + 1 = 1
3 + 4 = 7

        由于goroutine是异步执行的,那很有可能出现主程序退出时还有goroutine没有执行完,此时goroutine也会跟着退出。此时如果想等到所有goroutine任务执行完毕才退出,go提供了sync包和channel来解决同步问题,当然如果你能预测每个goroutine执行的时间,你还可以通过time.Sleep方式等待所有的groutine执行完成以后在退出程序(如上面的列子)。

        通过channel能在多个groutine之间通讯,当一个goroutine完成时候向channel发送退出信号,等所有goroutine退出时候,利用for循环channe去channel中的信号,若取不到数据会阻塞原理,等待所有goroutine执行完毕,使用该方法有个前提是你已经知道了你启动了多少个goroutine,举个例子。

package main

import (
	"fmt"
	"time"
)

func cal(a int , b int ,Exitchan chan bool)  {
	c := a+b
	fmt.Printf("%d + %d = %d\n",a,b,c)
	time.Sleep(time.Second*2)
	Exitchan <- true
}

func main() {

	Exitchan := make(chan bool,10)  //声明并分配管道内存
	for i :=0 ; i<10 ;i++{
		go cal(i,i+1,Exitchan)
	}
	for j :=0; j<10; j++{
		<- Exitchan  //取信号数据,如果取不到则会阻塞
	}
	close(Exitchan) // 关闭管道
}

运行结果:

9 + 10 = 19
3 + 4 = 7
6 + 7 = 13
5 + 6 = 11
4 + 5 = 9
2 + 3 = 5
0 + 1 = 1
1 + 2 = 3
7 + 8 = 15
8 + 9 = 17

goroutine之间可以通过channel进行通信或者说是数据共享,当然你也可以使用全局变量来进行数据共享。

package main

import (
	"fmt"
	"sync"
)

func Productor(mychan chan int,data int,wait *sync.WaitGroup)  {
	mychan <- data
	fmt.Println("product data:",data)
	wait.Done()
}
func Consumer(mychan chan int,wait *sync.WaitGroup)  {
	a := <- mychan
	fmt.Println("consumer data:",a)
	wait.Done()
}
func main() {

	datachan := make(chan int, 100)   //通讯数据管道
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		go Productor(datachan, i,&wg) //生产数据
		wg.Add(1)
	}
	for j := 0; j < 10; j++ {
		go Consumer(datachan,&wg)  //消费数据
		wg.Add(1)
	}
	wg.Wait()
}

运行结果:

product data: 1
consumer data: 1
consumer data: 0
product data: 2
consumer data: 2
product data: 3
product data: 4
product data: 5
consumer data: 3
product data: 6
consumer data: 4
product data: 7
consumer data: 5
product data: 8
consumer data: 6
consumer data: 7
consumer data: 8
product data: 9
consumer data: 9
product data: 0

二.channel

        说了这么多,终于要说channel,估计很多同学没看到这已经放弃了,为什么介绍前边那么多,因为在golang中channel用的最多就是groutine之间的通信。channel俗称管道,用于数据传递或数据共享,其本质是一个先进先出的队列,使用goroutine+channel进行数据通讯简单高效,同时也线程安全,多个goroutine可同时修改一个channel,不需要加锁。接下来讲讲channe的用法,channel分三类;

1.只读channel:只能读channel里面数据,不可写入

    read_only := make (<-chan int)

2.只写channel:只能写数据,不可读

    write_only := make (chan<- int)

定义只读,只写channle没有意义,一般用于参数传递,举个例子:

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    go send(c)
    go recv(c)
    time.Sleep(3 * time.Second)
}
//只能向chan里写数据
func send(c chan<- int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
}
//只能取channel中的数据
func recv(c <-chan int) {
    for i := range c {
        fmt.Println(i)
    }
}

运行结果:

0
1
2
3
4
5
6
7
8
9

main函数中调整send方法和recv方法的调用顺序会导致异常。

3.一般channel:可读可写

    read_write := make (chan int, len)

channel在定义时可以指定长度,当定义时有写红色部分时为有缓存channel,否则为无缓存channel。

1)从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。使用方式如下:

package main
import (
    "fmt"
)
func main() {
    c := make(chan int)
    //使用goroutine使当前channel的发送不会阻塞线程
    go func() {
        c <- 1
    }()
    fmt.Println(<-c)
}

运行结果:

1

2)有缓存的 channel 类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。使用方式如下:

package main
import (
    "fmt"
)
func main() {
    //创建一个缓冲大小为2的channel
    c := make(chan int, 2)
    c <- 1
    c <- 2
    fmt.Println(<-c)
    fmt.Println(<-c)
}

运行结果: 

1
2

3)搭配range使用

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)

	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
		}
		close(ch)  //需要关闭,否则range遍历的时候会异常
	}()
	for num := range ch {
		fmt.Println("num = ", num)
	}
}

 运行结果:

num =  0
num =  1
num =  2
num =  3
num =  4

4)搭配select使用

        select 可以同时监听多个 channel 的消息状态,golang 的 select 的功能和 select, poll, epoll 相似,就是监听 IO 操作,当 IO 操作发生时,触发相应的动作。举个例子:

package main

import (
	"fmt"
	"time"
)

func main() {
	timeout := make (chan bool, 1)
	go func() {
		time.Sleep(1e9 * 5) // sleep five second
		timeout <- true
	}()

	ch := make (chan int)
	//go func() {
	//	ch <- 1
	//}()

	select {
		case <-ch:
			fmt.Println("run ch")
		case <-timeout:
			fmt.Println("timeout!")
	}
}

五秒后输出结果: 

timeout!

ps:

  • select 可以同时监听多个 channel 的写入或读取
  • 执行 select 时,若只有一个 case 通过(不阻塞),则执行这个 case 块
  • 若有多个 case 通过,则随机挑选一个 case 执行
  • 若所有 case 均阻塞,且定义了 default 模块,则执行 default 模块。若未定义 default 模块,则 select 语句阻塞,直到有 case 被唤醒。
  • 使用 break 会跳出 select 块。

5)超时时间

        golang的time模块中的time.after()返回值是chan类型,利用其可以实现timeout功能。举个例子

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make (chan int)
	timeout := time.After(5 * time.Second)

	select {
		case <- ch:
			fmt.Println("task finished.")
		case <- timeout:
			fmt.Println("task timeout.")
	}
}

五秒后输出结果:

task timeout.

6)quit信号

        有一些场景中,一些 worker goroutine 需要一直循环处理信息,直到收到 quit 信号。

package main
import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
			case c <- x:
				x, y = y, x+y
			case <-quit:
				fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)   //管道c中无数据会阻塞
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

运行结果:

0
1
1
2
3
5
8
13
21
34
quit

7)close

        通过close函数来关闭channel。

package main

func main() {
	c := make(chan int, 10)
	c <- 1
	c <- 2
	close(c)
	//c <- 3  //关闭后再插入会异常
}

到此,关于golang的channel的介绍基本结束,以后有别的用法新功能再来更来更新。

查看下一篇函数类型请点我

 

 

你可能感兴趣的:(golang)