精华内容
下载资源
问答
  • channel

    千次阅读 2021-02-04 21:16:01
    1)通道基础 通道(Channel)是java.nio的第二个主要创新。它们既不是一个扩展也不是一项增强,而是全新、极好的Java I/O示例,提供与I/O服务的直接连接。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一...

    在这里插入图片描述1)通道基础  通道(Channel)是java.nio的第二个主要创新。它们既不是一个扩展也不是一项增强,而是全新、极好的Java I/O示例,提供与I/O服务的直接连接。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。  channel的jdk源码:123456package java.nio.channels; public interface Channel; { public boolean isOpen(); public void close() throws IOException; }  与缓冲区不同,通道API主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

    U2FsdGVkX18W5cQC6nuuTN8ykx1hIYIb2eUL2I9JL3M1whXHZkTu4s3O6riSIADi
    65s4MeNkiaZHgvdKTqkPhKt5489xJao1s+x3KpgVVl8jkdw1jNKaTECjhJIoEVF7
    1AA6o9tIN4vTZW3EHdXmCg+3nIoTrYtM3/ksHT4VTf3f6i3r+Wi+ZCowlJAWxjoZ
    H4E7fov+WNcGQ8VTaMaORBkiqIL29Rfat0XQqqBjd/CiUj92Wu6Sr0CUZi8nKQ5d
    P8dSLplBhS/B0wW3U6hW5xNbOpk6lmhZdwADnoj7pQ2tZbufzcs+ZvI1cEj1INrg
    f1qi+WDhHJSS1Pb30icHqO3/6gA4c1RyjdFtyVKERcH1N22jtsueMtm+k3UlZFbG
    jUTp0YRsBwzOTaq8W7As0flpNb4j3vWVDyNlT0EHzqDw4CAg+Wob7cqy/Z2khU8j
    dTzYVm1/i2boVUsK+vNUN5FCRKPAVKa3doVu/QItJ2TK8lTLqSqgO28/tWDtnT+/
    KQ+d6cl7xWqdNXkUMP02URrZrp9BkwwKm1K7mgOpO6GoH5jLxOeyznlnADkajaGs
    GJv0aVWS69CNPA46in9hqT1ZFzLjH/sXOb00NzLk4VJtuHiDCLC29YjrcH0q8ht/
    RYOiRmFKdKoIEPkcT6GR2kPhWDt/TAD47ig7VDPqa0vQh7y+QOMG7ed7gbm3vpcL
    sIKVI0WKB6vwcYsHK0UWl2D89Lcb1RoAUn0n+Qg/U1O6ydFTPKb+02ssvWe9Sp+8
    U3siuAu/s59X0BwbaJBLUsFHvRudUq2BDlbF1m11yTlCWrVDJvUVoH6HrAbB62Zt
    xqupJns7cJP+Rlq4d2tMcNXvoE+OA96spDx6/qifHVMrzm+KdAwZiwlgL6/b8oJj
    rXi6ZnNHbDt28UdBEEOBfj1uBEoOM4C+Ql9/93S6DoK8qKAKE8wOHuAdJob0sPSg
    UcDhSNYrtvo9wmc9lqBLh9/55GQKmDVXlphAo11GNok18ewj0TWkyife+iHIv2sM
    cQ/TqqjCGbssoGHYWKxHvRSeadw8Wej7Y9BMJgFKAmzwZuQf8WlO2DWOyBqh6u5L
    uCAnis+96aOZM1djbH3giW3xSS8U9HEd70rij6jZPE6pHk3EId9uqy65Lt/LAQDc
    srbVjhYJj5T9APdtFo+QC3rqRFiG4HA35O5SGbmswBhJ0H1UXi1ttyolkfZFattg
    jxeMu8Wo1bHf7VDLaa8EMjw/V6dXaMF3SV27BaF1c+sCE4cXAZ+h57TrnSRGUW0f
    7yhtL/GDot0XYmQi93jI944FXJjSmWLUbuUpsepY+kO8wrW/SC/oZeP2PqQj1tFF
    vSeFnULmviaCyZiDmsXDkK1+BFXt5YjUvWaPotIONUI9729T7mQCeMIlIfx01yoK
    2WHscZ8loSoTqk81AhLwU9Jc+E0+GzeC6hebUdJCviOhvtXDtQXAS4mJF9ctEjk3
    8t1zRRFsX3h2578Kvgyo/FUTOajtalZwtXWB0HT0TXeOQXcbbGYPqA81bEmwCSx0
    3FMnu1VrGJ06wxzYkZKjHTBXPMCnxaJ9QBIZOBcqtPpEjN7Cq3VJiSje19NfDwGv
    IWxDAKh/PsoOOIEpI+fx8bHQdqwCaEvpa60MBed/LWq0XAL65LDpKEPP8w0d/dEB
    j/vAsT8oCRAR9/DG9DlD+eOs5QiHR+hZKNV+tLZYjuCBri7NLTZ6DSR6juewjWkE
    4ZO6956bKTRMf4Zu53AoOo6zbCkHScwKCNBjf/TaK1DFY+rl9FJ6uqt7t61LVWcr
    S9yJgnYKe1joCca0DFLxvloG0c7Q+2NGFGGVTljmCRS08eA7S4Zqdi35zF/fi1q3
    pGdrmOnTGbXU1dqSVIdYwmmQ57gZi7ns4Wyhq+vI+y8OjHD6qszuYpfvcVzQ0CxT
    1AWLOwTOVPdnIGVohCLQEGcZ1HTlLvlOIa/PxQHinfTUCzrGFd4TmscwhYlU8dsi
    AG82L/Bf439yTzISaZVDVvqui/LyFeLKDUTzZ2l2CjQqUgdHc5FxuV3zc0Tivfzo
    4COQuiOkAPmvjvM+r8yqXAYxhtwSZSQ3ASnvqSj84jhrJ+c7O36eBabe+LpfT+Hu
    jAVbPJYYKhvtG9ZMhnxbQgzW4RKOxVv+fZIRo8CStxDzM7ccKsK3FfsdaDAriZ6p
    xHsaylEOZwh2fE1ifI2fhSQvcvZ2KbsDuOpgR1qafLsBBKPbA2Xbp5sT32rkxtjm
    ywu9V0RsiP68Urk8oDtC5MAo2TYLdNeZlCPZjFXMw76CeCeLh3qKuZr2P/tXvo1r
    MZS1n9CnJy19wmfvvJBWVHTW7HqP6l305LjwNy+hfAlF9sxPMAVjJYwHpridl6RU
    S31lIKAN2X5wh8pz27SEKi0GxbaEEzoNidutDQOeHO42WFwVcVwnPQco38xzoN6Y
    uPFiXirnOPLQy8r8JmgvGbnLTZ85BAFfrxBWxaCo2D+vWdtUPKLJGMbiEM9TNfyV
    AAv3fxZrRqdM3iy+6BIZsPg5J8q5eFwrxR4fBvc4Vy/vq5OkPgp5F8gAGZAPmyjJ
    crwlUagf3JkpqckkqM0vverDpjr9oFjiDqBoMOrqPSf+eBdz6is12RPfyw0NtfzS
    j9Kvj0tVQUzGzEWekpwh2UHNXnfeOX3ja7aEPC09TG+JIHa+QECAjzuSWxwM4B7u

    展开全文
  • Channel

    千次阅读 2020-06-28 17:07:26
    goroutine之间的通信使用channel。数据传送是阻塞式的,发了数据之后必须有人来收数据。 func chanDemo() { //var c chan int // c == nil c := make(chan int) go func() { //这里的匿名函数相当于闭包,引用了...

    goroutine之间的通信使用channel。数据传送是阻塞式的,发了数据之后必须有人来收数据。

    func chanDemo() {
    	//var c chan int // c == nil
    	c := make(chan int)
    	go func() {  //这里的匿名函数相当于闭包,引用了外面的c变量
    		for {
    			n := <-c //开了一个goroutine去接数据
    			fmt.Println(n)
    		}
    	}()
    	c <- 1 //往channel中发生数据
    	c <- 2
    	time.Sleep(time.Millisecond)
    
    }
    
    func main() {
    	chanDemo()
    }
    
    

    channel作为参数

    func woker(id int, c chan int) {
    	for {
    		fmt.Printf("Worker %d received %c\n", id, <-c) //因为协程是主动式非抢占,在遇到I/O操作时,会进行调度
    	}
    }
    
    func chanDemo() {
    	var channels [10]chan int
    	for i := 0; i < 10; i++ {
    		channels[i] = make(chan int)
    		go woker(i, channels[i])
    	}
    
    	for i := 0; i < 10; i++ {
    		channels[i] <- 'a' + i
    	}
    	for i := 0; i < 10; i++ {
    		channels[i] <- 'A' + i
    	}
    	time.Sleep(time.Millisecond) //防止main函数先退出
    
    }
    
    func main() {
    	chanDemo()
    }
    
    

    channel作为返回值

    func createWorker(id int) chan<- int { //返回一个只能往里写数据的channel
    	c := make(chan int) //创建一个可以写数据和读数据的channnel
    	go func() {
    		for {
    			fmt.Printf("Worker %d received %c\n", id, <-c)
    		}
    	}()
    	return c
    }
    
    func chanDemo() {
    	var channels [10]chan<- int //只能往channel中写数据
    	for i := 0; i < 10; i++ {
    		//channels[i] = make(chan int)
    		//go woker(i, channels[i])
    		channels[i] = createWorker(i)
    	}
    
    	for i := 0; i < 10; i++ {
    		channels[i] <- 'a' + i
    	}
    	for i := 0; i < 10; i++ {
    		channels[i] <- 'A' + i
    	}
    	time.Sleep(time.Millisecond)
    
    }
    

    bufferchannel

    func woker(id int, c chan int) {
    	for {
    		fmt.Printf("Worker %d received %c\n", id, <-c) //因为协程是主动式非抢占,在遇到I/O操作时,会进行调度
    	}
    }
    func bufferedChannel() {
    	c := make(chan int, 3) //创建缓冲区为3的channel
    
    	go woker(0, c)
    	//只要有人发数据,就必须有人来接数据。缓冲区为3说明 发送的数据小于3时,若没人来接数据,不会出现错误
    	c <- 'a'
    	c <- 'b'
    	c <- 'c'
    	c <- 'd'
    	time.Sleep(time.Millisecond)
    }
    

    channel是可以被发送方进行关闭的,接收方使用两种方法进行判断。

    func woker(id int, c chan int) {
    	for {
    	//当发送方关闭channel之后,接收方还会进行接收,值为channel对应类型的默认值。因此在这里进行判断是否channel关闭
    		if n, ok := <-c; ok {
    			fmt.Printf("Worker %d received %c\n", id, n) //因为协程是主动式非抢占,在遇到I/O操作时,会进行调度
    		} else {
    			break
    		}
    	}
    	//第二种方法
    	//for n := range c {
    	//	fmt.Printf("Worker %d received %c\n", id, n)
    	//}
    }
    func channelClosed() {
    	c := make(chan int, 3) //创建缓冲区为3的channel
    
    	go woker(0, c)
    	c <- 'a'
    	c <- 'b'
    	c <- 'c'
    	c <- 'd'
    	close(c) 
    	time.Sleep(time.Millisecond)
    }
    

    不要通过共享内存来通信,通过通信来共享内存。
    使用channel等待任务结束。两种方法:使用channel进行传递信息;使用sync.WaitGroup类型的变量。

    	data := make(chan int)
    	exit := make(chan bool)
    	go func() {
    		for d := range data {
    			fmt.Println(d)
    		}
    		fmt.Println("recv over.")
    		exit <- true
    	}()
    
    	data <- 1
    	data <- 2
    	data <- 3
    	close(data)
    	fmt.Println("send over.")
    	<-exit
    
    func doWoker(id int, c chan int, done chan bool) {
    	for n := range c {
    		fmt.Printf("Worker %d received %c\n", id, n)
    		//done <- true //往done中发送了数据,在外面必须有人来接收数据
    		go func() {
    			done <- true
    		}()
    	}
    }
    
    type worker struct {
    	in   chan int
    	done chan bool
    }
    
    func createWorker(id int) worker {
    	w := worker{
    		in:   make(chan int),
    		done: make(chan bool),
    	}
    	go doWoker(id, w.in, w.done)
    	return w
    }
    
    func chanDemo() {
    	var workers [10]worker
    	for i := 0; i < 10; i++ {
    		workers[i] = createWorker(i)
    	}
    
    	for i, worker := range workers {
    		worker.in <- 'a' + i
    		//<-workers[i].done //只有收到了数据说明打印已经完成了,才会往下执行
    	}
    	for i, worker := range workers {
    		worker.in <- 'A' + i
    		//<-workers[i].done
    	}
    
    	//将20个数据全部发出去,然后再进行等待
    	for _, worker := range workers {
    		<-worker.done
    		<-worker.done
    	}
    
    }
    
    
    func doWoker(id int, w worker) {
    	for n := range w.in {
    		fmt.Printf("Worker %d received %c\n", id, n)
    		w.done()
    	}
    }
    
    type worker struct {
    	in   chan int
    	done func()
    }
    
    func createWorker(id int, wg *sync.WaitGroup) worker {
    	w := worker{
    		in: make(chan int),
    		done: func() {
    			wg.Done() //完成一次任务调用一次Done()
    		},
    	}
    	go doWoker(id, w)
    	return w
    }
    
    func chanDemo() {
    	var workers [10]worker
    	var wq sync.WaitGroup
    
    	for i := 0; i < 10; i++ {
    		workers[i] = createWorker(i, &wq)
    	}
    
    	wq.Add(20) //添加20个任务
    	for i, worker := range workers {
    		worker.in <- 'a' + i
    
    	}
    	for i, worker := range workers {
    		worker.in <- 'A' + i
    	}
    	wq.Wait()
    }
    
    

    使用select进行调度。

    
    func generator() chan int {
    	out := make(chan int)
    	go func() {
    		i := 0
    		for {
    			time.Sleep(
    				time.Duration(rand.Intn(1500)) *
    					time.Millisecond)
    			out <- i
    			i++
    		}
    	}()
    	return out
    }
    func woker(id int, c chan int) {
    	for n := range c {
    		time.Sleep(time.Second)
    		fmt.Printf("Worker %d received %d\n", id, n)
    	}
    }
    func createWorker(id int) chan<- int {
    	c := make(chan int)
    	go woker(id, c)
    	return c
    }
    
    func main() {
    	//从c1, c2中读出数据 写入worker中
    	var c1, c2 = generator(), generator()
    	worker := createWorker(0)
    
    	var values []int //生成和消耗的速度不一样,需要存储接收到的数据
    
    	tm := time.After(10 * time.Second) //返回的是一个channel,10s后往这个channel中发生一个时间
    	tick := time.Tick(time.Second)
    	for {
    		var activeWorker chan<- int // activeWorker 是 nil,在select虽然不会运行错误,但是永远不是被select到
    		var activeValue int
    		if len(values) > 0 {
    			activeWorker = worker
    			activeValue = values[0]
    		}
    
    		select {
    		case n := <-c1:
    			values = append(values, n)
    		case n := <-c2:
    			values = append(values, n)
    		case activeWorker <- activeValue:
    			values = values[1:]
    		case <-time.After(800 * time.Millisecond): //如果两次生成数据的时间大于800ms,则会timeout
    			fmt.Println("Time out")
    		case <-tick: //每1s查看一下数据的长度
    			fmt.Println("queue lens = ", len(values))
    		case <-tm:
    			fmt.Println("Bye")
    			return
    		}
    	}
    }
    

    传统的同步机制:WaitGroup,mutex,conditional variable

    type atomicInt struct {
    	value int
    	m     sync.Mutex
    }
    
    func (a *atomicInt) increasement() {
    	fmt.Println("safe increasement")
    	func() {
    		a.m.Lock()
    		defer a.m.Unlock()
    		a.value++
    	}()
    
    }
    
    func (a *atomicInt) get() int {
    	a.m.Lock()
    	defer a.m.Unlock()
    	return a.value
    }
    

    select语句的用法


    通过select可以监听channel上的数据流动。
    每一个case语句里必须是一个I/O操作。

    elect {
    
          case <-chan1:
    
            // 如果chan1成功读到数据,则进行该case处理语句
    
          case chan2 <- 1:
    
            // 如果成功向chan2写入数据,则进行该case处理语句
    
          default:
    
            // 如果上面都没有成功,则进入default处理流程
    
        }
    

    在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

    如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

    如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:

    • 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。

    • 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。

    go的CSP模型


    go语言支持两种形式的并发:多线程共享内存;CSP并发模型。

    多线程共享内存模型:在访问数据的时候,通过锁来访问。

    CSP并发模型:通过channel和goroutine实现。goroutine是Go中并发的执行单位,channel是各个并发单位之前的通信机制,通俗来说就是各个goroutine之间的管道

    传数据用channel <- data,取数据用<-channel,在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。

    参考一

    参考二

    展开全文
  • 文章目录Channel shutdown: channel error; protocol method: #methodChannel shutdown: channel error; protocol method: #method<channel.close>(rep 1、启动springboot 应用报错 Channel shutdown: ...

    Channel shutdown: channel error; protocol method: #method<channel.close>(rep


    1、启动springboot 应用报错

    Channel shutdown: channel error; protocol method: #method<channel.close>(rep

    2、原因

    当应用启动时,spring 会去检查注册的队列,跟服务器上的队列配置是否一致,如果不一致,则抛出这个错误

    比如你在项目中的配置是

        @Bean(DEAD_LETTER_PROD_CLOSE_ORDER)
        Queue a() {
            Map<String, Object> args = Maps.newHashMap();
            args.put("x-dead-letter-exchange", RabbitMqExchange.ExchangeCenter.DEAD_LETTER_EXCHANGE_CONSUME);
            args.put("x-dead-letter-routing-key", DEAD_LETTER_CONSUME_CLOSE_ORDER);
            args.put("x-message-ttl", 600 * 1000);
    
            return new Queue(DEAD_LETTER_PROD_CLOSE_ORDER, true, false, false, args);
        }
    

    但是服务器上的配置是

    x-message-ttl=1000
    

    则代码配置与现有队列配置不一致,抛出该错误

    3、解决

    方式一、修改项目配置与mq 保持一致

    修改项目配置与MQ一致则可以正常运行


    方式二、删除mq上现有队列
    删除mq 上现有队列,则springboot 自动向mq 服务器注册一个新的队列,也可以解决该问题

    展开全文
  • channel详解

    万次阅读 2021-02-13 11:16:42
    channel详解 文章目录channel详解前言一、什么是 channel?二、channel 基本特性1.无缓冲 channel2.缓冲 channelchannel 本质基本原理数据结构channel 实现原理创建 chan发送数据前置处理上互斥锁直接发送缓冲...

    channel详解



    前言

    Go 语言中的一大利器那就是能够非常方便的使用 go 关键字来进行各种并发,而并发后又必然会涉及通信。
    Channel 自然而然就成为了 Go 语言开发者中必须要明白明了的一个 “东西” 了,更别提实际工程应用和日常面试了,属于必知必会。


    提示:以下是本篇文章正文内容,下面案例可供参考

    一、什么是 channel?

    在 Go 语言中,channel 可以称其为通道,也可以叫管道。channel 主要常见于与 goroutine+select 搭配使用,再结合语录的描述。可以知道 channel 就是用于 goroutine 的数据通信:
    演示代码如下:

    func main() {
     ch := make(chan string)
     go func() {
      ch <- "eating"
     }()
    
     msg := <-ch
     fmt.Println(msg)
    }
    

    在 goroutine1 中写入 “煎鱼” 到变量 ch 中,goroutine2 监听变量 ch,并阻塞等待读取到值 “煎鱼” 最终返回,结束流程。
    在此 channel 承载着一个衔接器的桥梁:
    在这里插入图片描述
    这也是 channel 的经典思想了,不要通过共享内存来通信,而是通过通信来实现内存共享(Do not communicate by sharing memory; instead, share memory by communicating)
    从模式上来看,其就是在多个 goroutine 借助 channel 来传输数据,实现了跨 goroutine 间的数据传输,多者独立运行,不需要强关联,更不影响对方的 goroutine 状态。不存在 goroutine1 对 goroutine2 进行直传的情况。
    这里思考一个问题,那 goroutine1 和 goroutine2 又怎么互相知道自己的数据 ”到“ 了呢?

    二、channel 基本特性

    在 Go 语言中,channel 的关键字为 chan,数据流向的表现方式为 <-,代码解释方向是从左到右,据此就能明白通道的数据流转方向了。
    channel 共有两种模式,分别是:双向和单向;三种表现方式,分别是:声明双向通道:chan T、声明只允许发送的通道:chan <- T、声明只允许接收的通道:<- chan T。
    channel 中还分为 “无缓冲 channel” 和 “缓冲 channel”。
    演示代码如下:

    // 无缓冲
    ch1 := make(chan int)
    
    // 缓冲区为 3
    ch2 := make(chan int, 3)
    

    无缓冲 channel

    无缓冲的 channel(unbuffered channel),其缓冲区大小则默认为 0。在功能上其接受者会阻塞等待并阻塞应用程序,直至收到通信和接收到数据。
    这种常用于两个 goroutine 间互相同步等待的应用场景:

    在这里插入图片描述

    缓冲 channel

    代有缓存的 channel(buffered channel),其缓存区大小是根据所设置的值来调整。在功能上,若缓冲区未满则不会阻塞,会源源不断的进行传输。当缓冲区满了后,发送者就会阻塞并等待。而当缓冲区为空时,接受者就会阻塞并等待,直至有新的数据:码如下(示例):

    在这里插入图片描述
    在实际的应用场景中,两者根据业务情况选用就可以了,不需要太过纠结于两者是否有性能差距,没意义。

    三、channel 本质

    channel 听起来实现了一个非常酷的东西,也是日常工作中常常会被面试官问到的问题。
    但其实 channel 并没有那么的 “神秘”,就是一个环形队列的配合。
    接下来我们一步步的剖开 channel,看看里面到底是什么,怎么实现的跨 goroutine 通信,数据结构又是什么,两者又如何实现数据传输的?

    基本原理

    本质上 channel 在设计上就是环形队列。其包含发送方队列、接收方队列,加上互斥锁 mutex 等结构。
    channel 是一个有锁的环形队列:
    在这里插入图片描述

    数据结构

    hchan 结构体是 channel 在运行时的具体表现形式:

    // src/runtime/chan.go
    type hchan struct {
     qcount   uint      
     dataqsiz uint     
     buf      unsafe.Pointer 
     elemsize uint16
     closed   uint32
     elemtype *_type 
     sendx    uint  
     recvx    uint  
     recvq    waitq  
     sendq    waitq  
    
     lock mutex
    }
    
    • qcount:队列中的元素总数量。
    • dataqsiz:循环队列的长度。
    • buf:指向长度为 dataqsiz 的底层数组,仅有当 channel 为缓冲型的才有意义。
    • elemsize:能够接受和发送的元素大小。
    • closed:是否关闭。
    • elemtype:能够接受和发送的元素类型。
    • sendx:已发送元素在循环队列中的索引位置。
    • recvx:已接收元素在循环队列中的索引位置。
    • recvq:接受者的 sudog 等待队列(缓冲区不足时阻塞等待的 goroutine)。
    • sendq:发送者的 sudog 等待队列。

    在数据结构中,我们可以看到 recvq 和 sendq,其表现为等待队列,其类型为 runtime.waitq 的双向链表结构:

    type waitq struct {
     first *sudog
     last  *sudog
    }
    

    且无论是 first 属性又或是 last,其类型都为 runtime.sudog 结构体:

    type sudog struct {
     g *g
    
     next *sudog
     prev *sudog
     elem unsafe.Pointer
     ...
    }
    

    g:指向当前的 goroutine。
    next:指向下一个 g。
    prev:指向上一个 g。
    elem:数据元素,可能会指向堆栈。
    sudog 是 Go 语言中用于存放协程状态为阻塞的 goroutine 的双向链表抽象,你可以直接理解为一个正在等待的 goroutine 就可以了。
    在后续的实现原理分析中,基本围绕着上述数据结构进行大量的讨论,建议可以认真思考一下。

    四、channel 实现原理

    在了解了 channel 的基本原理后,我们进入到与应用工程中更紧密相关的部分,那就是 channel 的四大块操作,分别是:“创建、发送、接收、关闭”。
    我们将针对这四块进行细致的分析和讲解。因此接下来的内容比较庞大,内容上将分为两个角度来讲述,分别是先从源码角度进行分析,再进行图示汇总。以便于大家更好的理解和思考

    创建 chan

    创建 channel 的演示代码:

    ch := make(chan string)
    

    其在编译器翻译后对应 runtime.makechan 或 runtime.makechan64 方法:

    // 通用创建方法
    func makechan(t *chantype, size int) *hchan
    
    
    // 类型为 int64 的进行特殊处理
    func makechan64(t *chantype, size int64) *hchan
    

    通过前面我们得知 channel 的基本单位是 hchan 结构体,那么在创建 channel 时,究竟还需要做什么是呢?
    我们一起分析一下 makechan 方法,就能知道了。
    源码如下:

    // src/runtime/chan.go
    func makechan(t *chantype, size int) *hchan {
     elem := t.elem
     mem, _ := math.MulUintptr(elem.size, uintptr(size))
    
     var c *hchan
     switch {
     case mem == 0:
      c = (*hchan)(mallocgc(hchanSize, nil, true))
      c.buf = c.raceaddr()
     case elem.ptrdata == 0:
      c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
      c.buf = add(unsafe.Pointer(c), hchanSize)
     default:
      c = new(hchan)
      c.buf = mallocgc(mem, elem, true)
     }
    
     c.elemsize = uint16(elem.size)
     c.elemtype = elem
     c.dataqsiz = uint(size)
     lockInit(&c.lock, lockRankHchan)
    
     return c
    }
    

    创建 channel 的逻辑主要分为三大块:
    当前 channel 不存在缓冲区,也就是元素大小为 0 的情况下,就会调用 mallocgc 方法分配一段连续的内存空间。
    当前 channel 存储的类型存在指针引用,就会连同 hchan 和底层数组同时分配一段连续的内存空间。
    通用情况,默认分配相匹配的连续内存空间。
    需要注意到一块特殊点,那就是 channel 的创建都是调用的 mallocgc 方法,也就是 channel 都是创建在堆上的。因此 channel 是会被 GC 回收的,自然也不总是需要 close 方法来进行显示关闭了。
    从整体上来讲,makechan 方法的逻辑比较简单,就是创建 hchan 并分配合适的 buf 大小的堆上内存空间。

    发送数据

    channel 发送数据的演示代码:

    go func() {
        ch <- "eating"
    }()
    

    其在编译器翻译后对应 runtime.chansend1 方法:

    func chansend1(c *hchan, elem unsafe.Pointer) {
     chansend(c, elem, true, getcallerpc())
    }
    

    其作为编译后的入口方法,实则指向真正的实现逻辑,也就是 chansend 方法。

    前置处理

    在第一部分中,我们先看看 chan 发送的一些前置判断和处理:

    func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
     if c == nil {
      if !block {
       return false
      }
      gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
      throw("unreachable")
     }
     
     if !block && c.closed == 0 && full(c) {
      return false
     }
    
     // 省略一些调试相关
     ...
    }
    
    func full(c *hchan) bool {
     if c.dataqsiz == 0 {
      return c.recvq.first == nil
     }
    
     return c.qcount == c.dataqsiz
    }
    

    一开始 chansend 方法在会先判断当前的 channel 是否为 nil。若为 nil,在逻辑上来讲就是向 nil channel 发送数据,就会调用 gopark 方法使得当前 Goroutine 休眠,进而出现死锁崩溃,表象就是出现 panic 事件来快速失败。
    紧接着会对非阻塞的 channel 进行一个上限判断,看看是否快速失败。
    失败的场景如下:

    • 若非阻塞且未关闭,同时底层数据 dataqsiz 大小为 0(缓冲区无元素),则会返回失败。
    • 若是 qcount 与 dataqsiz 大小相同(缓冲区已满)时,则会返回失败。

    上互斥锁

    在完成了 channel 的前置判断后,即将在进入发送数据的处理前,channel 会进行上锁:

    func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
     ...
     lock(&c.lock)
    }
    

    上锁后就能保住并发安全。另外我们也可以考虑到,这种场景会相对依赖单元测试的覆盖,因为一旦没考虑周全,漏上锁了,基本就会出问题。

    直接发送

    在正式开始发送前,加锁之后,会对 channel 进行一次状态判断(是否关闭):

    func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
     ...
     if c.closed != 0 {
      unlock(&c.lock)
      panic(plainError("send on closed channel"))
     }
    
     if sg := c.recvq.dequeue(); sg != nil {
      send(c, sg, ep, func() { unlock(&c.lock) }, 3)
      return true
     }
    }
    

    这种情况是最为基础的,也就是当前 channel 有正在阻塞等待的接收方,那么只需要直接发送就可以了。

    缓冲发送

    非直接发送,那么就考虑第二种场景,判断 channel 缓冲区中是否还有空间:

    func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
     ...
     if c.qcount < c.dataqsiz {
      qp := chanbuf(c, c.sendx)
      typedmemmove(c.elemtype, qp, ep)
      c.sendx++
      if c.sendx == c.dataqsiz {
       c.sendx = 0
      }
      c.qcount++
      unlock(&c.lock)
      return true
     }
    
     if !block {
      unlock(&c.lock)
      return false
     }
    }
    

    会对缓冲区进行判定(qcount 和 dataqsiz 字段),以此识别缓冲区的剩余空间。紧接进行如下操作:

    • 调用 chanbuf 方法,以此获得底层缓冲数据中位于 sendx 索引的元素指针值。
    • 调用 typedmemmove 方法,将所需发送的数据拷贝到缓冲区中。
    • 数据拷贝后,对 sendx 索引自行自增 1。同时若 sendx 与 dataqsiz 大小一致,则归 0(环形队列)。
    • 自增完成后,队列总数同时自增 1。解锁互斥锁,返回结果。

    至此针对缓冲区的数据操作完成。但若没有走进缓冲区处理的逻辑,则会判断当前是否阻塞 channel,若为非阻塞,将会解锁并直接返回失败。
    配合图示如下:
    在这里插入图片描述

    在当前 goroutine 被挂起后,其将会在 channel 能够发送数据后被唤醒:

    func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
     ...
     // 从这里开始唤醒,并恢复阻塞的发送操作
     if mysg != gp.waiting {
      throw("G waiting list is corrupted")
     }
     gp.waiting = nil
     gp.activeStackChans = false
     if gp.param == nil {
      	if c.closed == 0 {
       		throw("chansend: spurious wakeup")
      }
      	panic(plainError("send on closed channel"))
     }
     gp.param = nil
     if mysg.releasetime > 0 {
      	blockevent(mysg.releasetime-t0, 2)
     }
     mysg.c = nil
     releaseSudog(mysg)
     return true
    }
    

    唤醒 goroutine(调度器在停止 g 时会记录运行线程和方法内执行的位置)并完成 channel 的阻塞数据发送动作后。进行基本的参数检查,确保是符合要求的(纵深防御),接着开始取消 mysg 上的 channel 绑定和 sudog 的释放。
    至此完成所有类别的 channel 数据发送管理。

    五、接受消息

    channel 接受数据的演示代码:

    msg := <-ch
    
    msg, ok := <-ch
    

    两种方法在编译器翻译后分别对应 runtime.chanrecv1 和 runtime.chanrecv2 两个入口方法,其再在内部再进一步调用 runtime.chanrecv 方法:
    需要注意,发送和接受 channel 是相对的,也就是其核心实现也是相对的。因此在理解时也可以结合来看。

    前置处理

    func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
     if c == nil {
      if !block {
       return
      }
      gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
      throw("unreachable")
     }
    

    一开始时 chanrecv 方法会判断其是否为 nil channel。
    场景如下:
    若 channel 是 nil channel,且为阻塞接收则调用 gopark 方法挂起当前 goroutine。
    若 channel 是非阻塞模式,则直接返回。
    而接下来对于非阻塞模式的 channel 会进行快速失败检查,检测 channel 是否已经准备好接收。

    if !block && empty(c) {
      if atomic.Load(&c.closed) == 0 {
       return
      }
    
      if empty(c) {
       if ep != nil {
        typedmemclr(c.elemtype, ep)
       }
       return true, false
      }
     }
     ...
    }
    

    其分以下几种情况:

    • 无缓冲区:循环队列为 0 及等待队列 sendq 内没有 goroutine 正在等待。
    • 有缓冲区:缓冲区数组为空。

    随后会对 channel 的 closed 状态进行判断,因为 channel 是无法重复打开的,需要确定当前 channel 是否为未关闭状态。再确定接收失败,返回。
    但若是 channel 已经关闭且不存在缓存数据了,则会清理 ep 指针中的数据并返回。

    直接接收

    当发现 channel 上有正在阻塞等待的发送方时,则直接进行接收:

    func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
     lock(&c.lock)
    
     if sg := c.sendq.dequeue(); sg != nil {
      recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
      return true, true
     }
     ...
    }
    

    缓冲接收

    当发现 channel 的缓冲区中有元素时:

    func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
     if c.qcount > 0 {
      qp := chanbuf(c, c.recvx)
      if ep != nil {
       typedmemmove(c.elemtype, ep, qp)
      }
      typedmemclr(c.elemtype, qp)
      c.recvx++
      if c.recvx == c.dataqsiz {
       c.recvx = 0
      }
      c.qcount--
      unlock(&c.lock)
      return true, true
     }
    
     if !block {
      unlock(&c.lock)
      return false, false
     }
     ...
    }
    

    将会调用 chanbuf 方法根据 recvx 的索引位置取出数据,找到要接收的元素进行处理。若所接收到的数据和所传入的变量均不为空,则会调用 typedmemmove 方法将缓冲区中的数据拷贝到所传入的变量中。
    最后数据拷贝完毕后,进行各索引项和队列总数的自增增减,并调用 typedmemclr 方法进行内存数据的清扫。

    阻塞接收

    当发现 channel 上既没有待发送的 goroutine,缓冲区也没有数据时。将会进入到最后一个阶段阻塞接收:

    func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
     gp := getg()
     mysg := acquireSudog()
     mysg.releasetime = 0
     if t0 != 0 {
      mysg.releasetime = -1
     }
    
     mysg.elem = ep
     mysg.waitlink = nil
     gp.waiting = mysg
     mysg.g = gp
     mysg.isSelect = false
     mysg.c = c
     gp.param = nil
     c.recvq.enqueue(mysg)
    
     atomic.Store8(&gp.parkingOnChan, 1)
     gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
     ...
    }
    

    这一块接收逻辑与发送也基本类似,主体就是获取当前 goroutine,构建 sudog 结构保存当前待接收数据(发送方)的地址信息,并将 sudog 加入等待接收队列。最后调用 gopark 方法挂起当前 goroutine,等待唤醒。

    func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
     // 被唤醒后从此处开始
     if mysg != gp.waiting {
      throw("G waiting list is corrupted")
     }
     gp.waiting = nil
     gp.activeStackChans = false
     if mysg.releasetime > 0 {
      blockevent(mysg.releasetime-t0, 2)
     }
     closed := gp.param == nil
     gp.param = nil
     mysg.c = nil
     releaseSudog(mysg)
     return true, !closed
    }
    

    被唤醒后,将恢复现场,回到对应的执行点,完成最后的扫尾工作。

    六、关闭 chan

    关闭 channel 主要是涉及到 close 关键字:

    close(ch)
    

    其对应的编译器翻译方法为 closechan 方法:

    func closechan(c *hchan)
    

    前置处理

    func closechan(c *hchan) {
     if c == nil {
      panic(plainError("close of nil channel"))
     }
    
     lock(&c.lock)
     if c.closed != 0 {
      unlock(&c.lock)
      panic(plainError("close of closed channel"))
     }
    
     c.closed = 1
     ...
    }
    

    基本检查和关闭标志设置,保证 channel 不为 nil 和未关闭,保证边界。

    释放接收方

    在完成了异常边界判断和标志设置后,会将接受者的 sudog 等待队列(recvq)加入到待清除队列 glist 中:

    func closechan(c *hchan) {
    
     var glist gList
     for {
      	sg := c.recvq.dequeue()
      	if sg == nil {
       		break
      	}
      	if sg.elem != nil {
       		typedmemclr(c.elemtype, sg.elem)
       		sg.elem = nil
      	}
      	if sg.releasetime != 0 {
       		sg.releasetime = cputicks()
      	}
      	gp := sg.g
      	gp.param = nil
      	if raceenabled {
       		raceacquireg(gp, c.raceaddr())
      	}
      	glist.push(gp)
     }
     ...
    }
    

    所取出并加入的 goroutine 状态需要均为 _Gwaiting,以保证后续的新一轮调度。

    释放发送方

    同样,与释放接收方一样。会将发送方也加入到到待清除队列 glist 中:

    func closechan(c *hchan) {
    
     // release all writers (they will panic)
     for {
      sg := c.sendq.dequeue()
      if sg == nil {
       break
      }
      sg.elem = nil
      if sg.releasetime != 0 {
       sg.releasetime = cputicks()
      }
      gp := sg.g
      gp.param = nil
      if raceenabled {
       raceacquireg(gp, c.raceaddr())
      }
      glist.push(gp)
     }
     unlock(&c.lock)
     ...
    }
    

    协程调度

    将所有 glist 中的 goroutine 状态从 _Gwaiting 设置为 _Grunnable 状态,等待调度器的调度:

    func closechan(c *hchan) {
    
     // Ready all Gs now that we've dropped the channel lock.
     for !glist.empty() {
      gp := glist.pop()
      gp.schedlink = 0
      goready(gp, 3)
     }
    }
    

    后续所有的 goroutine 允许被重新调度后。若原本还在被动阻塞的发送方或接收方,将重获自由,后续该干嘛就去干嘛了,再跑回其所属的应用流程。

    七、channel send/recv 分析

    send

    send 方法承担向 channel 发送具体数据的功能:

    func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
     if sg.elem != nil {
      sendDirect(c.elemtype, sg, ep)
      sg.elem = nil
     }
     gp := sg.g
     unlockf()
     gp.param = unsafe.Pointer(sg)
     if sg.releasetime != 0 {
      sg.releasetime = cputicks()
     }
     goready(gp, skip+1)
    }
    
    func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
     dst := sg.elem
     typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
     memmove(dst, src, t.size)
    }
    
    • 调用 sendDirect 方法将待发送的数据直接拷贝到待接收变量的内存地址(执行栈)。例如:msg := <-ch 语句,也就是将数据从 ch 直接拷贝到了 msg 的内存地址。
    • 调用 sg.g 属性, 从 sudog 中获取等待接收数据的 goroutine,并传递后续唤醒所需的参数。
    • 调用 goready 方法唤醒需接收数据的 goroutine,期望从 _Gwaiting 状态调度为 _Grunnable。

    recv

    recv 方法承担在 channel 中接收具体数据的功能:

    func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
     if c.dataqsiz == 0 {
      if ep != nil {
       recvDirect(c.elemtype, sg, ep)
      }
     } else {
      qp := chanbuf(c, c.recvx)
      if ep != nil {
       typedmemmove(c.elemtype, ep, qp)
      }
      typedmemmove(c.elemtype, qp, sg.elem)
      c.recvx++
      if c.recvx == c.dataqsiz {
       c.recvx = 0
      }
      c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
     }
     sg.elem = nil
     gp := sg.g
     unlockf()
     gp.param = unsafe.Pointer(sg)
     if sg.releasetime != 0 {
      sg.releasetime = cputicks()
     }
     goready(gp, skip+1)
    }
    

    该方法在接受上分为两种情况,分别是直接接收和缓冲接收:

    1. 直接接收(不存在缓冲区):

    调用 recvDirect 方法,其作用与 sendDirect 方法相对,会直接从发送方的 goroutine调用栈中将数据拷贝过来到接收方的 goroutine。

    1. 缓冲接收(存在缓冲区):

    调用 chanbuf 方法,根据 recvx 索引的位置读取缓冲区元素,并将其拷贝到接收方的内存地址。
    拷贝完毕后,对 sendx 和 recvx 索引位置进行调整。

    最后还是常规的 goroutine 调度动作,会调用 goready 方法来唤醒当前所处理的 sudog 的对应 goroutine。那么在下一轮调度时,既然已经接收了数据,自然发送方也就会被唤醒。

    总结

    在本文中我们针对 Go 语言的 channel 进行了基本概念的分析和讲解,同时还针对 channel 的设计原理和四大操作(创建、发送、接收、关闭)进行了源码分析和图示分析。
    初步看过一遍后,再翻看。不难发现,Go 的 channel 设计并不复杂,记住他的数据结构就是带缓存的环形队列,再加上对称的 sendq、recvq 等双向链表的辅助属性,就能勾画出 channel 的基本逻辑流转模型。
    在具体的数据传输上,都是围绕着 “边界上下限处理,上互斥锁,阻塞/非阻塞,缓冲/非缓冲,缓存出队列,拷贝数据,解互斥锁,协程调度” 在不断地流转处理。在基本逻辑上也是相对重合的,因为发送和接收,创建和关闭总是相对的。
    如果更进一步深入探讨,还可以围绕着 CSP 模型、goroutine 调度等进一步的思考和理解。这一块会在后续的章节中再一步展开。

    展开全文
  • 在写这篇文章是,当前有几个channel可供选择,分别是Memory Channel, JDBC Channel , File Channel,Psuedo Transaction Channel。比较常见的是前三种channel。具体使用那种channel,需要根据具体的使用场景。这里我...
  • Channel 用法

    万次阅读 2020-07-08 11:45:15
    channeljava的NIOchannel实现举个栗子 java的NIO 我们知道java的流是单向的,可读可写类似于channel里的通道, 1、区别在于流是半双工,通道是全双工 2、通道读写要buffer channel实现 FileChannel 从文件中读写数据...
  • Golang channel

    万次阅读 2020-08-04 11:17:42
    channel 是 Go 语言中的一个核心类型,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。 channel 是一个数据类型,主要用来解决 go 程的同步问题...
  • Golang 深入源码 —— channel

    万次阅读 2020-07-21 15:58:12
    这句话是 Go 语言设计团队的首任负责人 Rob Pike 对并发编程的建议,也是 Go 的并发哲学,通道 Channel 便是基于这种哲学 我们可以把 Channel 看做是一个先进先出(FIFO)的数据队列 数据结构 type hchan struct { ...
  • channel介绍

    千次阅读 2020-08-12 11:22:10
    1、channel的创建方法 2、channel接收和发送数据的方式
  • 在写这篇文章是,当前有几个channel可供选择,分别是Memory Channel, JDBC Channel , File Channel,Psuedo Transaction Channel。比较常见的是前三种channel。具体使用那种channel,需要根据具体的使用场景。这里我...
  • Channel通道

    千次阅读 2019-05-09 08:16:01
    Channel 通道 Channel原理类似于传统的流对象, FileInputStream FileOutputStream 但是有两个主要的区别 1.Channel能够将指定的部分或者全部文件映射到内存中 全部映射 MappedByteBuffer 部分文件映射 2.程序如果想...
  • muduo之channel

    万次阅读 2019-10-10 23:18:20
    channel是muduo中的事件分发器,它只属于一个EventLoop,Channel类中保存着IO事件的类型以及对应的回调函数,每个channel只负责一个文件描述符,但它并不拥有这个文件描述符。channel是在epoll和TcpConnection之间起...
  • Java NIO Channel to Channel Transfers

    千次阅读 2014-11-20 08:50:22
    Java NIO Channel to Channel Transfers
  • 无缓冲channel

    万次阅读 2020-09-16 22:02:39
    ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步. ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步. channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。 ...
  • 单向channel

    千次阅读 2018-06-14 12:33:13
    单向channel 近日在面试中有提到过单向channel。问我是否了解。之前在golang的官方库中确实有看到相应的单向channel的例子,如context包,以及在使用第三方包imap的时候也有使用单向channel,那个是作为通知使用...
  • Channel shutdown: channel error; protocol method: springboot + rabbitMq 场景:使用RabbitTemplate操作mq,使用@RabbitListener申明消费者,并且在方法中手动ACK,发送消息的过程中报如下错误 Channel shutdown:...
  • Channel 详解

    千次阅读 2019-08-01 00:02:19
    java.nio.channels.FileChannel封装了一个文件通道和一个... a、所有通道接口都是扩展自java.nio.channels.Channel,该通道接口的声明有两个方法: 关闭通道close()方法; 测试通道状态isOpen(),打开true,关闭...
  • 整合RabbitMQ和SpringBoot报错如下: 原因:RabbitMQ中已经存在...2019-10-30 12:46:15.230 [AMQP Connection 127.0.0.1:5672] ERROR o.s.a.r.c.CachingConnectionFactory - Channel shutdown: channel error; protoc...
  • Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'type' for exchange 'kxg-fanout' in vhost '/': r...
  • 客户端主动关闭连接,服务端关闭channel的过程 在服务端中,在AbstractChannel#AbstractUnsafe的close(final ChannelPromise promise)方法上设置断点:这个是比较顶层的方法。 往下看底层开始时的调用栈:在...
  • 在Go中我们make一个channel有两种方式,分别是有缓冲的和没缓冲的 缓冲channel 即 buffer channel 创建方式为 make(chan TYPE,SIZE) 如 make(chan int,3) 就是创建一个int类型,缓冲大小为3的 channel 非缓冲...
  • pomelo Channel

    2015-03-26 13:50:22
    ApiChannelServiceChannelService is created by channel component which is a default loaded component of pomelo and channel service would be accessed by app.get(‘channelService’). createChannel(name) ...
  • Channel的几种状态

    万次阅读 2020-09-16 22:22:51
    Channel是异步进行的。 channel存在3种状态: nil,未初始化的状态,只进行了声明,或者手动赋值为nil active,正常的channel,可读或者可写 closed,已关闭,千万不要误认为关闭channel后,channel的值是...
  • NIO之旅Channel

    千次阅读 2020-08-14 17:56:26
    Java NIO中有三个重要的组件Channel、Buffer、Selector,本篇文章主要讲解NIO中的Channel组件。 Channel的基本概念 Channel是Java NIO核心概念之一,翻译过来就是“通道”的意思,在使用上,通道需和缓存类...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 121,137
精华内容 48,454
热门标签
关键字:

channel