Golang goroutine、通道、锁

goroutine是go语言的核心之一,非常重要的一点

它实际是在操作系统线程上执行的,并且属于广义上的协程并发机制

语法格式为:go 方法/函数

开启的goroutine对象只能是函数或者方法。

看下面的例子

Go 复制代码
func Test() {

	fmt.Println("1")
}
func main() {
	go Test()
	fmt.Println("2")
}

最后的结果是直接输出的2,虽然启动了协程去执行Test函数,但是它并没有被执行到

这里就要说到这一点:main结束之后所有的执行中、未执行、等待中的goroutine全部销毁。这里就引出了一个问题:如果是这样,就需要让主函数来等待所有goroutine结束之后才可以退出。

Go 复制代码
func Test(wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("1")
}
func main() {
	wg := new(sync.WaitGroup)
	wg.Add(1)
	go Test(wg)
	wg.Wait()
	fmt.Println("2")
}

这里我们使用了sync包里面的WaitGroup,直译为等待组。它被创建的时候有一个内置的计数器,值为0,执行了Add(n)函数之后,会+n,执行了Done()函数之后会-1,Wait()函数会阻塞程序,直到内置计数器值为0才放行。

创建的不同方法,wg1是指针类型,wg2就是 sync.WaitGroup类型

Go 复制代码
wg1 := new(sync.WaitGroup)
wg2 := sync.WaitGroup{}

函数接收等待组的时候接受的一定且必须是指针类型。不然会报错,报错的原因是死锁,因为值传递=copy一份新的,Done()函数的操作修改不了原来的等待组。但是呢main里面的等待组还是在等待,因为没有对应的Done()导致了一直在等待

在下面这个例子中,一共有两个goroutine,Test函数虽然逻辑不对,但是语法是没问题的。所以完整的执行并退出了。但是main所处的goroutine阻塞了,2-1=1,阻塞之后go检测到所有的goroutine都是阻塞状态,程序无法恢复正常运行就会报出死锁错误

Go 复制代码
func Test(wg sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("1")
}
func main() {
	wg := sync.WaitGroup{}

	wg.Add(1)
	go Test(wg)
	wg.Wait()
	fmt.Println("2")
}

通道,goroutine之间的通信常使用通道进行

Go 复制代码
ch := make(chan int,n)  //n代表通道缓存,可以存储多少个数据(单位是前面的int,可以自己随意定制)

无缓冲区的通道,接收方和发送方需要配合才可以进行下去。接收方准备好读取数据<- ch,发送方准备写入数据ch <- 先执行到的一方会阻塞,等待另一方执行到相同阶段(读写同时准备好)

而有缓冲区的就会相对方便一些。只要缓冲区没满,发送方就可以发送。

接下来的例子演示goroutine之间的通信

Go 复制代码
type T struct {
	Name string
	Age  int
}

func Send(wg *sync.WaitGroup, ch chan T) {
	defer func() {
		wg.Done()
		close(ch)
	}()
	for i := 1; i < 6; i++ {
		fmt.Println("输入数据:")
		ch <- struct {
			Name string
			Age  int
		}{Name: fmt.Sprintf("Test:%d", i), Age: i}
		time.Sleep(1 * time.Second)
	}

}
func Get(wg *sync.WaitGroup, ch chan T) {
	defer wg.Done()
	for x := range ch {
		fmt.Println("接收参数:")
		fmt.Println(x.Name)
		fmt.Println(x.Age)

	}
}
func main() {
	ch := make(chan T, 3)
	wg := sync.WaitGroup{}
	wg.Add(2)
	go Send(&wg, ch)
	go Get(&wg, ch)
	wg.Wait()
}

通道的读写通过 -> 和 <-来进行,但是针对于读操作,还有range可以,它会循环读取,没有数据就阻塞。

通道的关闭是close(ch)语法格式。

