美文网首页Go语言程序员Go
go| 感受并发编程的乐趣 前篇

go| 感受并发编程的乐趣 前篇

作者: daydaygo | 来源:发表于2018-02-22 22:02 被阅读728次

date: 2018-2-15 23:55:45
title: go| 感受并发编程的乐趣 前篇

学习了 ccmouse - googl工程师慕课网 - 搭建并行处理管道,感受GO语言魅力, 获益匪浅, 也想把这份编程的快乐传递给大家.

强烈推荐一下ccmouse大大的课程, 总能让我生出 Google工程师果然就是不一样 之感, 每次都能从简单的 hello world 开始, 一步步 coding 到教程的主题, 并在过程中给予充分的理由 -- 为什么要一步步变复杂. 同时也会亲身 踩坑 示范, 干货满满.

内容提要:

  • let's go: 为什么要使用 go ? swoole2.1 带来的 go + channel 编程体验
  • go's design: go语言的设计
  • go's hello-world: go 写 hello world 的各种姿势, 为之后并发编程解决的 big problem 埋下伏笔
  • go's sort: 内排排序 -> 外部排序
  • go's io: 二进制文件读写 + 大文件读写加速

另外, ccmouse大大关于语言学习的方法也值得借鉴:

  • 首先, 学习一下语言语法的要点
  • 立刻找一个不那么简单的项目来做, 边做边查文档/stackoverflow

let's go

swoole2.1 发布了(Swoole 2.1 正式版发布,协程+通道带来全新的 PHP 编程模式), 看着示例代码颇有点 陌生 之感, 看来真想要 写好 协程, 熟悉 go 应当是必选项了.

示例代码, go & channel:

// coroutine
go(function () {
    co::sleep(0.5);
    echo "hello";
});
go("test");
go([$object, "method"]);

// go + channel
$c3 = new chan(2);
$c4 = new chan(2);
$c3->push(3);
$c3->push(3.1415);
$c4->push(3);
$c4->push(3.1415);
go(function () use ($c3, $c4) {
    echo "producer\n";
    co::sleep(1);
    $data = $c3->pop();
    echo "pop[1]\n";
    var_dump($data);
});

go 看起来就是一个接收 闭包函数 作为入参的函数, channel(通道)看起来也不过是一个类似 的数据结构(push()pop() 2 种操作). 看到示例代码却感到十分 陌生 -- 为什么要这样写呀?

既然是 借鉴至 go 语言, 看来有必要了解一下 go, 来加深一下理解了

go's design

Google内部的「标准」编程语言:

  • c++: 性能保障部分, 如搜索引擎
  • java: 复杂业务逻辑, 如 adwords, Google docs
  • Python: 大量内部工具
  • go: 新的内部工具, 及其他业务模块

语言对比:

  • c/c++: 性能, 可以做系统开发; 没有繁琐的类型系统, 简单统一化的模块依赖管理, 编译速度飞快
  • java: 垃圾回收 -> 慢, 会影响业务
  • Python: 简单易学, 灵活类型, 函数式编程, 异步IO; 没有编译器静态类型检查

go 设计:

  • 类型检查: 编译时
  • 运行环境: 编译成机器码直接运行(不依赖虚拟机)
  • 编程范式: 面向接口, 函数式编程, 并发编程

go 并发编程:

  • CSP, Communication Sequential Process 模型
  • 不需要锁, 不需要 callback
  • 并行计算 + 分布式

go 线程实现模型 MPG:

  • M: Machine, 一个内核线程
  • P: Processor, M所需的上下文环境
  • G: Goroutine, 代表一段需要被并发执行的 go 语言代码的封装
  • KSE: 内核调度实体
  • 一个 M 和一个 P 关联, 形成一个有效的 G 运行环境, 每个 P 都会包含一个可运行的 G 的队列(runq)
go 线程实现模型 MPG

进程/线程开多了都会涉及到系统进行调度导致的消耗, go 通过 MPG 模型进行映射, 可以并行很多 go协程, 底层自动实现调度

go's hello world

4 个版本的 hello world:

  • 原始版: 直接 print 输出
  • http版: 使用 http 库输出到 web
  • go版: 开始接触 go协程
  • go+channel版: 开始接触 go协程 + channel数据传递
