Go语言-并发

并发的含义

并发:逻辑上具备同时处理多个任务的能力
并行:物理上在同意时刻执行多个并发任务
多线程或多进程是并行的基本条件,但是单线程也可用协程做到并发。尽管协程在单个线程上通过主动切换来实现多任务并发。通常情况下,用多进程来实现分布式和负载均衡。用多线程抢夺更多的处理器资源。使用协程来提高处理器时间片利用率。
Go语言创建并发任务只需在函数调用前添加关键字“go”

go println("hello word!")  //创建并发任务
go func(s string){         //使用匿名函数
    println(s)
}("hello word!")

关键字“go”并非执行并发操作,而是创建一个创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。
与defer一样,goroutine也会因“延迟执行”而立即计算并复制执行参数。

package main
import(
    "fmt"
    "time"
)
var c int              //未初始化会默认为0
func counter() int{
    c++
    return c
}
func main(){
    a := 10
    go func(x,y int){            //goroutine在main逻辑后执行
        time.Sleep(time.Second)
        fmt.Println("go:",x,y)
    }(a,counter())               //立即执行并复制参数
    a += 10
    fmt.Println("main:",a,counter())
    time.Sleep(time.Second *3)
}

输出:
main: 20 2
go: 10 1

wait

进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号。

package main
import(
    "fmt"
    "time"
)
func main(){
    exit := make(chan struct{})  //创建通道,通道只能使用make创建
    go func(){
        time.Sleep(time.Second)
        fmt.Println("go done!")
        close(exit)             //关闭通道
    }()
    fmt.Println("start....")
    <-exit                      //如通道关闭,结束阻塞
    fmt.Println("exit....")
}

输出:
start….
go done!
exit….
除了关闭通道外,写入数据也可以解除阻塞。

package main
import(
    "fmt"
    "time"
)
func main(){
    exit := make(chan int)
    go func(){
        time.Sleep(time.Second)
        fmt.Println("go done!")
        exit<-1            //写入数据
    }()
    fmt.Println("start....")
    <-exit                //读出数据
    fmt.Println("exit....")
}

输出:
start….
go done!
exit….
如果要等待多个任务结束,推荐使用sync.WaitGroup,通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。

package main
import(
    "fmt"
    "time"
    "sync"
)
func main(){
    var wg sync.WaitGroup
    for i := 0;i < 10;i++{    //累计计数,注意go中只支持后加加
        wg.Add(1)
        go func(id int){          
            defer wg.Done()        //递减计数
            time.Sleep(time.Second)
            fmt.Println("go ",id,"done")
        }(i)
    }
    fmt.Println("main:...")
    wg.Wait()                   //阻塞直至计数归零
    fmt.Println("main exit")
}

输出:
main:…
go 2 done
go 3 done
go 5 done
go 8 done
go 4 done
go 7 done
go 1 done
go 6 done
go 9 done
go 0 done
main exit
尽管WaitGroup.Add实现了原子操作,但建议在goroutine外累加计数器,以免Add尚未执行,wait以及退出。可在多处使用wait阻塞,它们都能接收到通知。

package main
import(
    "fmt"
    "time"
    "sync"
)
func main(){
    var wg sync.WaitGroup
    wg.Add(1)
    go func(){
        wg.Wait()
        fmt.Println("wait exit!")
    }()
    go func(){
        time.Sleep(time.Second)
        fmt.Println("done")
        wg.Done()
    }()
    wg.Wait()
    fmt.Println("main exit")
}

输出:
done
wait exit!
main exit

GOMAXPROCS

运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等,可使用runtime.GOMAXPROCS函数(或环境变量)修改。如果参数小于1,会返回当前设置值,不做任何调整。runtime.NumCPU函数可返回当前机器的核数。

package main
import(
    "fmt"
    "runtime"
)
func main(){
    n := runtime.GOMAXPROCS(0)
    m := runtime.NumCPU()
    fmt.Println(n,m)
}

