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

简介: 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大大的教程所说:

感受并发编程的乐趣

资源推荐:

目录
相关文章
|
6月前
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
208 33
|
5月前
|
存储 Go 开发者
Go 语言中如何处理并发错误
在 Go 语言中,并发编程中的错误处理尤为复杂。本文介绍了几种常见的并发错误处理方法,包括 panic 的作用范围、使用 channel 收集错误与结果,以及使用 errgroup 包统一管理错误和取消任务,帮助开发者编写更健壮的并发程序。
136 4
Go 语言中如何处理并发错误
|
3月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
3月前
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
5月前
|
数据采集 安全 Go
Go 语言并发编程基础:Goroutine 的创建与调度
Go 语言的 Goroutine 是轻量级线程,由 runtime 管理,具有启动快、占用小、支持高并发的特点。本章介绍 Goroutine 的基本概念、创建方式(如使用 `go` 关键字或匿名函数)、M:N 调度模型及其工作流程,并探讨其在高并发场景中的应用,帮助理解其高效并发的优势。
|
5月前
|
Go 开发者
Go 并发编程基础:无缓冲与有缓冲通道
本章深入探讨Go语言中通道(Channel)的两种类型:无缓冲通道与有缓冲通道。无缓冲通道要求发送和接收必须同步配对,适用于精确同步和信号通知;有缓冲通道通过内部队列实现异步通信,适合高吞吐量和生产者-消费者模型。文章通过示例对比两者的行为差异,并分析死锁风险及使用原则,帮助开发者根据场景选择合适的通道类型以实现高效并发编程。
|
7月前
|
数据采集 监控 Go
用 Go 实现一个轻量级并发任务调度器(支持限速)
本文介绍了如何用 Go 实现一个轻量级的并发任务调度器,解决日常开发中批量任务处理的需求。调度器支持最大并发数控制、速率限制、失败重试及结果收集等功能。通过示例代码展示了其使用方法,并分析了核心组件设计,包括任务(Task)和调度器(Scheduler)。该工具适用于网络爬虫、批量请求等场景。文章最后总结了 Go 并发模型的优势,并提出了扩展功能的方向,如失败回调、超时控制等,欢迎读者交流改进。
303 25
|
11月前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
9月前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
12月前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####