GOLANG笔记第三周

3.1 RWMutex

测试代码----读写者问题

package main

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

func main() {
	var rwlock sync.RWMutex
	var wg sync.WaitGroup

	wg.Add(6)

	//写的goroutine
	go func() {
		time.Sleep(3 * time.Second)
		defer wg.Done()
		rwlock.Lock() //写锁,会方式别的别的写锁获取,和读锁获取
		defer rwlock.Unlock()
		fmt.Println("get write lock")
		time.Sleep(5 * time.Second)
	}()

	time.Sleep(time.Second)

	for i := 0; i < 5; i++ {

		go func() {
			defer wg.Done()

			for {
				rwlock.RLock() //加读锁,读锁不会防止别的写锁获取,和读锁获取
				time.Sleep(500 * time.Millisecond)
				fmt.Println("get read lock")
				rwlock.RUnlock()
			}

		}()
	}
	wg.Wait()
}

问题:.比如下面的代码里,为什么每个goroutine都要先defer wg.Done()?为什么不直接wg.Done()?

答:无论函数中的代码如何执行,defer 后面的函数都会被执行。这对于清理资源(如关闭文件、解锁互斥锁等)非常有用。

3.2 通过channel进行goroutine之间的通信

使用channel

package main

import "fmt"

func main() {
	/*
		不要通过共享内存来通信,而要通过通信来实现内存共享
		php,python,java等多线程编程的时候,两个goroutine之间
		通信最常用的方式是---声明一个全局变量
		也会提供消息队列的机制,python-queue 即消费者和生产者之间的关系
	*/

	/*
		而go提供了channel 再加上语法糖让使用channel更加简单
	*/

	var msg chan string        //默认值是nil
	msg = make(chan string, 0) //channel的值为0的话,如果放值进去,会阻塞


	msg <- "tc "  //放到channel中
	data := <-msg //从channel中取出,放到data中

	fmt.Println(data)
}

此时,虽然是想使用无缓冲channel(msg = make(chan string, 0)),但是这样的使用方法不对,会阻塞
需要在通过goroutine包装data :=<-msg fmt.Println(data)操作
即,创建一个协程等在这输出,这时当msg <- "tc "向channel输入数据的时候,无缓冲的直接读出数据就没问题。
完整代码:

package main

import (
	"fmt"
	"time"
)

func main() {
	/*
		不要通过共享内存来通信,而要通过通信来实现内存共享
		php,python,java等多线程编程的时候,两个goroutine之间
		通信最常用的方式是---声明一个全局变量
		也会提供消息队列的机制,python-queue 即消费者和生产者之间的关系
	*/

	/*
		而go提供了channel 再加上语法糖让使用channel更加简单
	*/

	var msg chan string        //默认值是nil
	msg = make(chan string, 0) //channel的值为0的话,如果放值进去,会阻塞

	go func(msg chan string) {

		data := <-msg //从channel中取出,放到data中
		fmt.Println(data)
	}(msg)
	msg <- "tc " //放到channel中
	time.Sleep(time.Second * 10)
}
/*
tc
*/

tips:无缓冲channel容易出现deadlock
waitgroup.Done()的缺失也容易出现deadlock

有缓冲channel和无缓存channel

无缓冲channel适用于通知,B要第一时间知道A是否已经完成

有缓冲channel适用于消费者-生产者之间的通信

3.3 forrange对channel进行遍历

先看例子:

package main

import (
	"fmt"
	"time"
)

func main() {

	var msg chan int        //默认值是nil
	msg = make(chan int, 2) //channel的值为0的话,如果放值进去,会阻塞

	go func(msg chan int) {

		data := <-msg //从channel中取出,放到data中
		fmt.Println(data)
	}(msg)
	msg <- 1 //放到channel中

	msg <- 2 //放到channel中

	time.Sleep(time.Second * 10)
}

/*1

  进程 已完成,退出代码为 0
*/

只能打印出1,因为只有一个打印操作
使用forrange可以遍历输出

package main

import (
	"fmt"
	"time"
)

