zoukankan      html  css  js  c++  java
  • Go 深入多线程

     
    Golang

    介绍

    线程是cpu调度的最小单位,只有不同的线程才能同时在多核cpu上同时运行。但线程太占资源,线程调度开销大。go中的goroutine是一个轻量级的线程,执行时只需要4-5k的内存,比线程更易用,更高效,更轻便,调度开销比线程小,可同时运行上千万个并发。
    go语言中开启一个goroutine非常简单,go函数名(),就开启了个线程。

    默认情况下,调度器仅使用单线程,要想发挥多核处理器的并行处理能力,必须调用runtine.GOMAXPROCS(n)来设置可并发的线程数,也可以通过设置环境变量GOMAXPROCS打到相同的目的。

    goroutine

    Runtime包中提供了几个与goroutine相关的函数。Gosched()让当前正在执行的goroutine放弃CPU执行权限。调度器安排其他正在等待的线程运行。
    请看以下例子:

    package main
    
    import (
        "runtime"
        "fmt"
    )
    
    func main(){
        go sayHello()
        go sayWorld()
        var str string
        fmt.Scan(&str)
    }
    
    func sayHello(){
        for i := 0; i < 10; i++{
            fmt.Print("hello ")
            runtime.Gosched()
        }
    }
    
    func sayWorld(){
        for i := 0; i < 10; i++ {
            fmt.Println("world")
            runtime.Gosched()
        }
    }
    
     
    运行结果

    从上面输出结果可知,我们启动了两个线程,其中一个线程输出一句后调用Gosched函数,释放CPU权限;之后另一个线程获得CPU权限。这样两个线程交替获得cpu权限,才输出了以上结果。

    runtime.NumCPU()返回了cpu核数,runtime.NumGoroutine()返回当前进程的goroutine线程数。即便我们没有开启新的goroutine。

    package main
    
    import (
        "runtime"
        "fmt"
    )
    func main(){
        fmt.Println(runtime.NumCPU())
        fmt.Println(runtime.NumGoroutine())
    }
    
     
    运行结果

    runtime.Goexit()函数用于终止当前的goroutine,单defer函数将会继续被调用。

    package main
    
    import (
        "runtime"
        "fmt"
    )
    
    func test(){
        defer func(){
            fmt.Println(" in defer")
        }()
        for i := 0; i < 10; i++{
            fmt.Print(i)
            if i > 5{
                runtime.Goexit()
            }
        }
    }
    
    func main(){
        go test()
        var str string
        fmt.Scan(&str)
    }
    
     
    运行结果

    在这里大家或许有个疑问,下面这两句代码干嘛的呢

    var str string
    fmt.Scan(&str)
    

    这两句代码是等待输入的意思,在这里用来阻止主线程关闭的。如果没有这两句的话,会发现我们的程序瞬间就结束了,而且什么都没有输出。这是因为主线程关闭之后,所有开启的goroutine都会强制关闭,他还没有来得及输出,就结束了。
    但是这样感觉怪怪的。如果有一种机制,在子线程结束的时候通知一下主线程,然后主线程再关闭,岂不是更好,这样就不用无休止的等待了。于是就有了channel

    channel

    goroutine之间通过channel来通讯,可以认为channel是一个管道或者先进先出的队列。你可以从一个goroutine中向channel发送数据,在另一个goroutine中取出这个值。
    使用make创建

    var channel chan int = make(chan int)
    // 或
    channel := make(chan int)
    

    生产者/消费者是最经典的使用示例。生产者goroutine负责将数据放入channel,消费者goroutine从channel中取出数据进行处理。

    package main
    
    import (
        "fmt"
    )
    
    func main(){
        buf:=make(chan int)
        flg := make(chan int)
        go producer(buf)
        go consumer(buf, flg)
        <-flg //等待接受完成
    }
    
    func producer(c chan int){
        defer close(c) // 关闭channel
        for i := 0; i < 10; i++{
            c <- i // 阻塞,直到数据被消费者取走后,才能发送下一条数据
        }
    }
    
    func consumer(c, f chan int){
        for{
            if v, ok := <-c; ok{
                fmt.Print(v) // 阻塞,直到生产者放入数据后继续读取数据
            }else{
                break
            }
        }
        f<-1 //发送数据,通知main函数已接受完成
    }
    
     
    运行结果

    可以将channel指定为单向通信。比如<-chan int仅能接收,chan<-int仅能发送。之前的生产者消费者可以改为一下方式:
    func producer(c chan<-int){
        defer close(c) // 关闭channel
        for i := 0; i < 10; i++{
            c <- i // 阻塞,直到数据被消费者取走后,才能发送下一条数据
        }
    }
    
    func consumer(c <-chan int, f chan<-int){
        for{
            if v, ok := <-c; ok{
                fmt.Print(v) // 阻塞,直到生产者放入数据后继续读取数据
            }else{
                break
            }
        }
        f<-1 //发送数据,通知main函数已接受完成
    }
    

    channle可以是带缓冲的。make的第二个参数作为缓冲长度来初始化一个带缓冲的channel:

    c := make(chan int, 5)
    

    向带缓冲的channel发送数据时,只有缓冲区满时,发送操作才会被阻塞。当缓冲区空时,接收才会阻塞。
    可以通过以下程序调整发送和接收的顺序调试

    package main
    
    import (
        "fmt"
    )
    
    func main(){
        c := make(chan int, 2)
        c <- 1
        c <- 2
        fmt.Println(<-c)
        fmt.Println(<-c)
    }
    

    select

    如果有多个channel需要监听,可以考虑用select,随机处理一个可用的channel

    package main
    
    import (
        "fmt"
    )
    
    func main(){
        c := make(chan int)
        quit := make(chan int)
        go func(){
            for i := 0; i < 10; i++{
                fmt.Printf("%d ", <-c)
            }
            quit <- 1
        }()
        testMuti(c, quit)
    }
    
    func testMuti(c, quit chan int){
        x, y := 0, 1
        for {
            select{
            case c<-x:
                x, y = y, x+y
            case <-quit:
                fmt.Print("
    quit")
                return
            }
        }
    }
    
     
    运行结果

    channle超时机制

    当一个channel被read/write阻塞时,会被一直阻塞下去,直到channel关闭。产生一个异常退出程序。channel内部没有超时的定时器。但我们可以用select来实现channel的超时机制

    package main
    
    import (
        "time"
        "fmt"
    )
    
    func main(){
        c := make(chan int)
        select{
        case <- c:
            fmt.Println("没有数据")
        case <-time.After(5* time.Second):
            fmt.Println("超时退出")
        }
    }
    
     
    运行结果

    线程同步

    假设现在我们有两个线程,一个线程写文件,一个线程读文件。如果在读文件的同时,写文件的线程向文件中写数据,就会出现问题。为了保证能够正确的读写文件,在读文件的时候,不能进行写入文件的操作,在写入时,不能进行读的操作。这就需要互斥锁。互斥锁是线程间同步的一种机制,用了保证在同一时刻只用一个线程访问共享资源。go中的互斥锁在sync包中。下面是个线程安全的map:

    package main
    
    import (
        "errors"
        "sync"
        "fmt"
    )
    
    func main(){
        m := &MyMap{mp:make(map[string]int), mutex:new(sync.Mutex)}
        go SetValue(m)
        go m.Display()
        var str string
        fmt.Scan(&str)
    }
    
    type MyMap struct{
        mp map[string]int
        mutex *sync.Mutex
    }
    
    func (this *MyMap)Get(key string)(int, error){
        this.mutex.Lock()
        i, ok := this.mp[key]
        this.mutex.Unlock()
        if !ok{
            return i, errors.New("不存在")
        }
        return i, nil
    }
    
    func (this *MyMap)Set(key string, val int){
        this.mutex.Lock()
        defer this.mutex.Unlock()
        this.mp[key] = val
    }
    
    func (this *MyMap)Display(){
        this.mutex.Lock()
        defer this.mutex.Unlock()
        for key, val := range this.mp{
            fmt.Println(key, "=", val)
        }
    }
    
    func SetValue(m *MyMap){
        var a  rune
        a = 'a'
        for i := 0; i< 10; i++{
            m.Set(string(a+rune(i)), i)
        }
    }
    
     
    运行结果

  • 相关阅读:
    yzoj P2344 斯卡布罗集市 题解
    yzoj P2350 逃离洞穴 题解
    yzoj P2349 取数 题解
    JXOI 2017 颜色 题解
    NOIP 2009 最优贸易 题解
    CH 4302 Interval GCD 题解
    CH4301 Can you answer on these queries III 题解
    Luogu2533[AHOI2012]信号塔
    Luogu3320[SDOI2015]寻宝游戏
    Luogu3187[HNOI2007]最小矩形覆盖
  • 原文地址:https://www.cnblogs.com/cxy2020/p/14595691.html
Copyright © 2011-2022 走看看