【Go by Example】GO语言入门 32-39 中章

32. Timer

timer提供了一个定时器, 是一个管道, 在定时器时间到了之后会收到一个消息, 然后就得知是时间到了;
demo如下:

timer1 := time.NewTimer(2 * time.Second)
<-timer1.C
fmt.Println("Timer 1 fired")

这样能看到2s之后timer1被触发了;

它和直接sleep的最大区别在于, timer可以被中途停止;

timer2 := time.NewTimer(time.Second)
go func() {
    <-timer2.C
    fmt.Println("Timer 2 fired")
}()
stop2 := timer2.Stop()
if stop2 {
    fmt.Println("Timer 2 stopped")
}

time.Sleep(2 * time.Second)

上面这段代码, 1s之后触发timer2的任务被放到协程中去执行了, 但是实际上主线程顺序执行的时候就会把timer2给stop掉, 所以实际上即使再等待2s依然看不到timer2被触发(因为它已经被关闭了);

这里我在想, 协程里的任务会不会一直在等timer2.c, 导致占用资源呢

33. Ticker

打点器, 用来做固定间隔做某个任务用, demo:

func main() {
    ticker := time.NewTicker(500 * time.Millisecond)
    done := make(chan bool)

    go func() {
        for {
            select {
            case <-done:
                return
            case t := <-ticker.C:
                fmt.Println("Tick at", t)
            }
        }
    }()

    time.Sleep(1600 * time.Millisecond)
    ticker.Stop()
    done <- true
    fmt.Println("Ticker stopped")
}

主要思路就是, 起一个ticker, 应该会维护一个通道, 每500ms向通道里发一个消息;
然后起一个协程, 在协程里面死循环用select去接收ticker通道里的消息, 如果接收到了消息就打出来;

然后主线程里面等1600ms之后, 把ticker停掉, 然后给done发一个消息去把协程里的死循环停掉;

这里好像不要done也行, 如果select去看ticker.C里面没有消息就会报Ticker stopped然后停掉, 但是这样做, 协程里应该会持续运行, 即使ticker被stop了也会继续跑;
就是这么玩儿:

ticker := time.NewTicker(500 * time.Millisecond)

go func() {
	for {
		fmt.Println("Tick at", <-ticker.C)
	}
}()

time.Sleep(1600 * time.Millisecond)
ticker.Stop()
fmt.Println("Ticker stopped")

34. 工作池

这个工作池感觉类似于线程池, 可以先创建几个worker, 就类似于先往工作池里放几个worker进去, 在初始jobs的channel是空的时候应该会阻塞直到有任务进来(但是这里我还不太懂, 到底会阻塞到什么时候);

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "started  job", j)
        time.Sleep(time.Second)
        fmt.Println("worker", id, "finished job", j)
        results <- j * 2
    }
}

然后创建两个channel, jobs和results, 这两个channel分别用来放任务和执行结果;
然后在for循环里放创建三个worker;

    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

然后先往jobs通道里放几个任务进去, 然后关闭通道表示任务放完了(不知道这一步的意义何在, 是不是只是语义上的意义); 然后再从results里面取结果出来(一定要取5次);

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= numJobs; a++ {
        <-results
    }

我的疑问是, jobs在for循环中放进去之后, 是什么时候开始被worker执行的
我猜测可能是像线程池一样一放进去就由worker来争抢执行
但如果是这样, 问题在于worker的for j := range jobs循环中, 我的理解是jobs就类似于一个队列, 如果当前队列中没有元素, 遍历结束了就应该结束for循环了, 但事实似乎并非如此, 这个for循环就一直阻塞在这里等待jobs持续添加内容, 并且可能是在争抢读取jobs的内容;
这里我做了个实验, 发现确实对于有缓冲通道, 用for range去遍历的时候确实是会一直阻塞争抢读channel的, 对于无缓冲通道, 写完之后必须要close channel, 否则会死锁(为什么会死锁呢?)

35. WaitGroup

先看这里的worker, 传一个waitgroup进来;
这里要传指针进来, 因为如果不传指针会使用值引用, 会copy一份同样的waitgroup过来, 再在worker内部执行Done方法的时候, 原来的waitgroup不会收到这个信号;
然后这里面的defer的作用是, 可以视作在return之前执行wg.Done(), 就是在return之前调用;