func main() {

	var msg chan int        //默认值是nil
	msg = make(chan int, 2) //channel的值为0的话,如果放值进去,会阻塞

	go func(msg chan int) {

		for data := range msg {

			fmt.Println(data)
		}

		fmt.Println("all Done")
	}(msg)

	msg <- 1 //放到channel中

	msg <- 2 //放到channel中
	msg <- 3 //放到channel中
	msg <- 4 //放到channel中
	msg <- 1 //放到channel中
	//close(msg)
	time.Sleep(time.Second * 3)

}
/*
1
2
3
4
1

进程 已完成,退出代码为 0
*/

如果不使用代码中的close(msg)的话,创建的goroutine会一直等待看有没有信息输入到channel中,从而不会执行fmt.Println("all Done")语句,直到time.Sleep(time.Second * 3)—3秒后程序运行结束,子goroutine也随之结束,所以答案里没有"all Done"

当使用close(msg),就会把msg这个channel关闭,从而答案变成

1
2
3
4
1
all Done

进程 已完成,退出代码为 0

关闭channel之后,不能再放值了,会报错(但是可以取值)

package main

import (
	"fmt"
	"time"
)

func main() {

	var msg chan int        //默认值是nil
	msg = make(chan int, 2) //channel的值为0的话,如果放值进去,会阻塞

	go func(msg chan int) {

		for i := 0; i < 3; i++ {
			data := <-msg
			fmt.Println(data)
		}

		fmt.Println("all Done")
	}(msg)

	msg <- 1 //放到channel中

	msg <- 2 //放到channel中
	msg <- 3 //放到channel中
	msg <- 4 //放到channel中
	msg <- 1 //放到channel中
	close(msg)
	time.Sleep(time.Second)

	data := <-msg
	fmt.Println(data)
	time.Sleep(time.Second * 3)

}
/*
1
2
3
all Done
4

进程 已完成,退出代码为 0
*/

3.4 单向channel的应用场景

默认情况下channel是双向的
但是我们经常用一个channel作为参数进行单向传递
一个生产者-消费者的例子:

package main

import (
	"fmt"
	"time"
)

func producer(out chan<- int) {
	for i := 0; i < 10; i++ {
		out <- i * i
	}
	close(out)
}

func consumer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}

func main() {
	//默认情况下channel是双向的
	//但是我们经常用一个channel作为参数进行单向传递
	//var ch1 chan int       //正常双向channel
	//var ch2 chan<- float64 //单向channel 只能写入float64的数据
	//var ch3 <-chan int     //单向channel,只能读取

	//初始化
	//c := make(chan int, 3)  // 创建一个双向的channel,接下来将其变成单向channel
	//var send chan<- int = c // send-only
	//var read <-chan int = c // read-only
	//
	//send <- 1
	//data := <-read
	//fmt.Println(data)

	c := make(chan int) //
	go producer(c)
	go consumer(c)
	time.Sleep(time.Second * 3)
}

3.5 通过channel实现交叉打印

package main

import (
	"fmt"
	"time"
)

var number, letter = make(chan bool), make(chan bool)

func printNum() {
	i := 1
	for {
		<-number
		//怎么交叉打印?--等待另一个goroutine(无缓冲)通知
		fmt.Printf("%d%d", i, i+1)
		i += 2
		letter <- true

	}
}

func printLetter() {
	i := 0
	str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	for {
		<-letter
		if i >= len(str) {
			return
		}
		//怎么交叉打印?--等待另一个goroutine(无缓冲)通知
		fmt.Print(str[i : i+2])
		i += 2
		number <- true

	}
}

func main() {
	/*
			使用两个goroutine交替打印,一个打印数字,一个打印字母,最终结果如下:
		12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
	*/

	go printNum()
	go printLetter()
	number <- true
	time.Sleep(time.Second * 3)
}

3.6监控goroutine的执行

原方法

package main

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

var done bool
var lock sync.Mutex

func g1() {
	//
	time.Sleep(time.Second)
	lock.Lock()
	defer lock.Unlock()

	done = true
}

