Golang 并发编程

常见并发模型
  • 进程&线程 ( Apache )
    进程是一种系统运行行动, 就是代表计算机做某个事情的一系列行动的总和, 它是程序的执行实体, 一般情况下是一个程序一个进程, 多进程情况是一个程序多个进程
    线程是运算调度的最小单元, 它的作用就是运算, 由系统内核控制大小, 同时运行多个任务, 进程可以包含多个线程, 因此在同一个进程中, 可以执行多个线程, 每个线程负责一个动作
    系统最初的Web服务器都是基于进程&线程模型, 就是每新到一个请求就会分配一个进程或者线程, 每个进程只服务一个用户, 互联网初期用户的访问不多, 网站可以正常工作, 但是进程很昂贵, 一台服务器无法创建很多的进程, 随着互联网的发展, 网站越来越复杂, 一个页面可能就上百个请求, 操作系统就无法承受了
  • 异步非阻塞 ( Nginx, Libevent, NodeJS )
    一台服务器可以服务大量的用户, 并且资源消耗还很低, 但是其为了追求性能, 强行将线型的程序打乱, 开发和维护就变得很复杂, 调试也很困难
  • 协程 ( Golang, Erlang, Lua)
    协程 ( Coroutine ) 是轻量级的线程, 由用户来控制, 协程的内存消耗更小 ( 一个线程可以包含多个协程, 线程大约8MB的内存申请量, 协程大约2KB的内存申请量 ), 协程尚希文切换更快 ( 协程少一道手续, 线程申请内存需要走过内核, 协程申请内存不需要走过内核 )
    == Goroutine 也是一种协程, 它是在协程的基础上做了进一步优化, 并加入了Go语言的一些特性, 去掉了冗余的协程生命周期管理 ( 协程创建, 协程完成, 协程重用 ), 降低额外的延迟和开销 ( 由于协程间频繁交互导致的 ) 使得 Goroutine 更加高效也降低对系统的负担, 降低加锁/解锁的频率 ( 降低一部分额外的开销 )==
    降低了开发复杂度, 像写线型程序那样来书写全异步的程序, 其实协程底部就是线程, 但它比线程更轻量, 几十个协程体现在底层可能也就五六个线程, 协程可以理解为更高效更易用更轻量的线程
Golang 并发实现

多协程是指一段时间内协程的并行, 即某个任务使用多个协程同时进行处理, 多协程的必要条件: 协程任务之间有关联性 ( 相互之间组成整体任务, 相互之间有连续性 ), 有总分总的协调步骤过程
多协程任务步骤: 任务切分/分配, 启动多个协程, 合并多个协程的结果
多协程使用场景: 运算量比较多的流程上, 协程间依赖性比较弱
多协程的局限: 会增加额外的耗时, 会增加额外的内存消耗
协程等待: sync.WaitGroup
多协程生命周期: 协程的创建等全部生命历程的管理, 作用是为了便于协程的回收利用, 生命周期分类: 协程创建 ( 通过函数前加go关键字 ), 协程回收 ( Go语言的回收机制 ), 协程中断 ( Context 实现中断 )

package main

import (
	"context"
	"fmt"
	"sync"
)

// 协程等待
var wg sync.WaitGroup

// 主函数 主 Goroutine
func main() {

	// 初始化一个 context
	parent := context.Background()
	// 生成一个取消的 context
	ctx, cancel := context.WithCancel(parent)

	runTimes := 0
	wg.Add(1)
	// 启动 goroutine
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("Goroutine Done")
				return
			default:
				fmt.Println("Goroutine Running Times: ", runTimes)
				runTimes++
			}
			if runTimes >= 5 {
				cancel()
				wg.Done()
			}
		}

	}(ctx)

	wg.Wait()
}

  • 程序并发执行 ( goroutine ), 通过 goroutine 来保证程序的并发执行
foo() // 执行函数 foo, 程序等待函数 foo 返回

go foo() // 新建新的 goroutine 去执行函数 foo
bar() // 不用等待函数 foo 返回, 两个函数并发执行
  • 通过管道 channel 来实现多个 goroutine 间的数据同步和通信
    Channel 用于多个协程之间的通信
    Channel的妙用: 传递方面 ( 消息传递, 任务发送, 事件广播 ), 控制方面 ( 资源争抢, 并发控制, 流程开关 )
    Channel 关闭后, 可读不可写
    channel 在使用之前, 必须进行make初始化, 否则它是一个nil
    无缓冲区Channel, 使用时要同时具备输入和输出
    资源争抢应用场景: 电商的秒杀活动, 出行的司机抢单, 互金的股票抢购, 系统的计算资源争抢
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

// 协程等待
var wg sync.WaitGroup