func worker(id int, wg *sync.WaitGroup) {

    defer wg.Done()

    fmt.Printf("Worker %d starting\n", id)

    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

然后在主函数调用的时候这样做, 先定义一个waitgroup, 然后在协程里运行worker;
最后通过wg.Wait()来等待所有worker运行任务结束;

func main() {

    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
}

36. 速率限制

这里提供了两个做速率限制的demo;
先用一个channel来模拟request, 放了5个request进去;

requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
	requests <- i
}
close(requests)

建一个ticker, 每200ms有一条消息, 然后遍历这个limiter阻塞等消息, 每次消息来就处理;

limiter := time.Tick(200 * time.Millisecond)

for req := range requests {
	<-limiter
	fmt.Println("request", req, time.Now())
}

然后还可以做一个允许3次爆发请求的limiter, 思路也差不多, demo如下:

// 先建一个limiter
	burstyLimiter := make(chan time.Time, 3)

// 先放三条进去, 允许三次爆发请求
	for i := 0; i < 3; i++ {
		burstyLimiter <- time.Now()
	}
// 起一个ticker, 每200毫秒发一次消息
	go func() {
		for t := range time.Tick(200 * time.Millisecond) {
			burstyLimiter <- t
		}
	}()

	burstyRequests := make(chan int, 5)
	for i := 1; i <= 5; i++ {
		burstyRequests <- i
	}
	close(burstyRequests)

// 前3次会消耗掉burstyLimiter里面预留的3次, 然后去处理ticker发过来的
	for req := range burstyRequests {
		<-burstyLimiter	
		fmt.Println("request", req, time.Now())
	}

37. 原子计数器

这里展示了一下go里的协程竞争问题;

func main() {
// 建一个uint64的变量ops当计数器
    var ops uint64
// 起一个waitgroup用来等待多个协程完成递增操作
    var wg sync.WaitGroup

    for i := 0; i < 50; i++ {
        wg.Add(1)
// 起50个协程, 在每个协程里面做1000次自增1的操作
        go func() {
            for c := 0; c < 1000; c++ {
                atomic.AddUint64(&ops, 1)
            }
            wg.Done()
        }()
    }
// 等待50个协程全部完成
    wg.Wait()
// 输出ops的值, 发现是50000
    fmt.Println("ops:", ops)
}

这里如果不用atomic.AddUint64(&ops, 1), 直接用ops+=1的话就会出现竞争问题, 多个协程发生冲突, 最终ops的值就不会是50000了;

这里正好点进去看了下代码, 发现atomic.AddUint64(&ops, 1) 这个函数也可以用来做减法操作, 但是这个函数限制入参是两个uint, 如下

// AddUint64 atomically adds delta to *addr and returns the new value.
// To subtract a signed positive constant value c from x, do AddUint64(&x, ^uint64(c-1)).
// In particular, to decrement x, do AddUint64(&x, ^uint64(0)).
func AddUint64(addr *uint64, delta uint64) (new uint64)

所以, 第二个delta不能直接传一个负数进去, 要按照注释里面说的, 比如要减c, 那就要把delta传^uint64(c-1) , 这里是利用到了补码的概念, 一个负数的补码是正数取反再+1, 所以:

负数 = ^正数 +1
-c = ^c + 1 = ^(c-1)

所以, 要用atmoic的自减1就可以这么写:

atomic.AddUint64(&ops, ^(uint64(1))+1)
// or
atomic.AddUint64(&ops, ^(uint64(0)))

38. 互斥锁

互斥锁sync.Mutex, 就类似于java里的reentrantlock, 区别在于java的reentrantlock是可重入的, 但是go的sync.Mutex是不可重入的, 就比如在一个线程里, 已经获得了一次锁L, 然后又调用了一个需要占用锁L的方法, 那么java中是可以再次获得锁的, 还能知道获得了几次锁; 但是go中就不可以;

这里查了一下, go中如何实现可重入锁, 但是似乎没有, 因为 go的设计者认为,如果需要重入锁,就说明你的代码写的有问题

demo的代码有点长, 贴在下面, 很好理解, 就不解释了;

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "sync/atomic"
    "time"
)