读取已关闭且没有缓冲区或者缓冲区为空的通道会获取通道存储数据的零值。向已关闭的通道写数据会panic。

下面这个读取的方法会判断通道是否已关闭并且缓存区为空,满足条件值为false

Go 复制代码
x, ok := <-ch

有时候,我们在高并发的情况下操作同一个数据,可能会导致一些意料之外的问题

Go 复制代码
func Add(num *int, wg *sync.WaitGroup) {
	defer wg.Done()
	*num++
}
func Sub(num *int, wg *sync.WaitGroup) {
	defer wg.Done()
	*num--
}

func main() {
	var num int
	wg := sync.WaitGroup{}

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go Add(&num, &wg)
		wg.Add(1)
		go Sub(&num, &wg)
	}
	wg.Wait()
	fmt.Println(num)
}

这个例子会导致一个很严重的问题:缺少必要同步机制的操作数据会导致数据丢失。按理来说一百次加和一百次减,最后的结果一定是0,但是实验下来结果并不稳定。正负数和0都是会出现的结果。

这里会出现的问题称为数据竞争:指多个 goroutine 在缺少必要同步的情况下,并发访问同一块内存,并且至少有一个访问是写操作。

这里导致该问题的主要原因是:*num--操作分三步 第一步:先取地址的值,第二步:计算,第三步:写回到地址

如果在这三个步骤之间,有其他goroutine有执行了相同的操作,由于数值未更新,两次取值可能是一样的值。这就导致了操作执行了,但是数据丢了。这时候就需要一个工具,当有goroutine执行到的时候就锁上,表示这里暂时不可用。其他的先等一等。执行完之后开锁即可

Go 复制代码
func Add(num *int, wg *sync.WaitGroup, mu *sync.Mutex) {
	defer func() {
		defer wg.Done()
		defer mu.Unlock()
	}()
	mu.Lock()
	*num++
}
func Sub(num *int, wg *sync.WaitGroup, mu *sync.Mutex) {
	defer func() {
		defer wg.Done()
		defer mu.Unlock()
	}()
	mu.Lock()
	*num--
}

func main() {
	var num int
	wg := sync.WaitGroup{}
	var mu = new(sync.Mutex) //new方法直接返回指针

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go Add(&num, &wg, mu)
		wg.Add(1)
		go Sub(&num, &wg, mu)
	}
	wg.Wait()
	fmt.Println(num)
}

这里通过new(sync.Mutext)创建了一个指针变量。在常被当作指针传递的变量声明中,可以且推介使用new创建变量。

它提供了两个最基础的方法Lock() 和 Unlock()。当一个goroutine执行Lock的时候就会把这把锁锁上,其它的goroutine执行到Lock的时候发现已经是锁状态就会阻塞等待解锁。

针对于读写锁的介绍

在sync包中,不只有Mutex,还有RWMutex。这个是读写锁。针对于读操作远大于写操作的情况下,继续使用Mutex会影响程序运行速度。但是不上锁会导致数据竞争。这时候就可以使用读写锁

三个规则:

读锁不阻止其他读锁。

读锁会阻止写锁。

写锁会阻止读锁和其他写锁。

Go 复制代码
func main() {
	var mu sync.RWMutex
	var wg sync.WaitGroup

	// 第一个 goroutine:持有读锁
	wg.Add(1)
	go func() {
		defer wg.Done()

		mu.RLock()
		fmt.Println("读操作:获得读锁")

		time.Sleep(2 * time.Second)

		mu.RUnlock()
		fmt.Println("读操作:释放读锁")
	}()

	// 让读操作有机会先获得读锁
	time.Sleep(200 * time.Millisecond)

	// 第二个 goroutine:尝试获得写锁
	wg.Add(1)
	go func() {
		defer wg.Done()

		fmt.Println("写操作:尝试获得写锁")
		mu.Lock()
		fmt.Println("写操作:获得写锁")

		mu.Unlock()
		fmt.Println("写操作:释放写锁")
	}()

	wg.Wait()
}