输出:8 8 当前测试机器为8核。
goroutine任务无法设置优先级,无法设置优先级,无法获取编号,没有局部存储,甚至连返回值都会被丢弃。但是除了优先级外,其他的功能都容易实现。

package main
import(
    "fmt"
    "sync"
)

func main(){
    var wg sync.WaitGroup
    var gs [5]struct{
        id int           //编号
        result int       //返回值
    }
    for i := 1;i < len(gs);i++{
        wg.Add(1)
        go func(id int){
            defer wg.Done()
            gs[id].id = id
            gs[id].result = (id + 1) *100
        }(i)
    }
    wg.Wait()
    fmt.Printf("%+v\n",gs)
}

输出:[{id:0 result:0} {id:1 result:200} {id:2 result:300} {id:3 result:400} {id:4 result:500}]

Gosched

暂停,释放线程去执行其他任务。当前线程被放回队列,等待下次调度时恢复执行。

package main
import(
    "fmt"
    "runtime"
)

func main(){
    runtime.GOMAXPROCS(1)
    exit := make(chan struct{})
    go func(){                  //任务a
        defer close(exit)
        fmt.Println("go a")
        go func(){             //任务b
            fmt.Println("go b")
        }()
        for i := 0;i < 5;i++{
            fmt.Println("a:",i)
            if i == 1{
                runtime.Gosched()  //调度执行b
            }
        }
    }()
    <-exit
}

输出:
go a
a: 0
a: 1
go b
a: 2
a: 3
a: 4
该函数很少被使用,只是在主动切换时会被使用

Goexit

Goexit立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不影响其他并发任务,不会引发panic,无法捕获。

package main
import(
    "fmt"
    "runtime"
)

func main(){
    exit := make(chan struct{})
    go func(){
        defer close(exit)          //执行
        defer fmt.Println("a")     //执行
        func(){
            defer func(){
                fmt.Println("b",recover()==nil)   //执行,recover返回nil
            }()
            func(){
                fmt.Println("c")
                runtime.Goexit()            //多层调用中执行Goexi()
                fmt.Println("c done")       //不会执行
            }()
            fmt.Println("b done")           //不会执行
        }()
        fmt.Println("a done")               //不会执行
    }()
    <-exit
    fmt.Println("main exit")
}

输出:
c
b true
a
main exit

通道 channel

通道只可使用make创建,分为有缓存通道以及无缓存通道。
同步模式必须有配对操作的goroutine出现,否则会一直阻塞。而异步模式在缓冲区未满时或数据未被读完前不会阻塞。多数情况下异步通道有助于提升性能,减少排队阻塞。

package main
import(
    "fmt"
)

func main(){
    c := make(chan int,3)   //创建一个带3个缓冲的异步通道,int后无参数为无缓存的通道
    c <- 1                  //缓冲区未满不会阻塞
    c <- 2
    fmt.Println(<-c)        //缓冲区尚有数据不会阻塞
    fmt.Println(<-c)
}

输出:
1
2
缓冲区大小仅是内部属性,不属于类型组成部分。另外通道变量本身就是指针,可用相等操作符判断是否为同一对象或者为nil。内置函数cap和len返回缓冲区大小和当前缓冲区数量。而对于同步通道则都返回0,据此可判断通道是同步还是异步。

收发

除使用简单发送和接受操作符外,还可以使用ok-idiom或range模式处理数据。

package main
import(
    "fmt"
)

func main(){
    c := make(chan int)
    go func(){
        for{
            x, ok := <-c
            if !ok{
                return
            }
            fmt.Println(x)
        }
    }()
    c <- 1
    c <- 2
    c <- 3
    close(c)
}

输出:
1
2
3
对于循环接收数据,range模式更简洁一些。一次性事件使用close效率更高,没有多余开销。连续多样性事件,可传递不同数据标志实现还可以使用sync.Cond实现单播或广播事件。
对于closed或nil通道,发送和接收操作都有相应的规则:
- 向以关闭通道发送数据会引发panic
- 从已关闭接收数据,返回已缓冲数据或零值
- 无论收发nil通道都会阻塞
- 重复关闭或关闭nil通道会引发panic错误

