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

感受并发编程的乐趣

资源推荐:

目录
相关文章
|
12天前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
17天前
|
存储 Go 开发者
Go语言中的并发编程与通道(Channel)的深度探索
本文旨在深入探讨Go语言中并发编程的核心概念和实践,特别是通道(Channel)的使用。通过分析Goroutines和Channels的基本工作原理,我们将了解如何在Go语言中高效地实现并行任务处理。本文不仅介绍了基础语法和用法,还深入讨论了高级特性如缓冲通道、选择性接收以及超时控制等,旨在为读者提供一个全面的并发编程视角。
|
13天前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####
|
15天前
|
Go 调度 开发者
Go语言中的并发编程:深入理解goroutines和channels####
本文旨在探讨Go语言中并发编程的核心概念——goroutines和channels。通过分析它们的工作原理、使用场景以及最佳实践,帮助开发者更好地理解和运用这两种强大的工具来构建高效、可扩展的应用程序。文章还将涵盖一些常见的陷阱和解决方案,以确保在实际应用中能够避免潜在的问题。 ####
|
17天前
|
安全 Go 数据处理
Go语言中的并发编程:掌握goroutine和channel的艺术####
本文深入探讨了Go语言在并发编程领域的核心概念——goroutine与channel。不同于传统的单线程执行模式,Go通过轻量级的goroutine实现了高效的并发处理,而channel作为goroutines之间通信的桥梁,确保了数据传递的安全性与高效性。文章首先简述了goroutine的基本特性及其创建方法,随后详细解析了channel的类型、操作以及它们如何协同工作以构建健壮的并发应用。此外,还介绍了select语句在多路复用中的应用,以及如何利用WaitGroup等待一组goroutine完成。最后,通过一个实际案例展示了如何在Go中设计并实现一个简单的并发程序,旨在帮助读者理解并掌
|
17天前
|
安全 Java Go
Go语言中的并发编程:掌握goroutine与通道的艺术####
本文深入探讨了Go语言中的核心特性——并发编程,通过实例解析goroutine和通道的高效使用技巧,旨在帮助开发者提升多线程程序的性能与可靠性。 ####
|
18天前
|
Go 开发者
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念,重点介绍了goroutines和channels的工作原理及其在实际开发中的应用。文章通过实例演示如何有效地利用这些工具来编写高效、可维护的并发程序,旨在帮助读者理解并掌握Go语言在处理并发任务时的强大能力。 ####
|
16天前
|
算法 安全 程序员
Go语言的并发编程:深入理解与实践####
本文旨在探讨Go语言在并发编程方面的独特优势及其实现机制,通过实例解析关键概念如goroutine和channel,帮助开发者更高效地利用Go进行高性能软件开发。不同于传统的摘要概述,本文将以一个简短的故事开头,引出并发编程的重要性,随后详细阐述Go语言如何简化复杂并发任务的处理,最后通过实际案例展示其强大功能。 --- ###
|
20天前
|
存储 安全 Go
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制。本文介绍了这两者的概念、用法及如何结合使用,实现任务的高效并发执行与数据的安全传递,强调了并发编程中的注意事项,旨在帮助开发者更好地掌握 Go 语言的并发编程技巧。
29 2
|
21天前
|
存储 负载均衡 监控
如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
在数字化时代,构建高可靠性服务架构至关重要。本文探讨了如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
29 1