zoukankan      html  css  js  c++  java
  • Golang并发编程之channel

    1、channel

    单纯将函数实现并发是没有任何意义的,函数与函数之间需要交换数据才能够体现并发执行函数的意义。
    虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态情况。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法肯定会造成性能影响。

    Go语言的并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信。

    如果说goroutine是Go程序并发的执行体,channel就是他们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

    Go语言中的通道(channel)是一种特殊的类型,通道像一个传送带或者队列,总是遵循先入先出(FIFO)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其制定元素类型。

    1.1、channel类型

    声明通道类型的格式如下:

    var 变量 chan 元素类型
    

    举几个例子

    var a1 chan int   // 声明一个int类型的chan
    var a2 chan str   // 声明一个str类型的chan
    var a3 chan []int // 声明一个int slice的chan
    

    1.2、创建channel

    channel是一个引用类型,往channel里写入数据相当于是拿到其内存地址操作,必须使用make函数初始化(通道类型的控制是nil)

    package main
    
    import "fmt"
    
    var a chan int
    
    func main(){
    	a = make(chan int,15) // 通道初始化(带缓冲区)
    	fmt.Println(a)     // 0xc000098000
    
    }
    

    1.3、channel操作

    通道有发送(send)、接收(receive)和关闭(close)三种操作,发送和接收都是用<-符号。
    先定义一个通道

    b := make(chan int,15)
    

    发送
    将一个值发送到通道中

    b <- 20 // 把20发送到b中
    

    接收

    x := <- b // 从b中接收值并赋值给变量x
    <-b       // 从b中接收值      
    

    关闭
    通过调用内置的close函数来关闭通道

    close(b)
    

    关于关闭通道需要注意的是,只有在通知接收方goroutine所有的数据都是发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件机制是不一样的,在结束操作之后关闭文件时必须要做的,但是关闭通道不是必须的。

    1.4、channel练习题

    需求:
    1.启动一个goroutine,生成100个数发送到ch1
    2.启动一个goroutine,从ch1中取值,计算其平方放到ch2中
    3.在main中,从ch2取值打印出来

    代码:

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    var wg sync.WaitGroup
    
    func f1(ch1 chan int){
    	defer wg.Done()
    	for i:=0;i<100;i++{
    		ch1 <- i
    	}
    	close(ch1)
    }
    
    func f2(ch1,ch2 chan int){
    	defer wg.Done()
    	for {
    		x,ok := <- ch1
    		if !ok{
    			break
    		}
    		ch2 <- x * x
    	}
    	close(ch2)
    }
    
    func main(){
    	a := make(chan int,100)
    	b := make(chan int,100)
    	wg.Add(2)
    	go f1(a)
    	go f2(a,b)
    	wg.Wait()
    	for ret := range b {
    		fmt.Println(ret)
    	}
    }
    

    2、单向通道

    有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如只能发送或只能接收。Go语言中提供了单向通道来处理这种情况,例如。我们把上面的例子改造如下:

    var wg sync.WaitGroup
    
    func f1(ch1 chan <- int){
    	defer wg.Done()
    	for i:=0;i<100;i++{
    		ch1 <- i
    	}
    	close(ch1)
    }
    
    func f2(ch1 <- chan  int, ch2 chan <- int){
    	defer wg.Done()
    	for {
    		x,ok := <- ch1
    		if !ok{
    			break
    		}
    		ch2 <- x * x
    	}
    	close(ch2)
    }
    
    func main(){
    	a := make(chan int,100)
    	b := make(chan int,100)
    	wg.Add(2)
    	go f1(a)
    	go f2(a,b)
    	wg.Wait()
    	for ret := range b {
    		fmt.Println(ret)
    	}
    }
    

    其中,chan <- int是一个只能发送的通道,可以发送但是不能接收;<- chan int是一个只能接收的通道,可以接收但是不能发送。

    3、worker pool(goroutine池)

    编写代码实现一个计算随机数的每个位置数字之和的程序,要求使用goroutinechannel构建生产者和消费者模式,可以指定启动的goroutine数量
    在工作中我们通常会使用workerpool模式,控制goroutine数量

    4、select多路复用

    在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。

    for {
    	data,ok := <- ch1
    	data,ok := <- ch2
    }
    

    这种方式虽然可以实现在多通道接受值的需求,但是运行性能会差很多,为了应对这个场景,Go内置select关键字,可以同时响应多个通道的操作,select的使用类似于switch语句,它有一些列cae分支和一个默认的分支。每个case会对应一个通道通信(接收和发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

    select {
    	case <- ch1:
    	     ...
    	case data := <- ch2
    		 ...
    	case ch3 <- data:
    	 	 ...
    	default:
    	     默认操作
    }
    

    举个例子来演示select的使用

    package main
    
    import "fmt"
    
    func main(){
    	ch := make(chan int,1)
    	for i:=0;i<10;i++{
    		select {
    		case x := <-ch:
    			fmt.Println(x)
    		case ch <- i:
    		}
    	}
    }
    

    5、sync

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    // 锁
    
    var x = 0
    var wg sync.WaitGroup
    
    func add(){
    	for i:=0;i<5000;i++{
    		x = x + 1
    	}
    	wg.Done()
    }
    
    func main(){
    	wg.Add(2)
    	go add()
    	go add()
    	wg.Wait()
    	fmt.Println(x)
    }
    

    上面的代码中我们开启两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与预期不符

    6、互斥锁

    互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。使用互斥锁来实现上面代码的问题

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    // 锁
    
    var x = 0
    var wg sync.WaitGroup
    var lock sync.Mutex // 声明一个全局锁
    func add(){
    	for i:=0;i<5000;i++{
    		lock.Lock()    // 上锁
    		x = x + 1
    		lock.Unlock()  // 释放锁
    	}
    	wg.Done()
    }
    
    func main(){
    	wg.Add(2)
    	go add()
    	go add()
    	wg.Wait()
    	fmt.Println(x)
    }
    

    使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其它的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

    7、读写互斥锁

    互斥锁是完全互斥的,但是有很多实际的场景下是读多写少,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加载锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMtex类型。

    读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其它的goroutine如果是获取读锁会继续获取锁,如果是获取写锁就会等待;大概一个goroutine获取写锁之后,其它的goroutine无论是获取读锁还是写锁都会等待。

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    var (
    	x = 0
    	wg sync.WaitGroup
    	lock sync.Mutex
    	rwLock sync.RWMutex
    )
    
    // 读操作
    func read(){
    	defer wg.Done()
    	//lock.Lock()
    	rwLock.RLock()
    	fmt.Println(x)
    	time.Sleep(time.Millisecond)
    	//lock.Unlock()
    	rwLock.RUnlock()
    }
    
    // 写操作
    func write(){
    	defer wg.Done()
    	//lock.Lock()
    	rwLock.Lock()
    	x = x + 1
    	time.Sleep(time.Millisecond * 5)
    	//lock.Unlock()
    	rwLock.Unlock()
    }
    func main(){
    	start := time.Now()
    	for i:=0;i<100;i++{
    		go write()
    		wg.Add(1)
    	}
    	time.Sleep(time.Second)
    	for i:=0;i<1000;i++{
    		go read()
    		wg.Add(1)
    	}
    	wg.Wait()
    	fmt.Println(time.Now().Sub(start))
    }
    

    8、sync.Once

    在编程的很多场景中,我们需要确保某些操作在高并发场景中只执行一次,例如只加载一次配置文件、只关闭一次通道等。
    Go语言中sync包中提供了一个针对只执行一次场景的解决方案sync.Once
    sync.Once只有一个Do方法,其签名如下:

    func (o *Once) Do(f func()){}
    

    如果要执行函数f需要传递参数就需要搭配闭包来使用。

    sync.map

    Go语言中内置的map不是并发安全的。请看下面实例:

    package main
    
    import (
    	"fmt"
    	"strconv"
    	"sync"
    )
    
    var m2 = sync.Map{}
    func main(){
    	wg := sync.WaitGroup{}
    	for i:=0;i<21;i++{
    		wg.Add(1)
    		go func(n int){
    			key := strconv.Itoa(n)
    			m2.Store(key,n)
    			value,_ := m2.Load(key)
    			fmt.Printf("k=:%v,v=:%v
    ",key,value)
    			wg.Done()
    		}(i)
    	}
    	wg.Wait()
    }
    
  • 相关阅读:
    通过SQL Server 2008数据库复制实现数据库同步备份
    SQL Server进制
    Server2008+SQL2008 日志读取代理器未运行 进程无法在“WINXXX”上执行“sp_replcmds”
    swing中使用皮肤包
    JTextArea的自动定位最后一行
    JFrame如何设置背景图片
    swing中单击回车相当于点击登录
    execute、executeUpdate、executeQuery三者的区别(及返回值)
    JTable设置透明
    在eclipse中导入android项目
  • 原文地址:https://www.cnblogs.com/jasonminghao/p/12348281.html
Copyright © 2011-2022 走看看