go 线程与channel

线程, 程序运行的分支

go中的线程是轻量级的线程, 不需要使用线程池管理

开启线程: go func()

import (
    "fmt"
    "runtime"
)
func main() {
    maxCpu := runtime.NumCPU()        // 获取cpu核数
    fmt.Println(maxCpu)
    runtime.GOMAXPROCS(maxCpu)        // 使用多少核运行程序(go1.8以后默认使用全部核数)
}

多线程的程序会存在资源共享竞争, 所以在需要的地方要加锁

互斥锁: 在写多读少的地方使用

读写锁: 在读多写少的地方使用

import (
    "fmt"
    "time"
    "sync"
)

// 全局数据
var sumMap = make(map[int]int)
// 互斥锁
var lock sync.Mutex

func main() {
    for i:=0; i<10; i++ {
        go calcNum(i)
    }
    time.Sleep(time.Second * 2)
	
    lock.Lock()
    for k, v := range sumMap {			// 读共享数据也要加锁
        fmt.Printf("%d --> %d\n", k, v)
    }
    lock.Unlock()
}

// 给一个整数, 求其累加和
func calcNum(n int) {
    sum := 0
    for i:=0; i<n; i++ {
        sum += i
    }

    lock.Lock()				// 加锁
    // 将结果存起来
    sumMap[n] = sum			// 多个goroute访问它, 可能千万数据错误, 所以需要加锁
    lock.Unlock()			// 解锁
}

这个是使用全局变量来做到线程同步的, 这种必须要加锁

另一种方式, 使用Channel来同步

它是线程安全的, 多个goroute同时访问, 不用加锁

Channel类似于管道 -- 先进先出

Channel是一种类型, 声明时和普通变量一样, 只多一个关键字chan

var t chan int  -->只能放int型数据的管道(变量t的类型为 chan int 型)

也可以定义只读或只写的channel

read_only := make (<-chan int)        // 只读

write_only := make (chan<- int)      // 只写

read_write := make (chan int)         // 可读写

只读和只写一般用于在参数传递中, 防止错误操作, 一般不定义只读或只写的channel

channel是引用类型, 和map, 切片一样, 在使用前必须要先分配空间, 使用make

var t chan int = make(chan int, 10)

type Student struct {
    name string
}

func main() {
    var c chan *Student = make(chan *Student, 10)
    stu := &Student{
        name : "张三",
    }
    c <- stu		// 将stu放入管道
    var stu2 *Student = <- c		// 从管道中取出第一个元素
    fmt.Printf(stu2.name)
}

channel和map, 切片一样, 可以存入任何类型的数据 (空接口)

type Student struct {
    name string
}

func main() {
    var c chan interface{} = make(chan interface{}, 10)
	
    stu := &Student{
        name : "张三",
    }
    c <- stu
	
    var s interface {} = <- c
    student, ok := s.(*Student)	// 从一般类型转换为具体类型
    if !ok {
        fmt.Printf("can not conver to Student")
        return
    }
    fmt.Printf(student.name)
}

管道阻塞

如果管道是 满载空载, 则会自动阻塞写入或 读取

关闭管道后, 读取元素, 不会再阻塞, 所以对不再存入元素的管道, 将其关闭

func main() {
    var c = make(chan int, 10)
    for i:=0; i<10; i++ {
        c <- i
    }
    close(c)	// 关闭管道后, 不能再写入元素, 但是可以读取元素
    for {
        i, ok := <- c	// 和map类似, 取出元素的时候, 可以判断, 如果为false, 则表示没有元素了
        if !ok {
            break
        }
        fmt.Printf("%d\n", i)
    }
}
func insertElem(c chan string) {
    for i:=0; i<100; i++ {
        c <- strconv.Itoa(i) + "A"
        fmt.Printf("put data %d\n", i)
    }
}

func getElem(c chan string) {
    time.Sleep(time.Second)    // 先让管道满载
    for {
        var str string
        str = <- c
        fmt.Printf("get data %s\n", str)
    }
}

func main() {
    var c chan string = make(chan string, 10)
    go insertElem(c)
    go getElem(c)
    time.Sleep(time.Second * 20)
}

和map, 切片, 数组一样, 可以使用len函数获取其元素个数:

func main() {
    var c chan int = make(chan int, 1000)
    for i:=0; i< 100; i++ {
        c <- i                // 保证放入元素的个数小于容量, 否则会阻塞 --> deadlock (死锁)
}
    length := len(c)
    fmt.Printf("%d", length)
}

关闭管道:

func main() {
    var c = make(chan int, 10)
    for i:=0; i<10; i++ {
        c <- i
    }
    close(c)	// 关闭管道后, 能再写入元素, 但是可以读取其中的元素
    for {
        i, ok := <- c	// 和map一样, 取出元素的时候, 可以判断一下是否取到了
        if !ok {
            break
        }
        fmt.Printf("%d\n", i)
    }
}

使用for range循环管道:

for range 有个操作, 会取走管道中的元素, 不需要使用: <-

func main() {
    var c chan int = make(chan int, 1000)
    for i:=0; i< 100; i++ {
        c <- i
    }
	
    for v := range c {
        fmt.Printf("元素为: %d\n", v)
        fmt.Printf("元素个数为: %d\n", len(c))

        fmt.Println("--------------")
       
        /*
        if len(c) == 0{
            close(c)            // 关闭管道
        }
        */
    }
}

因为最后所有元素都被取走了, 程序处于阻塞状态 -- > deadlock!

为避免deadlock, 打开注释就行了

协程通信:

如下以数据转移为例, 演示协程通信

package main
import (
    "fmt"
)

//注入原始数据
func insertData(source chan int, flag chan int) {
    for i:=0; i<10; i++ {
        source <- i
    }
    close(source)	// 这里必须要关闭, 因为下面recvData函数在循环source(不再向channel中添加数据时应尽早关闭它)
    flag <- 1		// 在函数末尾, 向flag这个channel中随意加一个数据
}

//转移数据
func recvData(dest chan int, source chan int, flag chan int) {
    for {
        data, ok := <- source
        if !ok {		// 判断是否成功从channel中获取数据
            break
        }
        dest <-data
    }
    close(dest)
    flag <- 1
}

/*将一个goroute中的数据转移到另一个goroute中*/
func main() {
    var source = make(chan int, 10)
    var dest = make(chan int, 10)
    var flag = make(chan int, 2)		// 用于统计协程结束的状态
	
    // 向source添加数据
    go insertData(source, flag)			// 启动一个协程

    // 从source转移数据到 dest
    go recvData(dest, source, flag)		// 再启动一个协程

    /*如下使用channel阻塞的特性, 来保证上面的两个协程执行完了其中的所有代码*/
    count := 0
    for _ = range flag {
        count ++			// 读取到一个数据, 就加1
        if count == 2 {		// 为了保证flag channel不deadlock
            break
        }
    }

    // 检测dest channle中的数据
    for v := range dest {
        fmt.Println(v)
    }
}
/*
    注: 当循环一个channel时, 如果该channel没有关闭, 则可能会deadlock
    如果循环一个没有关闭的channel时, 应当小心deadlock
    如上面循环flag channel, 或循环source 两种方式都可使用
*/

select ... case 处理阻塞:

func main() {
    c1 := make(chan int, 10)
    c2 := make(chan int, 10)

    go func() {
        for i:=0; i<10; i++ {
            c1 <- i
            c2 <- 2*i
            time.Sleep(time.Second)
        }
    }()

    for {
        select {
            case v := <- c1 : fmt.Println(v)    
            case v := <- c2 : fmt.Println(v)
            default : fmt.Println("获取数据超时")
            time.Sleep(time.Second)
        }
    }
}

先从case中做一个预判断, 如果没有获取到, 则它不会执行这个case的语句, 也就不会阻塞

当所有case都不执行, 则会执行default, 从而default代替了阻塞

保护进程:

一个进程由主线程和多个协程共同组成

如果某个协程出现异常, 则进程会被终止

不同的协程可以做不同的任务, 那么当一个协程 "挂掉" 后另外的协程应该继续运行

使用defer - recover捕捉异常, 让让进程不会终止:

/*任务函数1*/
func calc(a, b int, c chan int) {
    defer handDefer()          // 在这时接入defer处理函数
    c <- a+b
    var m map[string]int       // 用一个map来制造异常
    m["a"] = 1                 // 这里会出现nil指针错误
    fmt.Println(m)
}

/*任务函数2*/
func showTime() {
    defer handDefer()
    t := time.Now()
    fmt.Printf("现在时间是: %s", t)
}

/*封装了处理异常的函数*/
func handDefer() {
    fmt.Println("进入defer...")
    if err := recover(); err != nil {
        fmt.Println("panic: ", err)	
    }
}

func main() {
    a := 10
    b := 20
    c := make(chan int, 1)

    go calc(a, b, c)		// 让协程1求和
    sum := <- c
    fmt.Println(sum)

    go showTime()		// 让协程2呈现当前时间
}

这个协程1会出现panic, 但是协程2照样正常执行

猜你喜欢

转载自blog.csdn.net/lljxk2008/article/details/87778842