func main() {

    var state = make(map[int]int)

    var mutex = &sync.Mutex{}

    var readOps uint64
    var writeOps uint64

    for r := 0; r < 100; r++ {
        go func() {
            total := 0
            for {

                key := rand.Intn(5)
                mutex.Lock()
                total += state[key]
                mutex.Unlock()
                atomic.AddUint64(&readOps, 1)

                time.Sleep(time.Millisecond)
            }
        }()
    }

    for w := 0; w < 10; w++ {
        go func() {
            for {
                key := rand.Intn(5)
                val := rand.Intn(100)
                mutex.Lock()
                state[key] = val
                mutex.Unlock()
                atomic.AddUint64(&writeOps, 1)
                time.Sleep(time.Millisecond)
            }
        }()
    }

    time.Sleep(time.Second)

    readOpsFinal := atomic.LoadUint64(&readOps)
    fmt.Println("readOps:", readOpsFinal)
    writeOpsFinal := atomic.LoadUint64(&writeOps)
    fmt.Println("writeOps:", writeOpsFinal)

    mutex.Lock()
    fmt.Println("state:", state)
    mutex.Unlock()
}

最后我机子上的运行结果是:

readOps: 99031
writeOps: 9903
state: map[0:1 1:5 2:79 3:62 4:36]

因为read起了100个协程, write只起了10个协程, 所以差不多readOps是writeOps的十倍左右;
(似乎demo里的total是没用的)

39. 状态协程 Stateful Goroutines

上面38里避免线程同步问题的方法是用锁, go还提供了一个更有意思的方法是使用go的channel选择器来做;

对于一个对象, 如果要限制同一时间只有一个线程能访问, 那么可以使用锁, 其实也可以把这个对象交给一个协程管理, 然后留两个channel, 一个读一个写, 然后在这个协程内部使用select处理来自reads管道和writes管道的请求, 这样就可以保证线程安全;
demo如下:

先定义读和写两种操作

type readOp struct {
	key  int
	resp chan int
}
type writeOp struct {
	key  int
	val  int
	resp chan bool
}

定义两个readOps和writeOps用来计数分别做了多少次读写操作;
起两个管道reads和writes, 用来发read和write请求;

	var readOps uint64
	var writeOps uint64

	reads := make(chan readOp)
	writes := make(chan writeOp)

起一个协程, 里面的state就是需要上锁的对象, 需要保障线程安全;
在协程中用死循环, 用select去读reads和writes管道发来的请求, 然后做对应处理;

这里我当时有一个担心, select如果同时收到来自reads和writes的请求, 是不是会按顺序先处理read后处理write呢, 这样就会让read更容易被处理, 肯定有问题;
但是实际上这样的担心是多余的, 因为go在运行的时候会自动打乱select内部的顺序, 所以并不会先处理read, 也不会先处理write, 都是随机的

	go func() {
		var state = make(map[int]int)
		for {
			select {
			case read := <-reads:
				read.resp <- state[read.key]
			case write := <-writes:
				state[write.key] = write.val
				write.resp <- true
			}
		}
	}()

然后分别测试读和写:
读的时候要建一个readOp对象, 把这个对象发到reads这个channel, 然后通过readOp的resp这个管道来接收结果;
写同理;

	for r := 0; r < 100; r++ {
		go func() {
			for {
			
				read := readOp{
					key:  rand.Intn(5),
					resp: make(chan int)}
				reads <- read
				<-read.resp
				atomic.AddUint64(&readOps, 1)
				time.Sleep(time.Millisecond)
			}
		}()
	}
	
	for w := 0; w < 10; w++ {
		go func() {
			for {
				write := writeOp{
					key:  rand.Intn(5),
					val:  rand.Intn(100),
					resp: make(chan bool)}
				writes <- write
				<-write.resp
				atomic.AddUint64(&writeOps, 1)
				time.Sleep(time.Millisecond)
			}
		}()
	}

最后看结果:

	readOpsFinal := atomic.LoadUint64(&readOps)
	fmt.Println("readOps:", readOpsFinal)
	writeOpsFinal := atomic.LoadUint64(&writeOps)
	fmt.Println("writeOps:", writeOpsFinal)

我电脑的输出结果是

readOps: 99120
writeOps: 9918

可见, read大概是write的十倍, 也并没有出现非常夸张的更优先处理read;

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页