多路复用

Go语言中提供了一个关键字select,通过select可以监听channel上的数据流动。select的用法与switch语法类似,由select开始一个新的选择块,每个选择条件由case语句来描述。只不过,select的case有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作。

select 语法如下:

   select {
    case <-chan1:
        // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
        // 如果成功向chan2写入数据,则进行该case处理语句
    default:
        // 如果上面都没有成功,则进入default处理流程
    }

在一个select语句中,会按顺序从头至尾评估每一个发送和接收的语句;如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:⑴ 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。⑵ 如果没有default语句,那么select语句将被阻塞,直到至少有一个channel可以进行下去。

在一般的业务场景下,select不会用default,当监听的流中再没有数据,IO操作就 会阻塞现象,如果使用了default,此时可以出让CPU时间片。如果使用了default 就形成了非阻塞状态,形成了忙轮训,会占用CPU、系统资源。

阻塞与非阻塞使用场景

  • 阻塞: 如:在监听超时退出时,如果100秒内无操作,择退出,此时添加了default会形成忙轮训,超时监听变成了无效。
  • 非阻塞: 如,在一个只有一个业务逻辑处理时,主进程控制进程的退出。此时可以使用default。

定时器

Go语言中定时器的使用有三个方法

  • time.Sleep()
  • time.NewTimer() 返回一个时间的管道, time.C 读取管道的内容
  • time.After(5 * time.Second) 封装了time.NewTimer(),反回了一个 time.C的管道

示例

select {
case <-time.After(time.Second * 10):
}

锁和条件变量

Go语言中为了解决协程间同步问题,提供了标准库代码,包syncsync/atomic中。

互斥锁

互斥锁是传统并发编程对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。

package main

import (
"fmt"
"runtime"
"sync"
"time"
) var mutex sync.Mutex func print(str string) {
mutex.Lock() // 添加互斥锁
defer mutex.Unlock() // 使用结束时解锁 for _, data := range str { // 迭代器
fmt.Printf("%c", data)
time.Sleep(time.Second) // 放大协程竞争效果
}
fmt.Println()
} func main() {
go print("hello") // main 中传参
go print("world")
for {
runtime.GC()
}
}

读写锁

读写锁的使用场景一般为读多写少,可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine不能进行读写操作;当一个goroutine获取读锁之后,其他的goroutine获取写锁都会等待

package main

import (
"fmt"
"math/rand"
"sync"
"time"
) var count int // 全局变量count
var rwlock sync.RWMutex // 全局读写锁 rwlock func read(n int) {
for {
rwlock.RLock()
fmt.Printf("reading goroutine %d ...\n", n)
num := count
fmt.Printf("read goroutine %d finished,get number %d\n", n, num)
rwlock.RUnlock()
} }
func write(n int) {
for {
rwlock.Lock()
fmt.Printf("writing goroutine %d ...\n", n)
num := rand.Intn(1000)
count = num
fmt.Printf("write goroutine %d finished,write number %d\n", n, num)
rwlock.Unlock()
}
} func main() {
for i := 0; i < 5; i++ {
go read(i + 1)
time.Sleep(time.Microsecond * 100)
}
for i := 0; i < 5; i++ {
go write(i + 1)
time.Sleep(time.Microsecond * 100)
}
for { }
}

可以看出,读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间不存在互斥关系。

Go语言中的死锁

死锁 deadlock 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

单gorutine同时读写,写死锁

在一个gorutine中,当channel无缓冲,写阻塞,等待读取导致死锁

解决,应该至少在2个gorutine进行channle通讯,或者使用缓冲区。

package main

func main() {
channel := make(chan int)
channel <- 1
<-channel
}

多gorutine使用一个channel通信,写先于读

代码顺序执行时,写操作阻塞,导致后面协程无法启动进行读操作,导致死锁

package main

func main() {
channel := make(chan int)
channel <- 1
go func() {
<-channel
}()
}

多channel交叉死锁

在goroutine中,多个goroutine使用多个channel互相等待对方写入,导致死锁

package main

func main() {

	channel1 := make(chan int)
channel2 := make(chan int) go func() { select {
case <-channel1:
channel2 <- 1
}
}() select {
case <-channel2:
channel1 <- 1
}
}

隐性死锁

尽量不要将 互斥锁、读写锁 与 channel 混用情况下,让读先进行读时,因为没写入被阻塞,无法解除。写入时,因为没有读出被阻塞,锁无法解除,导致无数据输出,形成隐形死锁。此时编译器是不报错的。

