Golang基础第四篇——从go并发到channel到定时器实现

目录

一,Golang的并发编程

二,关于goroutine

三,关于channel

四,Golang中的定时器


一,Golang的并发编程

在第一篇我们已经提到,golang最重要的一个特色就是他通过go关键字的并发处理。

首先要知道为什么我们需要并发,并发为什么在如今作为一种语言特性如此重要?

关于进程线程,堆栈的基础知识就不赘述(感兴趣可以翻我之前的blog),这里从市场发展需求方面说:1. 一方面, 由于应用程序的迅速增加刺激了用户对于网络产品的依赖,我们既要处理灵敏相应的图形用户界面,又要执行IO和运算,传统串行程序肯定会导致IO阻塞,用户使用体验变差,最后被市场淘汰。 2. 另一方面,事务越来越分配到分布式的环境上,这导致相同的工作单元在不同计算机上处理分片数据,必然要通过线程的切换达到分布式的高性能运转,此时并发成为刚需。


关于并发的实现,

从操作系统的实现模型上发展由多进程->多线程->基于回调的非阻塞/异步和协程机制

关于异步框架已经经受过市场的检验,可以减小消耗,不过编程难度要比多线程大;

协程是用户级线程,开销极小,编程简单结构清晰,不过需要语言的支持(支持的语言很少,比如lua,C#)

golang正是利用在语言级别实现轻量级线程(goroutine),避开java繁琐的多线程框架,成为国内多家公司的当红语言,可以想像简单的多线程处理方式会对服务器方的编写节省多少资源。

二,关于goroutine

我这里讲goroutine就是golang支持的协程

协程的实现:协程是基于线程的。内部实现上,维护了一组数据结构和 n 个线程,真正的执行还是线程,协程执行的代码被扔进一个待执行队列中,由这 n 个线程从队列中拉出来执行。golang 利用并封装了操作系统的异步函数。包括 linux 的 epoll、select 和 windows 的 iocp、event 等,当这些异步函数返回 busy 或 blocking 时,golang 利用这个时机将现有的执行序列压栈,让线程去拉另外一个协程的代码来执行。简单来说,一个线程可以利用队列拥有多个串行执行的协程

协程的特性:在任务调度上,协程是弱于线程的。但是在资源消耗上,协程则是极低的。一个线程的内存在 MB 级别,而协程只需要 KB 级别。而且线程的调度需要内核态与用户的频繁切入切出,资源消耗也不小。这就导致线程和进程最多可以创建不到1万个,而协程可以轻松创建几百万个也不会令系统资源枯竭。

当我们想调用goroutine的时候,使用go关键字就可以,在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束。


然而并发的难度在于协调,而不是调用就行,我们要使用goroutine就需要有两种最常见的并发通信模型:共享内存 和 消息。

共享数据是指多个并发单元分别保持对同一个数据的引用,实现数据的共享,在实际工程中最常见的就是共享内存了,通过频繁的加锁释放 达到资源的有效调用。在这里golang提供了另一种通信模型解决这个问题,即以消息机制而非共享内存作为通信方式。

消息机制即每个并发单元是自包含的独立的个体,并且都有自己的变量,变量互相不共享,每个单元相互独立通过消息(输入输出)来达成一致,实现消息共享。golang提供的消息机制就是channel

三,关于channel

学会channel,你就搞懂了golang的并发编程。

channel可以理解成类型安全的unix管道,具体实现见$GOROOT/src/runtime/chan.go,或者转http://litang.me/post/golang-channel/ 由读队列,写队列,缓存队列构成

这里讲简单实用 给下一节做铺垫,还是拿例子做实战,充分调用你之前学过语言的支持,结合编译原理做理解:

//channel.go
package main

import "fmt"

func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum // 把 sum 发送到通道 c
}

func main() {
        s := []int{7, 2, 8, -9, 4, 0}

        c := make(chan int)
        go sum(s[:len(s)/2], c)
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c // 从通道 c 中接收

        fmt.Println(x, y, x+y)
}

其中channel可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