这个例子很清晰的展示了读锁会阻塞写锁

针对一只想执行一次的操作,引入了sync包的Once

Go 复制代码
var once sync.Once

once.Do(func() {
    fmt.Println("A")
})

once.Do(func() {
    fmt.Println("B")
})

同一个once实例只能执行一次函数,通过Do()方法接收。不管是不是同一个函数,一个once实例只能执行一次函数

并发map,下面是部分的基础用法。

Go 复制代码
func main() {
	m := sync.Map{}

	m.Store(1, "name")
	m.Store(2, "age")
	m.Store("id", 123)

	value, ok := m.Load(3)
	if ok {
		fmt.Println(value)
	} else {
		fmt.Println("the key is not exist")
	}

	m.Delete("id")

	m.Range(func(key, value interface{}) bool {
		fmt.Println("key", key, "  value", value)
		return true
	})
}

它并不能代替Mutex,因为在某些场景还是不安全的,它可以保证每次操作的并发安全,但是不能保证多次操作组合起来的业务逻辑具有原子性。这个问题可以通过Mutex来解决

其实就是说在执行a逻辑的时候如果b逻辑也同时操作了同一个数据。就可能会导致数据竞争

条件变量:sync .Cond 它是基于互斥锁的。条件不满足的时候让goroutine阻塞并释放锁。等待条件完成之后重新执行

Go 复制代码
func main() {
	var mu sync.RWMutex
	cond := sync.NewCond(&mu)

	queue := []int{}

	var wg sync.WaitGroup
	wg.Add(2)

	// 消费者
	go func() {
		defer wg.Done()

		mu.Lock()

		for len(queue) == 0 {
			fmt.Println("队列为空,消费者开始等待")
			cond.Wait()
		}

		value := queue[0]
		queue = queue[1:]

		mu.Unlock()

		fmt.Println("消费者取出数据:", value)
	}()

	// 生产者
	go func() {
		defer wg.Done()

		mu.Lock()

		queue = append(queue, 100)
		fmt.Println("生产者放入数据:100")

		cond.Signal()
        //假设后面还有很多逻辑操作

		mu.Unlock()
	}()

	wg.Wait()
}

这里需要讲解一点:cond.Wait()之后会释放锁并阻塞当前goroutine。一直等到cond.Sinal()才会重新获得锁。在代码中cond.Sinal()下有一行注释:假设后面还有很多逻辑操作,cond.Sinal()调用时候等待中的goroutine不会立刻重新获取锁,而是等到当前当前锁被释放之后才会给等待中的goroutine

相关推荐
ThinkerQAQ_1 小时前
并发编程(1.5):操作系统层——Thread、Scheduler 与 Runtime 调度
后端
开开心心_Every1 小时前
内存清理工具定时自动清理开机自启动
java·开发语言·spring·计算机外设·ocr·intellij-idea·excel
纪念 2291 小时前
c++ string(二)
开发语言·c++
SimonKing1 小时前
零大模型依赖!SQLite FTS5 + jieba 打造纯本地知识库搜索
后端·python·程序员
李福春1 小时前
斗破Google ADK Java苍穹-第5境斗灵-凝物通脉-Context与State
java·开发语言·人工智能·java-ee·google adk·深圳同盟·腾讯云架构师技术同盟
十年Java程序媛1 小时前
Java 接口和抽象类对比|抛弃老旧八股,正确选型
java·开发语言·spring boot·后端
见叶之秋1 小时前
【C++】从基类到派生类:关于C++继承的务实梳理
开发语言·c++
kyle~1 小时前
C++ 智能指针:RAII、所有权与对象生命周期
开发语言·c++
Eyfcom1 小时前
快递管理系统中的电子围栏实战:高德与谷歌双平台技术选型与实现
java·后端·快递管理系统