func g2() {
	time.Sleep(2 * time.Second)

	lock.Lock()
	defer lock.Unlock()

	done = true
}

func main() {
	//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
	//select主要作用于多个channel
	//

	//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
	//当某一个执行完成之后,我会立马知道
	go g1()
	go g2()

	for {
		if done {
			fmt.Println("done")
			time.Sleep(10 * time.Millisecond)
			return
		}

	}

}

我们应该使用channel来进行goroutine之间的通信
使用channel的阻塞的方法

package main

import (
	"fmt"
	"time"
)

var done = make(chan struct{}) //channel是多线程安全的,多个线程对channel读写也是安全的

func g1() {
	//
	time.Sleep(time.Second)

	done <- struct{}{}
}

func g2() {
	time.Sleep(2 * time.Second)

	done <- struct{}{}
}

func main() {
	//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
	//select主要作用于多个channel
	//

	//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
	//当某一个执行完成之后,我会立马知道
	go g1()
	go g2()
	<-done
	fmt.Println("done")

}

这样的话就出现了新问题

如果有两个及以上的channel个数的话,哦那个这种直接阻塞的方法就会互相卡住。
所以使用select语句

package main

import (
	"fmt"
	"time"
)

func g1(ch chan struct{}) {
	//
	time.Sleep(2 * time.Second)

	ch <- struct{}{}
}

func g2(ch chan struct{}) {
	time.Sleep(time.Second)

	ch <- struct{}{}
}

func main() {
	//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
	//select主要作用于多个channel
	//

	//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
	//当某一个执行完成之后,我会立马知道
	g1channel := make(chan struct{})
	g2channel := make(chan struct{})
	go g1(g1channel)
	go g2(g2channel)

	//某一个分支就绪了,就执行该分支
	//如果两个都就绪了,那就是随机执行,目的是,防止饥饿!!!
	select {
	case <-g1channel:
		fmt.Println("g1 done")
	case <-g2channel:
		fmt.Println("g2 done")
	}

	fmt.Println("done")

}

某一个分支就绪了,就执行该分支
如果两个都就绪了,那就是随机执行,目的是,防止饥饿!!!

问题:那么如果两个goroutine都阻塞住了,那主程序不就是也阻塞住了吗?
使用,引入超时机制,具体是使用select里的default

	select {
	case <-g1channel:
		fmt.Println("g1 done")
	case <-g2channel:
		fmt.Println("g2 done")
	default:
		fmt.Println("default")
	}

但是这样就直接default,来不及等待g1和g2完成,没有给他们时间

所以在外面使用ttime.NewTimer(time.Second)创建一个新的 channel,会在1s之后生成

package main

import (
	"fmt"
	"time"
)

func g1(ch chan struct{}) {
	//
	time.Sleep(2 * time.Second)

	ch <- struct{}{}
}

func g2(ch chan struct{}) {
	time.Sleep(time.Second)

	ch <- struct{}{}
}

func main() {
	//select 类似于switch case语句,但是select功能和Linux里面提供的io的select,poll,epoll相似
	//select主要作用于多个channel
	//

	//现在有个需求,现在有两个goroutine都在执行,我现在需要知道我在主的goroutine,
	//当某一个执行完成之后,我会立马知道
	g1channel := make(chan struct{})
	g2channel := make(chan struct{})
	go g1(g1channel)
	go g2(g2channel)

	//某一个分支就绪了,就执行该分支
	//如果两个都就绪了,那就是随机执行,目的是,防止饥饿!!!

	timer := time.NewTimer(time.Second)

	for {

		select {
		case <-g1channel:
			fmt.Println("g1 done")
		case <-g2channel:
			fmt.Println("g2 done")
		case <-timer.C:
			fmt.Println("timeout")
		}
	}

}

3.7 通过context解决goroutine的信息传递

使用全局变量的方法

package main

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

var wg sync.WaitGroup

func cpuIInfo() {

	for {
		time.Sleep(2 * time.Second)
		fmt.Println("get cpu")
	}
}