main函数是起点,s声明了一个包含6个整数的数组,c是一个channel,分别通过go执行goroutine读前半数组的sum和后半数组的sum,然后把在函数里读到的数据写到x和y上,打印结果为-5,17,12,在x和y赋值时,主进程会因为等待两个goroutine的返回结果而等待,我们使用channel在这里也避免了加锁的问题。

另外一个例子:

//channel2.main
package main

import "fmt"

func Count(ch chan int) {
    ch <- 1
    fmt.Println("Counting")
}

func main() {
    chs := make([]chan int, 6)
    for i := 0; i < 6; i++ {
        chs[i] = make(chan int)
        go Count(chs[i])
    }

    for _, ch := range(chs) {
        <- ch
    }
}

这里除了channel数组的使用,请大家注意在main函数中channel写出数据是没有接应的,也就是说,在循环中的channel们会在被读前加锁,但一旦读取主进程会立即执行,所以上边的函数调用结果会输出5个Counting,而不是6个。

四,Golang中的定时器

golang在src包中的timer有定义定时器的结构(或者ticks)可以根据timer.go去直接拿去使用,下面节选讲解一下结构:

timer的结构里一般有interval(时间间隔),一把互斥锁mu,一个判断是否运行的bool变量,和我们自定义的消息管道

type Timer struct {
	interval sync2.AtomicDuration

	// state management
	mu      sync.Mutex
	running bool

	// msg is used for out-of-band messages
	msg chan typeAction
}

start函数执行时,首先加锁,然后判断timer的运行状态,如果还没启动timer,就把状态设置为true,然后通过goroutine执行keephouse函数实体,整个函数执行完去掉资源的互斥锁

// Start starts the timer.
func (tm *Timer) Start(keephouse func()) {
	tm.mu.Lock()
	defer tm.mu.Unlock()
	if tm.running {
		return
	}
	tm.running = true
	go tm.run(keephouse)
}

上边start函数的管道去执行下边的run函数:goroutine不断循环写进管道时间信息并和interval做判断,当channel被写进消息执行判断action或者成为nil输出时,带着执行keephouse跳出循环(这里golang的select函数需要注意)

func (tm *Timer) run(keephouse func()) {
	for {
		var ch <-chan time.Time
		interval := tm.interval.Get()
		if interval <= 0 {
			ch = nil
		} else {
			ch = time.After(interval)
		}
		select {
		case action := <-tm.msg:
			switch action {
			case timerStop:
				return
			case timerReset:
				continue
			}
		case <-ch:
		}
		keephouse()
	}
}

触发函数将触发消息直接写入管道中,上边的run函数我们知道,当管道收到消息时会立即执行keephouse,所以trigger函数会先执行然后重置(除了写进channel timerstop消息会返回,否则run循环会一直持续下去)

// Trigger will cause the timer to immediately execute the keephouse function.
// It will then cause the timer to restart the wait.
func (tm *Timer) Trigger() {
	tm.mu.Lock()
	defer tm.mu.Unlock()
	if tm.running {
		tm.msg <- timerTrigger
	}
}

// TriggerAfter waits for the specified duration and triggers the next event.
func (tm *Timer) TriggerAfter(duration time.Duration) {
	go func() {
		time.Sleep(duration)
		tm.Trigger()
	}()
}

stop函数就很简单了,向channel里写进timerstop消息,然后更新running状态,便于timer可以重新开启

// Stop will stop the timer. It guarantees that the timer will not execute
// any more calls to keephouse once it has returned.
func (tm *Timer) Stop() {
	tm.mu.Lock()
	defer tm.mu.Unlock()
	if tm.running {
		tm.msg <- timerStop
		tm.running = false
	}
}

了解这个之后我们就可以使用timer,下面这个是使用golang原生ticker的demo可以对比实现一下:每5秒报一次数


import (
    "fmt"
    "time"
)
 
func main() {
    ticker := time.NewTicker(time.Second * 5)
    i := 0
    for {
        <-ticker.C
        i++
        fmt.Println("i=", i)
 
        if i == 5 {
            ticker.Stop()
            break
        }
    }
}

参考资料:go语言编程-许式伟

猜你喜欢

转载自blog.csdn.net/wannuoge4766/article/details/104579079