单向通道

通道默认是双向的,并不区分发送和接收端。但在某些时候,我们可限制收发操作的方向来获得更严谨的操作逻辑。尽管可使用make创建单向通道,但是那没有什么意义。通常使用类型转换类获取单向通道并赋予操作双方。
使用make直接创建单向通道:

ReadOnly := make(<-chan int)  //创建只读channel
WriteOnly := make(chan<- int) //创建只写channel
package main
import(
    "fmt"
    "sync"
)

func main(){
    var wg sync.WaitGroup
    wg.Add(2)
    c := make(chan int)
    var send chan<- int = c   //单向通道
    var recv <-chan int = c
    go func(){
        defer wg.Done()
        for x := range recv{
            fmt.Println(x)
        }
    }()
    go func(){
        defer wg.Done()
        defer close(c)
        for i := 0;i < 3;i++{
            send <- i
        }       
    }()
    wg.Wait()
}

输出:
0
1
2

  • 不能再单向通道上做逆向操作
  • close不用用于接收端
  • 无法将单向通道重新转换回去

选择 select

如要同时处理多个通道,可选用select语句。它会随机选择一个可用通道做收发操作。

package main
import(
    "fmt"
    "sync"
)

func main(){
    var wg sync.WaitGroup
    wg.Add(2)
    a,b := make(chan int),make(chan int)
    go func(){                    //接收端
        defer wg.Done()          
        for {
            var(
                name string
                x int 
                ok bool
            )
            select{                //随机选择可用channel接收数据
                case x,ok = <-a:
                    name = "a"
                case x,ok = <-b:
                    name = "b"
            }
            if !ok{              //若任意通道关闭则结束
                return
            }
            fmt.Println(name,x)
        }
    }()
    go func(){                  //发送端
        defer wg.Done()
        defer close(a)
        defer close(b)
        for i := 0;i < 10;i++{
            select{               //随机选择发送channel
                case a <- i:
                case b <- i*10:
            }
        }       
    }()
    wg.Wait()
}

输出:
b 0
b 10
b 20
b 30
b 40
b 50
b 60
b 70
a 8
a 9
如果要等全部通道消息处理结束(closed),可将完成通道设置为nil。这样它就会被阻塞,不在被select选中。即便是同一个通道也会随机选择

同步

通道并不是用来取代锁的,它们有各自不同的使用场景。通道倾向于解决逻辑层次的并发处理架构,而锁则用来保护局部范围内的数据安全。标准库sync提供了互斥和读写锁,领有原子操作等,可满足日常开发需求。Mutex、RWMutex的使用并不复杂,只有几个地方需要注意。

  • 将Mutex作为匿名字段时,相关方法必须实现为pointer-reveiver,否则会因复制导致锁机制失效。
  • 应将Mutex粒度控制在最小范围内,及早释放
  • Mutex不支持递归,即便在同一goroutine也会导致死锁
  • 对读写性能要求较高时,应避免使用defer Unlock
  • 读写并发时,用RWMutex性能会更好
  • 对单个数据读写保护可尝试用原子操作
package main
import(
    "fmt"
    "time"
    "sync"
)
type data struct{
    sync.Mutex
}
func (d data)test(s string){
    d.Lock()
    defer d.Unlock()
    for i := 0;i < 5;i++{
        fmt.Println(s,i)
        time.Sleep(time.Second)
    }
}
func main(){
    var wg sync.WaitGroup
    wg.Add(2)
    var d data
    go func(){
        defer wg.Done()
        d.test("read")
    }()
    go func(){
        defer wg.Done()
        d.test("write")
    }()
    wg.Wait()
}

输出:
write 0
read 0
read 1
write 1
write 2
read 2
read 3
write 3
write 4
read 4

发布了27 篇原创文章 · 获赞 7 · 访问量 5万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览