func main() {
	//场景:有一个goroutine 监控cpu的信息
	wg.Add(1)
	go cpuIInfo()

	wg.Wait()

	fmt.Println("done")
}

现在有新的需求-----我可以主动退出监控程序

比较容易想到的方法,使用一个共享变量,for循环中不断查询是否要停止

package main

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

var wg sync.WaitGroup
var stop bool

func cpuIInfo() {
	defer wg.Done()
	for {

		if stop {
			break
		}

		time.Sleep(2 * time.Second)
		fmt.Println("get cpu")
	}
}

func main() {
	//场景:有一个goroutine 监控cpu的信息
	wg.Add(1)
	go cpuIInfo()
	time.Sleep(8 * time.Second)
	stop = true
	wg.Wait()

	fmt.Println("done")
}

当然,由于主程序和子goroutine都对stop进行了读写,最好加一个锁(lock),这里就不演示了。

但是我们说过尽量不要使用共享变量的方式。
接下来演示消息(管道)方式

package main

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

var wg sync.WaitGroup

func cpuIInfo(stop chan struct{}) {
	defer wg.Done()
	for {

		select {
		case <-stop:
			fmt.Println("退出cpu监控")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("get cpu")
		}

	}
}

func main() {
	var stop = make(chan struct{})

	//场景:有一个goroutine 监控cpu的信息
	wg.Add(1)
	go cpuIInfo(stop)
	time.Sleep(8 * time.Second)
	stop <- struct{}{}
	wg.Wait()

	fmt.Println("done")
}

接下来介绍一种更优雅的方式----context

package main

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

var wg sync.WaitGroup

func cpuIInfo(ctx context.Context) { //context 是一个Interface
	defer wg.Done()
	for {

		select {
		case <-ctx.Done():
			fmt.Println("退出cpu监控")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("get cpu")
		}

	}
}

func main() {

	//场景:有一个goroutine 监控cpu的信息
	wg.Add(1)

	//有两个返回值,
	ctx1, cancel1 := context.WithCancel(context.Background()) //Backgroud一般作为父的context
	//context提供了三种函数,WithCancel,withTimeout,WithValue
	//如果你的goroutine,函数中希望被控制,但是不希望影响原来的接口信息的时候
	//函数参数中,第一个参数尽量选择context

	ctx2, _ := context.WithCancel(ctx1) // 子cancel方法
	go cpuIInfo(ctx2)
	time.Sleep(8 * time.Second)

	cancel1() //调用父亲的cancel,也会调用子context的cancel,也可以起到控制子cancel的作用
	wg.Wait()

	fmt.Println("done")
}

3.8 WithValue,WithTimeout的应用场景

WithTimeout方式:
使用ctx, _ := context.WithTimeout(context.Background(), 8*time.Second)
可以创建一个context,并且在6秒后自动调用cancel,可以达到之前

	time.Sleep(6 * time.Second)
	cancel()

相同的效果

WithValue方式:
简单来说就是可以传值

package main

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

var wg sync.WaitGroup

func cpuIInfo(ctx context.Context) { //context 是一个Interface

	fmt.Printf("tracid: %s\r\n", ctx.Value("tracid"))
	//记录日志,这一次请求是哪个tracid打印的

	defer wg.Done()
	for {

		select {
		case <-ctx.Done():
			fmt.Println("退出cpu监控")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("get cpu")
		}

	}
}

func main() {

	//场景:有一个goroutine 监控cpu的信息
	wg.Add(1)

	//有两个返回值,
	//ctx1, cancel1 := context.WithCancel(context.Background()) //Backgroud一般作为父的context
	//context提供了三种函数,WithCancel,withTimeout,WithValue
	//如果你的goroutine,函数中希望被控制,但是不希望影响原来的接口信息的时候
	//函数参数中,第一个参数尽量选择context

	//ctx2, _ := context.WithCancel(ctx1) // 子cancel方法
	ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)

	value := context.WithValue(ctx, "tracid", "gjw12j")

	go cpuIInfo(value)
	wg.Wait()
	fmt.Println("done")
}
 

结束!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值