[Golang] Sync

news2024/11/13 7:56:11

[Golang] Sync

文章目录

  • [Golang] Sync
    • sync.WaitGroup
    • sync.Once
      • 对比init()
    • sync.Lock
      • 互斥锁和读写锁
        • 互斥锁
        • 读写锁
      • 死锁问题
        • 加锁解锁不成对
        • 循环等待
    • sync.Map
    • sync/atomic
      • atomic和mutex的区别
      • atomic.value
    • sync.Pool
      • 如何使用
      • sync.Pool使用场景

Golang中我们一般提倡使用通信来共享内存,不使用共享内存来通信,比如Goroutine之间通过channel来协作。而其他语言中,都是通过共享内存加锁机制来保证并发安全的,同样的Golang中也提供对共享内存并发安全机制的支持,它们都在sync包中。

sync.WaitGroup

之前我们已经使用过sync.WaitGroup来替换time.Sleep,让一个协程等待一个协程执行完毕,也就是使用sync.WaitGroup来实现并发任务的同步以及协程任务等待。

使用方式:

sync.WaitGroup是一个对象,里面维护一个计数器,并通过三个方法来配合使用

  • (wg *WaitGroup) Add(delta int) 计数器加cnt

  • (wg *WaitGroup) Done() 计数器减1

  • (wg *WaitGroup) Wait() 阻塞代码运行,直到计数器减为0

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go myGoroutine(&wg)
	}
	wg.Wait()
}
func myGoroutine(wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("myGoroutine")
}

执行结果:

image-20240916175936050

先把计数器设置为10,每运行完一个myGoroutine就把计数器减1,main函数等待计数器值为0,也就是10个子协程打印完10个myGoroutine后,子协程全部退出,主协程才会退出。

ps:计数器的值不能减为负数,不然就会panic。

sync.Once

很多时候,程序中有很多逻辑只需要执行一次,比如配置文件的加载,我们只需要加载一次,让配置保存在内存中,下次直接使用内存中的配置文件即可,这时就会用到sync.Once

sync.Once可以在代码的任意位置初始化和调用,并且线程安全。对于一个sync.Once变量我们并不会在程序启动时初始化,而是在第一次使用到它时才进行初始化,并且只初始化这一次,初始化后保存在内存中,这就很符合我们刚刚说到的配置文件加载的场景,这其实也就是单例模式中的懒汉模式。毕竟一开始就加载到内存中,长时间不用就浪费了内存。

package main

import (
	"fmt"
	"sync"
)

type Config struct{}

var instance *Config
var once sync.Once
var cnt int

func InitConfig() *Config {
	once.Do(func() {
		fmt.Printf("第%d次被调用", cnt)
		instance = &Config{}
	})
	return instance
}

func main() {
	cnt = 1
	InitConfig()
	InitConfig()
	go InitConfig()
	InitConfig()
	go InitConfig()
	InitConfig()
	InitConfig()

}

执行结果:

image-20240916181728455

只有第一次调用InitConfig()时获取Config指针时才会执行once.Do()语句,执行完后instance就保留在内存中了,后面再次执行时,会直接返回这个instance。

对比init()

  • init():适用于程序启动时的初始化,确保在主函数执行前完成初始化任务。比如,日志配置初始化,在程序启动时配置日志文件。
  • sync.Once:适用于延迟初始化且在并发环境下只需要执行一次的初始化任务。比如,配置文件加载、数据库连接池初始化。

sync.Lock

并发编程中资源的竞争,Golang给出了两种解决方案:锁和原子操作。

package main

import (
    "fmt"
    "sync"
)

func main() {
    n := 10000
    sum := 0

    var wg sync.WaitGroup
    wg.Add(n)

    for i := 0; i < n; i++ {
       go func() {
          defer wg.Done()
          sum += 1
       }()
    }
    wg.Wait()
    fmt.Println(sum)
}

执行结果:

image-20240916182645164

最后10000个协程执行完之后,sum并不是1000,这就出现了并发问题。同一时间多个Goroutine对sum做+1操作,但是不是在在前一个协程执行完的基础上做的累加,这样前一个协程的执行就会被后一个协程的执行结果覆盖了。

互斥锁和读写锁

互斥锁

互斥锁:同一时间只允许一个goroutine对共享资源进行访问。

var lock sync.Lock
func (m *Mutex) Lock()		// 加锁
func (m *Mutex) UnLock()	// 解锁

我们对上面的代码稍加修改:

package main

import (
	"fmt"
	"sync"
)

func main() {
	n := 10000
	sum := 0

	var wg sync.WaitGroup
	wg.Add(n)
	mu := sync.Mutex{}

	for i := 0; i < n; i++ {
		go func() {
			mu.Lock()
			defer wg.Done()
			sum += 1
			mu.Unlock()
		}()
	}
	wg.Wait()
	fmt.Println(sum)
}

执行结果:

image-20240916183516253

注意:加锁之后不用忘记解锁,否则会造成其他goroutine一直阻塞。

读写锁

把读操作和写操作分离,一般用于大量读操作、少量写操作的情况。

var mr sync.RWMutex

func (rw *RWMutex) Lock()		// 对写锁加锁
func (rw *RWMutex) UnLock()		// 对写锁解锁

func (rw *RWMutex) RLock()		// 对读锁加锁
func (rw *RWMutex) RUnLock()	// 对读锁解锁

读写锁的使用:多个Goroutine可以同时读,但是只有一个Goroutine能写;共享资源要么在被一个或多个Goroutine读,要么在被一个Goroutine写,读写不能同时进行。

死锁问题

死锁:在两个以上的Goroutine执行过程中,因为争抢共享资源处于互相等待的状态,如果没有外部干涉将一直处于这个阻塞的状态。

加锁解锁不成对

这种场景一般是对锁进行拷贝的使用:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	mu.Lock()
	defer mu.Unlock()
	copyMutex1(mu)
	copyMutex2(&mu)
	fmt.Println("main end...")
}
func copyMutex1(mu sync.Mutex) {
	mu.Lock()
	defer mu.Unlock()
	fmt.Println("copyMutex1 end...")
}
func copyMutex2(mu *sync.Mutex) {
	mu.Lock()
	defer mu.Unlock()
	fmt.Println("copyMutex2 end...")
}

执行结果:

image-20240916184736568

如果把带有锁结构的变量赋值给其他变量,锁的状态会复制。所以复制之后的锁已经有了原来的锁状态,那么copyMutex()中执行mu.Lock()会被一直阻塞,因为外面的main函数中已经Lock()过了一次但是还没有Unlock()。这就导致了copyMutex()内锁一直在等待Lock(),而main()内一直在等待解锁,这就导致了死锁。

所以在使用锁时,我们要避免拷贝锁,并且Lock()和UnLock要成对出现。

循环等待
package main

import (
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	var mu1, mu2 sync.Mutex
	go func() {
		defer wg.Done()
		mu1.Lock()
		defer mu1.Unlock()
		time.Sleep(time.Second)
		mu2.Lock()
		defer mu2.Unlock()
	}()
	go func() {
		defer wg.Done()
		mu2.Lock()
		defer mu2.Unlock()
		time.Sleep(time.Second)
		mu1.Lock()
		defer mu1.Unlock()
	}()
	wg.Wait()
}

执行结果:

image-20240916190424536

两个Goroutine在加第二个锁时,都会等待对方释放锁,造成了循环等待,一直阻塞,形成了死锁。

sync.Map

golang中内置的Map不是并发安全的,多个goroutine同时操作map时会有并发问题。

比如:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(10)

	mp := make(map[int]int)
	for i := 0; i < 10; i++ {
		go func(num int) {
			defer wg.Done()
			mp[num] = num + 1
			fmt.Printf("key=%v, value=%v", num, mp[num])
		}(i)
	}
	wg.Wait()
}