package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    // 原始版
    helloWorld1()

    // http版
    helloWorld2()

    // go协程版
    for i := 0; i < 5000; i++ { // 协程 <-> 线程 之间存在隐射, 具体参考 <go并发编程> 一书中的「线程实现模型」
        go helloWorld3(i)
    }
    time.Sleep(time.Microsecond) // 添加延时, 协程才有机会在 main() 退出前执行

    // go + channel
    ch := make(chan string)
    for i := 0; i < 5; i++ {
        go helloWorld4(i, ch)
    }
    for {
        msg := <-ch
        fmt.Println(msg)
    }
}

func helloWorld1() {
    fmt.Println("hello world")
}

func helloWorld2() {
    // 为什么要使用指针: 因为参数可以被改变
    http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        //fmt.Fprintln(writer, "<h1>hello world</h1>")
        fmt.Fprintf(writer, "<h1>hello world %v</h1>", request.FormValue("name"))
    })
    http.ListenAndServe(":8888", nil)
}

func helloWorld3(i int) {
    fmt.Printf("hello world from goroutine %v \n", i)
}

func helloWorld4(i int, ch chan string) {
    for {
        ch <- fmt.Sprintf("hello world %v", i)
    }
}

go's sort

先科普一下排序的知识:

  • 排序分 内部排序 + 外部排序 两种, 区分在于数据量, 内部排序可以将数据全部放到内存中, 然后进行排序
  • 常见的内部排序算法: 冒泡, 快排, 归并排序等, 其中 快排 是速度最快的不稳定排序算法, 归并排序可以应用于外部排序
  • 归并排序时, 可以一次归并多节点达到加速的效果, 例子中使用 二路归并 来简单演示

再来看看 go 实现的归并排序:

package main

import (
    "sort"
    "fmt"
)

func main() {
    // 内部排序 -> 快排
    a := []int{3, 6, 2, 1, 9, 10, 8}
    sort.Ints(a)
    fmt.Println(a)

    // 外部排序 -> 归并排序
    c := Merge(inMemSort(arraySource(3, 6, 2, 1, 9, 10, 8)),
        inMemSort(arraySource(7, 4, 0, 3, 2, 8, 13)))
    // channel 中获取数据: 原始
    //for {
    //  // 风格一
    //  //if v, ok := <-c; ok {
    //  //  fmt.Println(v)
    //  //} else {
    //  //  break
    //  //}
    //  // 风格二
    //  v, ok := <-c
    //  if !ok {
    //      break
    //  }
    //  fmt.Println(v)
    //}
    // channel 中获取数据: 简写
    for v := range c {
        fmt.Println(v)
    }

}

func arraySource(a ...int) <-chan int { // 指定从 channel 获取数据
    out := make(chan int)
    go func() {
        for _, v := range a { // for + range + _
            out <- v
        }
        close(out) // 数据传递完毕, 关闭channel
    }()
    return out
}

func inMemSort(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        // read into memory
        a := []int{}
        for v := range in {
            a = append(a, v)
        }
        // sort
        sort.Ints(a)
        // output
        for _, v := range a {
            out <- v
        }
        close(out)
    }()
    return out
}

func Merge(in1, in2 <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        // 归并的过程要处理某个通道可能没有数据的情况, 代码非常值得一读
        v1, ok1 := <-in1
        v2, ok2 := <-in2
        for ok1 || ok2 {
            if !ok2 || (ok1 && v1 <= v2) {
                out <- v1
                v1, ok1 = <-in1
            } else {
                out <- v2
                v2, ok2 = <-in2
            }
        }
        close(out)
    }()
    return out
}

go's io

文件读写算是一个 常见且简单 的任务, 但是:

  • 如何读写二进制文件呢? int 大小是多少? 大端序小端序?
  • 如何读写大文件, 比如 800m ? 什么是 buffer ? 什么是 flush ?

这一节的代码就用来处理这些问题:

package main

import (
    "encoding/binary"
    "fmt"
    "io"
    "os"
    "math/rand"
    "bufio"
)

func main() {
    TestIo()
    largeIo()
}

// 文件读写测试
func TestIo() {
    // 写
    file, err := os.Create("small.in") // 二进制数据
    if err != nil {
        panic(err) // 遇到错误, 暂时不处理
    }
    defer file.Close()    // 运行结束后, 自动关闭文件
    p := randomSource(50) // small.in 的大小 = 8*50
    writerSink(file, p)
    // 读
    file, err = os.Open("small.in")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    p = readerSource(file)
    for v := range p {
        fmt.Println(v)
    }
}