// 主函数 主 Goroutine
func main() {

	// 资源争抢, 100个人抢10个鸡蛋
	eggs := make(chan int, 10) // 设定Channel缓冲区, 根据业务场景需求, 设置适合大小

	// 超时 Context
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

	// 鸡蛋
	for i := 0; i < 10; i++ {
		eggs <- i
	}

	// close(eggs) 关闭后只读不可写, 用for range 可循环获取到通道内的值, 但是用for循环读取时, 即使通道已没有值, 也能获取到对应类型的零值, 一般 close 和 for range 搭配使用

	// 100个人并行抢
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(n int, c context.Context) {
			// 多管道select调度
			select {
			case egg := <-eggs:
				fmt.Printf("people: %d get egg: %d\n", n, egg)
			case <-c.Done():
				fmt.Println("timeout")
				cancel()
				return
			default:
			}

			wg.Done()

		}(i, ctx)
	}

	wg.Wait()
}


// 初始化
c := make(chan string) // 创建一个通道 channel, 无缓冲区, 使用时要同时具备输入和输出, 即该管道要同时可写入和读取
go func (){
	time.Sleep(1 * time.Second)
	// 输入
	c <- "message" // 发送消息到 channel
	// 关闭
	close(c)
}()
// 输出
msg := <- c // 阻塞直到接收到数据
  • 通过 select 在多个 channel 中选择数据读取或者写入
c1 := make(chan string, 1)
	c2 := make(chan string, 1)

	select {
	// c1 和 c2 都有值, 则随机其中之一执行
	case v := <-c1: // c1 管道有值则执行该条case
		fmt.Println("channel1 sends", v)
	case v := <-c2: // c2 管道有值则执行该条case
		fmt.Println("channel2 sends", v)
	default: // 可选
		fmt.Println("neither channel was ready")

	}
并发和并行
  • 并发
    指同一时刻, 系统通过调度, 来回切换交替的运行多个任务, "看起来"是同时进行
    并发有一个好处, 假如任务A由于一些IO或者Sleep操作阻塞了, CPU就立即进行切换, 去执行任务B, 这样就可以充分利用CPU的资源, 提升整个程序的执行效率
  • 并行
    指同一时刻, 两个任务"真正的"同时进行
    Golang 并发编程_第1张图片
    在Golang中, 通过在函数前加关键字go就可以轻松开启一个goroutine去实现并发, 不用去关心多个goroutine同时是并行还是并发的执行, 即多个goroutine执行到底是一个CPU核心通过不断的切换时间片去调度并发的执行, 还是将多个goroutine去分散到多个CPU上并行执行, 具体的底层细节就交给Golang去解决
    在 Golang 中, 通常将一个复杂的任务拆分成一个个相互关系不大的小任务, 通过 goroutine 去并发执行, 然后再通过 channel 做数据通信, 这样就可以发挥出 Golang 的并发优势, 提升整个任务的执行效率
Golang 中的面向对象

在 Golang 中没有类和对象的概念, 但是可以通过结构体 struct 和接口 interface 来实现面向对象的三大特性( 封装, 继承, 多态 ), 进而实现面向对象编程

  • 封装
// Foo 定义结构体, 相当于类
type Foo struct {
	// 成员属性
	baz string
}

// 接收者, 方法
func (f *Foo) echo() {
	fmt.Println(f.baz)
}

func main() {
	// 初始化结构体, 相当于实例化类
	f := Foo{baz: "hello"}
	// 调用方法
	f.echo()
}
  • 继承
// Foo 定义结构体, 相当于类
type Foo struct {
	// 成员属性
	baz string
}

// Bar 继承
type Bar struct {
	// 拥有Foo所有的属性和方法
	Foo
}

// 接收者, 方法
func (f *Foo) echo() {
	fmt.Println(f.baz)
}

func main() {
	// 初始化结构体, 相当于实例化类
	b := Bar{Foo{baz: "chao"}}
	// 调用父类的方法
	b.echo()
}
  • 多态
// Foo 定义接口, 需实现的方法
type Foo interface {
	// 方法
	qux()
}

// Bar 结构体
type Bar struct{}

// Baz 结构体
type Baz struct{}

// 实现接口, 只要实现了接口的所有方法, 即实现了该接口
func (b *Baz) qux() {
	fmt.Println("baz")
}

func (b *Bar) qux() {
	fmt.Println("bar")
}

func main() {
	// 定义一个接口类型变量
	var f Foo
	// 实现了该接口, 即属于该接口类型的变量, 可以直接存到接口变量中
	f = &Bar{}
	f.qux()

	f = &Baz{}
	f.qux()
}

你可能感兴趣的:(GoLand,Go,golang,开发语言,后端)