执行结果:

image-20240916192225248

说明内置的map不能并发操作。

解决方案1:

加锁:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	var mu sync.Mutex

	mp := make(map[int]int)
	for i := 0; i < 10; i++ {
		go func(num int) {
			defer wg.Done()
			mu.Lock()
			defer mu.Unlock()
			mp[num] = num + 1
			fmt.Printf("key=%v, value=%v\n", num, mp[num])
		}(i)
	}
	wg.Wait()
}

执行结果:

image-20240916192118056

解决方案2:

使用并发安全的map:sync.Map

比如:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mp sync.Map
	// 1.写入
	mp.Store("name", "张三")
	mp.Store("age", 18)
	// 2.读取
	age, _ := mp.Load("age")
	fmt.Println(age.(int)) // 断言age为int类型
	// 3.遍历
	mp.Range(func(key, value any) bool {
		fmt.Printf("key = %v, value = %v\n", key, value)
		return true
	})
	// 4.删除
	mp.Delete("age")
	age, ok := mp.Load("age")
	fmt.Println(age, ok)
	// 5.读取或写入
	mp.LoadOrStore("name", "李四")
	name, _ := mp.Load("name")
	fmt.Printf("name = %v", name)
}

执行结果:

image-20240916193324191

sync/atomic

之前说了锁,现在说另一种解决并发安全的策略:atomic原子操作。

// T的类型为 int32、int64、uint32、uint64和uintptr的任意一种
func AddT(addr *T, delta T)(new T)
func StoreT(addr *T, val T)
func LoadT(addr *T) (val T)
func SwapT(addr *T, new T) (old T)
func CompareAndSwap(addr *T, old,new T) (swapped bool)

例如:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(100)
	var num int32 = 0
	for i := 0; i < 100; i++ {
		go func() {
			defer wg.Done()
			atomic.AddInt32(&num, 1)
		}()
	}
	wg.Wait()
	fmt.Println(num)
}

执行结果:

image-20240916194456421

atomic和mutex的区别

使用方式:mutex通常用于保护一段代码执行逻辑;atomic一般用于对变量的操作

底层实现:mutex由操作系统调度器实现;atomic操作有底层硬件指令支持,保证cpu在执行上不中断。

atomic.value

atomic也支持了对struct这种复合类型进行原子操作。

比如:

package main

import (
	"fmt"
	"sync/atomic"
)

type Student struct {
	Name string
	Age  int
}

func main() {
	st1 := Student{
		Name: "张三",
		Age:  18,
	}
	st2 := Student{
		Name: "李四",
		Age:  20,
	}
	st3 := Student{
		Name: "王五",
		Age:  22,
	}
	var v atomic.Value
	v.Store(st1)             // 1.写入
	st := v.Load().(Student) // 2.读取
	fmt.Println(st)

	old := v.Swap(st2) // 3.交换
	st = v.Load().(Student)
	fmt.Println("after swap :", st)
	fmt.Println("old :", old)

	swapped := v.CompareAndSwap(st1, st3) // 4.比较并交换
	fmt.Println("交换: ", swapped)
	st = v.Load().(Student)
	fmt.Println(st)
	swapped = v.CompareAndSwap(st2, st3) // 4.比较并交换
	fmt.Println("交换: ", swapped)
	st = v.Load().(Student)
	fmt.Println(st)
}

执行结果:

image-20240916195800443

sync.Pool

sync.Pool是sync包下的一个内存池组件,用来实现对象的复用,避免重复创建相同的对象,造成频繁的内存分配和gc,以达到提升程序性能的目的。虽然池子中的对象可以被复用,但是sync.Pool并不会永久保存这个对象,池中的对象会在一定时间后被gc回收,这个时间是随机的。所以不能用sync.Pool来持久化存储对象。