// 大文件读写
func largeIo() {
    const filename = "large.in"
    const n = 100000000 // 8*100*1000*100 = 800M
    file, err := os.Create(filename)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    p := randomSource(n)
    writer := bufio.NewWriter(file) // io buffer 加快文件读写
    writerSink(writer, p)
    writer.Flush() // flush 掉 io buffer 中的数据
    // 读
    file, err = os.Open(filename)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    // 写, 只测试前 100 个
    p = readerSource(bufio.NewReader(file))
    count := 0
    for v := range p {
        fmt.Println(v)
        count++
        if count >= 100 {
            break
        }
    }
}

func readerSource(reader io.Reader) <-chan int {
    out := make(chan int)
    go func() {
        buffer := make([]byte, 8) // int: 64bit -> 8byte
        for {
            n, err := reader.Read(buffer)
            if n > 0 { // 可能数据不足 8byte
                v := int(binary.BigEndian.Uint64(buffer)) // Uint64 -> int
                out <- v
            }
            if err != nil {
                break
            }
        }
        close(out)
    }()
    return out
}

func writerSink(writer io.Writer, in <-chan int) {
    for v := range in {
        buffer := make([]byte, 8)
        binary.BigEndian.PutUint64(buffer, uint64(v))
        writer.Write(buffer)
    }
}

func randomSource(count int) <-chan int {
    out := make(chan int)
    go func() {
        for i := 0; i < count; i++ {
            out <- rand.Int()
        }
        close(out)
    }()
    return out
}

写在最后

go 的「强制」在编程方面感觉优点大于缺点:

  • 强制代码风格: 读/写代码都轻松了不少
  • 强制类型检查: 出错时的错误提示非常友好

书写过程中, 基本根据编译器提示, 就可以把大部分 bug 清理掉.

再次推荐一下 go, 给想要写 并发编程 的程序汪, 就如 ccmouse大大的教程所说:

感受并发编程的乐趣

资源推荐:

相关文章

  • go| 感受并发编程的乐趣 前篇

    date: 2018-2-15 23:55:45title: go| 感受并发编程的乐趣 前篇 学习了 ccmou...

  • go| 感受并发编程的乐趣 后篇

    date: 2018-2-22 22:12:29title: go| 感受并发编程的乐趣 后篇 这篇 blog 紧...

  • Go基础语法(九)

    Go语言并发 Go 是并发式语言,而不是并行式语言。 并发是指立即处理多个任务的能力。 Go 编程语言原生支持并发...

  • Go并发

    Go语言中的并发编程 并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很...

  • 瞅一眼就会使用GO的并发编程分享

    [TOC] GO的并发编程分享 之前我们分享了网络编程,今天我们来看看GO的并发编程分享,我们先来看看他是个啥 啥...

  • day08-go.GPM

    当别人到go为什么支持高并发,或者问为什么go本身对并发编程友好?以及go与Java对比的并发对比 正确回答: 在...

  • Go语言简介

    Go语言简介 Go语言设计的初衷 针对其他语言的痛点进行设计并加入并发编程为大数据,微服务,并发而生的通用编程语言...

  • Go语言学习笔记 - 并发

    Goroutine Go在语言层面对并发编程提供支持,采用轻量级线程(协程)实现。只需要在函数调用语句前添加go关...

  • Go 的并发性与调度器

    本篇文章是我对 Go 语言并发性的理解总结,适合初步了解并发,对 Go 语言的并发编程与调度器原理有兴趣的读者。 ...

  • Go网络编程之并发聊天室

    并发聊天室 并发编程和网络编程是现今行业开发中常用的技术。Go语言强大的语法设定使得并发和网络编程都变的简洁而高效...

网友评论

  • 智障程序猿:看了这个课,两路归并的时候最后返回一个关闭的channel,感觉不怎么理解,其实如果是同步的话 ,返回slice也可以啊。
    daydaygo:@智障程序猿 之后有一篇理解协程基础的文章, 虽然使用语言是 php, 但是对理解协程很有帮助, 你可以看看
    智障程序猿:我一开始看这个写法误解成同步了,因为是在方法内部构造的channel,如果是外部构造channel,然后传给生产者的方法,另一边消费,更好容易理解成异步。:smile:
    daydaygo:@智障程序猿 没有跑大数据源的例子么? 800G 用同步 slice 就爆内存了.
  • daydaygo:感受并发编程的乐趣

本文标题:go| 感受并发编程的乐趣 前篇

本文链接:https://www.haomeiwen.com/subject/ickztftx.html