线程, 程序运行的分支
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照样正常执行