如何使用

New()	// sync.Pool的构造函数,用于指定sync.Pool中缓存的数据类型,
		// 如果调用Get()时,池中没有元素就会调用New()方法创建一个新对象
Get()	// 从对象池取对象
Put()	// 向对象池中放对象,下次Get()时可以复用

例如:

package main

import (
	"fmt"
	"sync"
)

type Student struct {
	Name string
	Age  int
}

func main() {
	pool := sync.Pool{
		New: func() interface{} {
			return &Student{
				Name: "张三",
				Age:  18,
			}
		},
	}
    st := pool.Get().(*Student)			//(*Student)断言
	println(st.Name, st.Age)
	fmt.Printf("st addr = %p\n", st)
	pool.Put(st)

	st2 := pool.Get().(*Student)
	println(st2.Name, st2.Age)
	fmt.Printf("st addr = %p\n", st2)
}

执行结果:

image-20240916204315463

程序逻辑:

我们先初始化一个sync.Pool对象,初始化New()方法,用于创建对象,这里是返回一个*Student。

第一次调用Get()时,池中没有对象,所以会调用New()方法创建一个,由于返回类型为interface{}所以需要我们断言一下

使用完后,再调用Put()方法,把对象放回池中,再调用pool.Get取对象,此时我们可能看到的对象地址是同一个,如果看到同一个说明sync.Pool有缓存对象的功能。

ps:由于 sync.Pool 的设计是为了在高并发环境下工作,它的行为可能不是完全可预测的。在某些情况下,即使你将对象放回了池中,下次获取时也可能得到一个新的对象。

一般我们如果在修改对象字段后,回收前记得Reset,否则取到的对象是同一个,但是字段内容变化了。

比如:

package main

import (
	"fmt"
	"sync"
)

type Student struct {
	Name string
	Age  int
}

func main() {
	pool := sync.Pool{
		New: func() interface{} {
			return &Student{
				Name: "张三",
				Age:  18,
			}
		},
	}
	st := pool.Get().(*Student)
	println(st.Name, st.Age)
	fmt.Printf("st addr = %p\n", st)
	
    // 修改
	st.Name = "李四"
	st.Age = 20

	pool.Put(st)

	st2 := pool.Get().(*Student)
	println(st2.Name, st2.Age)
	fmt.Printf("st2 addr = %p\n", st2)
}

执行结果:

image-20240916204503090

sync.Pool没有提供Reset函数,一般需要我们进行手写:

package main

import (
	"fmt"
	"sync"
)

type Student struct {
	Name string
	Age  int
}

func main() {
	pool := sync.Pool{
		New: func() interface{} {
			return &Student{
				Name: "张三",
				Age:  18,
			}
		},
	}

	st := pool.Get().(*Student)
	println(st.Name, st.Age)
	fmt.Printf("st addr = %p\n", st)

	// 修改对象状态
	st.Name = "李四"
	st.Age = 20

	// 在放回池中之前重置对象状态
	resetStudent(st)

	pool.Put(st)

	st2 := pool.Get().(*Student)
	println(st2.Name, st2.Age)
	fmt.Printf("st2 addr = %p\n", st2)
}

// resetStudent 用于重置 Student 对象的状态
func resetStudent(s *Student) {
	s.Name = "张三"
	s.Age = 18
}

sync.Pool使用场景

sync.Pool通过复用对象来降低gc带来的性能损耗,高并发场景下,每个Goroutine都可能频繁创建一些大对象,造成gc压力很大。所以在高并发场景下出现gc问题时,可以使用sync.Pool减少gc负担。

sync.Pool不能存储带状态的对象,比如Socket连接、数据库连接,因为池中的对象随时可能被gc回收释放;sync.Pool不适合控制缓存对象个数的场景,因为sync.Pool中的对象个数是随机变化的,池中的对象随时有可能被gc的,释放时机是随机的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2141672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java入门,初识Java

