Go语言入门总结(八)-并发编程

关于编程语言中的进程、线程、调度

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程。
有些进程不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
同一个 CPU 在同一时间只能执行一个任务,这是物理上的限制。所以任务一般只会处于正在执行或者未执行(等待或者终止)的状态;这些而用于处理这些任务的 CPU 往往都是不可再分的。
在操作系统的进程调度器(Process Scheduler)中,待调度的任务就是线程,Go 语言的调度器与操作系统的调度器面对的是几乎相同的场景,其中的任务是 Goroutine,可以分配的资源是在 CPU 上运行的线程。

1.goroutine

在go语言里面实现并发很容易。
独立运行的任务被称为goroutine,goroutine不同于线程,它的运行表面上看似乎都是在同时运行,但是由于计算机通常只具有有限数量的处理单元,所以,从技术上来说,goroutine并不是真正的同时在运行。其实相当于是在一条时间轴上,go语言会分配时间,让他们轮流运行。至于运行顺序,那将是随机的。
启动goroutine就像调用函数一样简单,只需要在调用的前面加上go就可以了。

func main() {
    for i := 0;i <= 5;i++{
        go printNum(i)
    }
    time.Sleep(1 * time.Second)
}

func printNum(i int){
    fmt.Println(strconv.Itoa(i))
}

上面这段代码在每次运行时,你都可以看到不同的运行结果,这也证实了运行顺序的随机性。
但是一旦注释掉time.Sleep(1 * time.Second)这句代码,你就得不到任何输出。
因为golang的主函数(其实也是跑在一个goroutine中)并不会等待其他goroutine结束。如果主goroutine结束了,所有其他goroutine都将结束。

2.channel(通道)

和go语言的其他类型一样,你可以将通道用作变量、传递至函数、存储在结构中,或者其它你想做的事情。
通道是负责在多个goroutine之间进行通信的。
创建一个通道使用make函数创建 ,如 c := make(chan int),chan表示是通道类型,int表示这个通道的接受的值类型。
数据使用 <- 来进行传递,放在chan变量的左边表示把chan接收到的值传递给一个变量,放在右边表示将数据传递给chan。

c <- 99  //表示将99传递给chan变量c
v := <- c //表示将chan变量c里面的值赋值给变量v

当在执行发送操作,也就是正在执行将值传递给chan变量的操作时,执行该操作的goroutine任务会处于等待的过程中,无法执行其它操作。但是其它未等待的扔可以继续自由的运行。执行接收操作的也一样,接收的会等待接收到下一个数据以后才会继续执行。

func main() {
    c := make(chan int)
    for i := 0;i <= 5;i++{
        go printNum(i,c)//1.for循环先会创建6个goroutine任务,去执行,对于goroutine的执行是无序的。
    }
    time.Sleep(200) //4.增加sleep是为了让读者更直观的感受到chan等待时后续代码是不执行的。因为等待,所以第3步发送给chan的数据没有接收者。
    for i := 0;i <= 5;i++{
        fmt.Println("get " + strconv.Itoa(i))//5.等待结束,开始执行打印
        newi := <- c  //6.接收到了通道传来的值。但是接收到的6个数据肯定是按通道发送数据的顺序接收到的。但是下面一条语句执行的时机就不一定了
        fmt.Println("get end " + strconv.Itoa(newi))7.这个执行肯定是要在第6步执行完成以后,再根据系统分配,随机执行了。但是执行的肯定都是已经接收到值了的。
    }
    time.Sleep(1 * time.Second)
}

func printNum(i int,c chan int){
    fmt.Println("go " + strconv.Itoa(i)) //2.先执行到这里,创建出的6个goroutine会先执行这一句,即使下面一句代码的通道处于等待状态也不影响6个goroutine这句的执行。
    c <- i //3.当第一个goroutine执行到这里的时候发现没有通道接收值的时候,就会在这里等待。后面5个goroutine会在这里排队,后面5个排队执行的顺序也要看系统分配。
    fmt.Println("go end " + strconv.Itoa(i))7.这个执行肯定是要在第3步执行完成以后,再根据系统分配,随机执行了。但是执行的肯定都是已经发送过值了的。
}

上面这段代码读者运行一下就可以很清楚channel的等待机制。
具体分析可以看上面代码的注释部分。

对于chan的结束发送,我们也有方法能够处理。使用close()方法可以在channel发送完数据以后关掉它,当然,如果接受channel数据的方法是使用for循环,那么在main函数退出之前会一直接受到定义的chan类型的零值。这个时候可以使用range来进行处理。

func main() {
    var c = make(chan string)
    go func (c chan string){
        for n := range c{
            println(n)
        }
    }(c)
    sendChan(c)
}

func sendChan(c chan string) {
    c <- "hello"
    c <- "world"
    c <- "你好"
    close(c)
}

除了上面的方式以外,我们还可以使用sync.WaitGroup来进行等待处理。

func main() {
    var wg sync.WaitGroup
    var c = make(chan string)
    go func (c chan string,wg *sync.WaitGroup){
        for n := range c{
            println(n)
            wg.Done()
        }
    }(c,&wg)
    wg.Add(3)
    sendChan(c)
    wg.Wait()
}

func sendChan(c chan string) {
    c <- "hello"
    c <- "world"
    c <- "你好"
}

3.select处理通道

上面介绍channel的时候使用的都是int类型的chan,所以这其实是一种理想情况,真正的使用时,可能会存在不同类型的chan。这个时候我们就可以使用select来处理不同类型的chan了。

func main() {
    c := make(chan int)
    for i := 0;i <= 5;i++{
        go printNum(i,c)
    }
    timeOut := time.After(time.Second)
    for i := 0;i <= 5;i++{
        select {
        case newi := <- c:
            fmt.Println(strconv.Itoa(newi))
        case <- timeOut:
            fmt.Println("运行超时")
            return
        }

    }
}

func printNum(i int,c chan int){
    time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
    c <- i
}

4.阻塞和死锁

当goroutine在等待通道的发送或者接收操作的时候,我们就说它被阻塞了。当时在go语言中,除goroutine本身占用的少量内存之外,被阻塞的goroutine并不消耗任何资源。
goroutine会静静的停在那里,等待导致它阻塞的事情发生,然后解除阻塞。
当一个或者多个goroutine因为某些永远无法发生的事情而被阻塞时,我们称之为死锁。

5.互斥锁

在go语言中,当有2个或者多个goroutine同时使用一个共享值的时候,程序可能会出错。两个goroutine同时读取相同的事物并不会产生问题,如果一个goroutine在写入事物的同时,另一个goroutine尝试写入或者读取相同的事物,那么后者的行为将是未定义的。
所以为了解决这种问题,我们需要使用到互斥锁。goroutine可以通过互斥锁阻止其他goroutine在同一时间进行某些事情。
互斥锁具有Lock和Unlock两个方法。如果持有互斥锁的goroutine因为某些原因而尝试锁定同一个互斥锁,那么就会引发死锁。

func main() {
    p := people{}
    p.saveSameName("张三")
}

type people struct {
    mu sync.Mutex
    num int32
}

func (p *people)saveSameName(name string)int32{
    p.mu.Lock()
    defer p.mu.Unlock()
    p.num++
    return p.num
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342