package main

import (
"fmt"
"sync"
) func main() {
channel := make(chan int)
var rwlock sync.RWMutex
go func() {
for {
rwlock.Lock()
channel <- 1
fmt.Println("write", 1)
rwlock.Unlock()
}
}() go func() {
for {
rwlock.RLock()
n := <-channel
fmt.Println(n)
rwlock.Unlock()
} }() for { }
}

Context 上下文

context定义了上下文类型,该类型在API边界之间以及进程之间传递截止时间,取消信号和其他请求范围的值。当在对请求传入一个上下文,可以选择将其替换为使用WithCancelWithDeadlineWithTimeout。在取消后,从该context处派生的所有子请求也会被取消。

Context的结构体

  • Deadline() 返回context的截止时间。
  • Done() 返回一个channle,当timeout或cancelfuc将会close(chan)
  • Err() 返回错误,未关闭Done()返回nil,取消,返回 "context canceled", Deadline返回超时
  • Value 返回值。
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool) // Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{} // If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
Err() error // Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stored using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key interface{}) interface{}
}

演示使用可取消的上下文。可在函数结束时defer cancel() 防止goroutine的泄露。

package main

import (
"context"
"fmt"
"time"
) func worker(ctx context.Context, name string) {
n := 0
for {
select {
case <-ctx.Done():
fmt.Println(name, "去划水了", n)
return
default:
fmt.Println(name, "干活中", n)
time.Sleep(time.Second)
}
n++
}
} func main() {
ctx, cancel := context.WithCancel(context.Background()) for n := 0; n < 5; n++ {
go worker(ctx, fmt.Sprintf("worker%d", n))
} <-time.After(time.Second * 5)
cancel()
for { }
}

超时处理,WithTimeout 当时间到达设置的时间后退出,也可以使用cancelFunc()退出处理

package main

import (
"context"
"fmt"
"time"
) func worker(ctx context.Context, name string) {
n := 0
for {
select {
case <-ctx.Done():
fmt.Println(name, "去划水了", n)
return
default:
fmt.Println(name, "干活中", n)
time.Sleep(time.Second)
}
n++
}
} func main() {
//ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) for n := 0; n < 2; n++ {
go worker(ctx, fmt.Sprintf("worker%d", n))
} <-time.After(time.Second * 5)
fmt.Println("取消了")
cancel()
}

WithDeadline,在标准库中可以看出,实际上WithTimeout是封装了WithDeadline。其功能也是超时退出。

package main

import (
"context"
"fmt"
"time"
) func worker(ctx context.Context, name string) {
n := 0
for {
select {
case <-ctx.Done():
fmt.Println(name, "去划水了", n)
fmt.Println(ctx.Err())
return
default:
fmt.Println(name, "干活中", n)
time.Sleep(time.Second)
}
n++
}
} func main() {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3))
for n := 0; n < 2; n++ {
go worker(ctx, fmt.Sprintf("worker%d", n))
} <-time.After(time.Second * 5)
fmt.Println("取消了")
cancel()
}

Context总结

  • Context是Go语言在1.7中加入标准库的,是作为Goroutine线程安全,防止线程泄露的上下文管理的操作。
  • context包的核心是Context结构体。
  • Context的常用方法为 WithTimeout()WithCancel()
  • Context在使用时,不要放在结构体内使用,要以函数的参数进行传递。
  • Context是线程安全的,可以在多个Goroutine传递,当对其取消操作时,所有Goroutine都执行取消操作。