Java背景知识 Java是早期美国 sun 公司&#xff08;Stanford University Network&#xff09;在1995年推出的一门计算机高级编程语言。Java早期称为Oak&#xff08;中文翻译为&#xff1a;橡树&#xff09;&#xff0c;后期改名为Java。&#xff08;因为当时sun公司门口有很多…

MySQL_数据类型简介

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

【计算机网络】TCP 协议——详解三次握手与四次挥手

文章目录 一、引言二、TCP1、TCP 的数据格式2、TCP 的三次握手3、TCP 的四次挥手4、TCP 的全双工通信 三、TCP 的状态转换1、TCP 连接的建立&#xff08;三次握手&#xff09;状态2、TCP 连接的终止&#xff08;四次挥手&#xff09;状态3、TCP 异常情况 一、引言 TCP与UDP的区…

Linux操作系统文件权限管理

Linux操作系统下文件的权限分为当前用户权限、用户组权限和其他用户权限&#xff0c;然后每一类用户或组又分为读权限(r)、写权限(w)和可执行权限(x)。 如图1&#xff0c;打开任一目录&#xff0c;右键单击文件&#xff0c;在弹出菜单选择“属性”&#xff0c;在弹出的属性选项…

一款强大的吉他乐谱编辑软件GuitarPro 8.2中文解锁版

GuitarPro 8.2中文解锁版是一款强大的吉他乐谱编辑软件&#xff0c;适合新手和专业乐手。它提供详尽教程和实用工具&#xff0c;助力初学者掌握吉他技巧&#xff1b;对于专业乐手&#xff0c;它精准记录音符和节奏&#xff0c;提供丰富编辑功能和音效处理。此外&#xff0c;软件…

【python版】示波器输出的csv文件(时间与电压数据)如何转换为频率与幅值【方法④】

将示波器输出的 CSV 文件中的时间和电压数据转换为频率和幅值数据的过程涉及几个步骤&#xff0c;下面是详细的步骤和相关的计算公式&#xff1a; 1. 导入数据 首先&#xff0c;你需要将 CSV 文件中的时间和电压数据导入到数据分析工具中&#xff0c;比如 Python&#xff08;…

【网络】TCP/IP 五层网络模型:网络层

最核心的就是 IP 协议&#xff0c;是一个相当复杂的协议 TCP 详细展开讲解&#xff0c;是因为 TCP 确实在开发中非常关键&#xff0c;经常用到&#xff0c;IP 则不同&#xff0c;和普通程序猿联系比较浅。和专门开发网络的程序猿联系比较紧密&#xff08;开发路由器&#xff0…

深度学习自编码器 - 随机编码器和解码器篇

序言 在深度学习领域&#xff0c;自编码器作为一种无监督学习技术&#xff0c;凭借其强大的特征表示能力&#xff0c;在数据压缩、去噪、异常检测及生成模型等多个方面展现出独特魅力。其中&#xff0c;随机编码器和解码器作为自编码器的一种创新形式&#xff0c;进一步拓宽了…

COTERRORSET—— LLM训练新基准让模型从自身错误中学习

概述 论文地址&#xff1a;https://arxiv.org/pdf/2403.20046.pdf 在最近的研究中&#xff0c;大规模语言模型因其推理能力而备受关注。这些模型在各种任务和应用中取得了令人瞩目的成果&#xff0c;尤其是使用思维链&#xff08;CoT&#xff09;提示方法的有效性已得到证实。…

智慧宿舍平台|基于Springboot+vue的智慧宿舍系统(源码+数据库+文档)

智慧宿舍系统 目录 基于Springbootvue的智慧宿舍系统 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取 博主介绍&#xff1a;✌️大厂码农|毕设布道师&#xff0c;阿里云开发社区乘风者…

FPGA基本结构和简单原理

前言&#xff1a; FPGA全程为&#xff08;Field Programmable Gate Array&#xff09;现场可编程逻辑阵列&#xff0c;以基本的逻辑为主可以实现大多数芯片可以实现的功能&#xff0c;比如说&#xff1a;ASIC芯片等&#xff0c;在半导体领域有着重要的作用。 本文…

3. 轴指令(omron 机器自动化控制器)——>MC_MoveAbsolute

机器自动化控制器——第三章 轴指令 4 MC_MoveAbsolute变量▶输入变量▶输入输出变量▶输入输出变量 功能说明▶指令详情▶时序图▶重启运动指令▶多重启动运动指令▶异常 示例程序1▶参数设定▶动作示例▶梯形图▶结构文本(ST) 示例程序2▶参数设定▶动作示例▶梯形图▶结构文…

【智路】智路OS 应用开发

1 总览 1.1 功能 智路OS的应用基于框架提供的服务数据&#xff0c;实现场景理解、指标计算、V2X核心功能实现&#xff0c;应用包括但不限于&#xff1a; V2X应用场景实现交通事件检测交通流指标计算系统监控路径规划控制 开发者可以基于智路OS框架和数据&#xff0c;实现自…

【CSS in Depth 2 精译_030】5.2 Grid 网格布局中的网格结构剖析(下)

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一章 层叠、优先级与继承&#xff08;已完结&#xff09; 1.1 层叠1.2 继承1.3 特殊值1.4 简写属性1.5 CSS 渐进式增强技术1.6 本章小结 第二章 相对单位&#xff08;已完结&#xff09; 2.1 相对…

Python编程 - 线程

目录 前言 一、线程的使用 &#xff08;一&#xff09;基础使用 &#xff08;二&#xff09;等待线程完成 &#xff08;三&#xff09;多个线程 &#xff08;四&#xff09;守护线程 &#xff08;五&#xff09;线程同步 &#xff08;六&#xff09;总结 二、队列对象 …

编译成功!QT/6.7.2/Creator编译Windows64 MySQL驱动

找了很多编译博文&#xff0c;都错误。最后找到了正确编译办法&#xff1a; https://zhuanlan.zhihu.com/p/567883165 直接下载编译好的源码及dll文件 https://download.csdn.net/download/quantum7/89760587 安装VS Virsual Studio 2022 Community D:\Program Files\Micr…

Double Write

优质博文&#xff1a;IT-BLOG-CN 一、存在的问题 为什么需要Double Write&#xff1a; InnoDB的PageSize是16kb&#xff0c;其数据校验也是针对这16KB来计算的&#xff0c;将数据写入磁盘是以Page为单位的进行操作的。而计算机硬件和操作系统&#xff0c;写文件是以4KB作为基…

Spark Streaming基础概论

1. 简介 1.1 什么是 Spark Streaming&#xff1f; Spark Streaming 是 Apache Spark 的一个扩展模块&#xff0c;专门用于处理实时数据流。它通过将数据流切分为一系列小批次&#xff08;微批次&#xff09;进行处理&#xff0c;使得开发者能够使用与批处理相同的 API 来处理…

免费还超快,我用 Cursor 做的“汉语新解”插件开源了

前两天,你是否也被 汉语新解 卡片刷屏,却苦恼于无法快速生成? 记得当时,微信群里、朋友圈里、某书上以及公众号里,到处都在谈论这些生动有趣的“汉语新解”卡片。 这是由提示词大神 @李继刚老师 在 Claude 3.5 上开发的提示词。其辛辣的风格和全新的视角,令人耳目一新。…

SpringBoot+vue集成sm国密加密解密

文章目录 前言认识SM2后端工具类实现引入依赖代码实现工具类&#xff1a;SM2Util 单元测试案例1&#xff1a;生成服务端公钥、私钥&#xff0c;前端js公钥、私钥案例2&#xff1a;客户端加密&#xff0c;服务端完成解密案例3&#xff1a;服务端进行加密&#xff08;可用于后面前…