golang:协程安全的更多相关文章

  1. golang协程同步的几种方法

    目录 golang协程同步的几种方法 协程概念简要理解 为什么要做同步 协程的几种同步方法 Mutex channel WaitGroup golang协程同步的几种方法 本文简要介绍下go中协程的几 ...

  2. Python与Golang协程异同

    背景知识 这里先给出一些常用的知识点简要说明,以便理解后面的文章内容. 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定 ...

  3. 面试必问:Golang高阶-Golang协程实现原理

    引言 实现并发编程有进程,线程,IO多路复用的方式.(并发和并行我们这里不区分,如果CPU是多核的,可能在多个核同时进行,我们叫并行,如果是单核,需要排队切换,我们叫并发) 进程和线程的区别 进程是计 ...

  4. Golang协程实现流量统计系统(3)

    进程.线程.协程 - 进程:太重 - 线程:上下文切换开销太大 - 协程:轻量级的线程,简洁的并发模式 Golang协程:goroutine Hello world package main impo ...

  5. golang协程池设计

    Why Pool go自从出生就身带“高并发”的标签,其并发编程就是由groutine实现的,因其消耗资源低,性能高效,开发成本低的特性而被广泛应用到各种场景,例如服务端开发中使用的HTTP服务,在g ...

  6. Golang 协程调度

    一.线程模型 N:1模型,N个用户空间线程在1个内核空间线程上运行.优势是上下文切换非常快但是无法利用多核系统的优点. 1:1模型,1个内核空间线程运行一个用户空间线程.这种充分利用了多核系统的优势但 ...

  7. golang 协程嵌套,会产生依赖关系(父子关系)么?

    编码时冒出一个问题:在一个协程内部,再创建一个或多个协程,是否会产生依赖关系? 做了一个小实验,这里随笔记录一下经过,备注后续深入研究. test代码: package main import ( & ...

  8. Golang协程与通道整理

    协程goroutine        不由OS调度,而是用户层自行释放CPU,从而在执行体之间切换.Go在底层进行协助实现      涉及系统调用的地方由Go标准库协助释放CPU      总之,不通 ...

  9. 关于协程:nodejs和golang协程的不同

    nodejs和golang都是支持协程的,从表现上来看,nodejs对于协程的支持在于async/await,golang对协程的支持在于goroutine.关于协程的话题,简单来说,可以看作是非抢占 ...

  10. golang协程——通道channel阻塞

    新的一年开始了,不管今天以前发生了什么,向前看,就够了. 说到channel,就一定要说一说线程了.任何实际项目,无论大小,并发是必然存在的.并发的存在,就涉及到线程通信.在当下的开发语言中,线程通讯 ...

随机推荐

  1. Java例题_26 请输入星期几的第一个字母来判断一下是星期几,如果第一个字母一样,则继续 判断第二个字母。

    1 /*26 [程序 26 求星期] 2 题目:请输入星期几的第一个字母来判断一下是星期几,如果第一个字母一样,则继续 判断第二个字母. 3 程序分析:用情况语句比较好,如果第一个字母一样,则判断用情 ...

  2. OLAP引擎:基于Druid组件进行数据统计分析

    一.Druid概述 1.Druid简介 Druid是一款基于分布式架构的OLAP引擎,支持数据写入.低延时.高性能的数据分析,具有优秀的数据聚合能力与实时查询能力.在大数据分析.实时计算.监控等领域都 ...

  3. Kubernetes 常用日志收集方案

    Kubernetes 常用日志收集方案 学习了 Kubernetes 集群中监控系统的搭建,除了对集群的监控报警之外,还有一项运维工作是非常重要的,那就是日志的收集. 介绍 应用程序和系统日志可以帮助 ...

  4. 从零搭建一个IdentityServer——会话管理与登出

    在上一篇文章中我们介绍了单页应用是如何使用IdentityServer完成身份验证的,并且在讲到静默登录以及会话监听的时候都提到会话(Session)这一概念,会话指的是用户与系统之间交互过程,反过来 ...

  5. mybatis的本质和原理

    背景 项目需要,我们需要自己做一套mybatis,或者使用大部分mybatis的原始内容.对其改造,以适应需要.这就要求我再次学习一下mybatis,对它有更深入的了解. 是什么    MyBatis ...

  6. python 闭包函数与装饰器

    1.什么是闭包函数 (1):什么是闭包函数: #内部函数包含对外部作用域而非全局作用域的引用, 简而言之, 闭包的特点就是内部函数引用了外部函数中的变量. 在Python中,支持将函数当做对象使用,也 ...

  7. ingress controller 和ingress使用实例

    ingress controller安装 k8s集群版本:1.15+ 官方文档: https://kubernetes.github.io/ingress-nginx/ 创建基础配置 kubectl ...

  8. Weekly Contest 184

    1408. String Matching in an Array Given an array of string words. Return all strings in words which ...

  9. Wordpress学习链接整理

    Wordpress学习链接整理 获取和使用 WordPress 中的全局变量 wordpress模板加载顺序汇总 WordPress载入页面时的模板加载机制(图) WordPress 条件判断标签及用 ...

  10. 【运维--系统】nacos介绍和安装

    目录: 简介 安装java 安装mysql 安装nacos 附录 简介 Nacos 致力于帮助您发现.配置和管理微服务.Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现.服务配置.服 ...