首页
留言
友情链接
壁纸
更多
关于
Search
1
dockerfile怎么执行shell脚本(利用tail -f /dev/null命令防止container启动后退出)
5,016 阅读
2
channel常见的异常总结
4,276 阅读
3
支付宝支付配置开发流程
1,431 阅读
4
HTTP 协议中的Content-Encoding
1,286 阅读
5
Laravel底层原理(二) —— 契约(Contracts)
950 阅读
PHP
composer
laravel
swoole
Docker
Linux
Go
随笔
mysql
nginx
Search
标签搜索
kratos
golang
微服务
gopher
PHP
Docker
dockerfile
通道
go
defer
alipay
支付
git
phpstorm
IDEA
web安全
漏洞
socket
设计原则
依赖注入
Royal
累计撰写
51
篇文章
累计收到
0
条评论
首页
栏目
PHP
composer
laravel
swoole
Docker
Linux
Go
随笔
mysql
nginx
页面
留言
友情链接
壁纸
关于
搜索到
51
篇与
Royal
的结果
2023-07-25
Golang的特性五(堆内存和栈内存)----内存管理
一. 内存管理概述 内存管理是编程语言设计中至关重要的一环,它直接影响着程序的性能、稳定性和开发效率。Go 语言作为一门现代化的编程语言,在内存管理方面做了许多精妙的设计,使其在高效、安全和易用性之间取得了良好的平衡。内存管理一般包含三个不同的组件,分别是用户程序(Mutator)、分配器(Allocator)和收集器(Collector),当用户程序申请内存时,它会通过内存分配器申请新内存,而分配器会负责从堆中初始化相应的内存区域。在了解内管管理之前,我们先了解下什么是堆内存?什么是栈内存?什么是堆?什么是栈?二. 堆和栈:程序运行时的内存空间 在计算机科学中,堆(Heap) 和 栈(Stack) 是程序运行时用于存储数据的两种重要内存区域。它们有着不同的特性和用途,理解它们的区别对于编写高效、可靠的程序至关重要。1、 栈 (Stack) 栈是一种遵循 后进先出 (LIFO) 原则的数据结构,类似于一摞盘子,最后放上去的盘子会被最先拿走。 2、堆 (Heap) 堆是一种用于动态内存分配的内存区域,程序可以在运行时从堆中申请和释放内存。堆和栈的比较特性栈堆分配方式编译器自动分配手动分配 (new/malloc)释放方式编译器自动释放手动释放 (delete/free)效率高低空间小大线程安全线程私有线程共享生命周期与函数调用绑定灵活三. 内存分区 在 Go 语言中,程序运行时的内存空间主要分为以下几个部分:栈内存 (Stack) : 用于存储局部变量、函数参数、函数调用的返回地址等。每个 goroutine 都有自己的栈空间,栈内存的分配和释放由编译器自动完成,效率极高。堆内存 (Heap): 用于存储动态分配的数据,例如使用 new 或 make 创建的对象。堆内存是所有 goroutine 共享的,由垃圾回收器负责管理。全局区: 用于存储全局变量、常量等。 代码区: 用于存储程序的二进制代码。四. 栈内存详解 栈内存的特点高效: 栈内存的分配和释放由编译器自动完成,只需要移动栈顶指针即可,效率极高。线程私有: 每个 goroutine 都有自己的栈空间,栈内存是线程私有的,不需要加锁保护。空间有限: 栈内存的空间有限,通常只有几 MB 大小。栈内存的分配 当一个函数被调用时,编译器会为该函数的局部变量和参数在栈上分配内存空间。函数返回时,这些内存空间会被自动释放。 栈内存的优缺点 优点分配和释放效率高。无需手动管理,由编译器自动完成。线程私有,无需加锁保护。缺点空间有限,不适合存储大量数据。生命周期与函数调用绑定,不够灵活。五. 堆内存详解 堆内存的特点动态分配: 堆内存的空间是动态分配的,可以根据需要申请和释放。空间较大: 堆内存的空间通常比栈内存大得多,可以存储大量数据。线程共享: 堆内存是所有 goroutine 共享的,需要加锁保护。堆内存的分配new: 用于分配值类型的内存,例如 int, float64, struct 等。make: 用于分配引用类型的内存,例如 slice, map, channel 等。堆内存的释放 堆内存的释放由垃圾回收器负责管理。垃圾回收器会定期扫描堆内存,回收不再使用的对象。堆内存的优缺点 优点:空间较大,可以存储大量数据。生命周期灵活,不受函数调用限制。缺点:分配和释放效率比栈内存低。需要手动管理,容易造成内存泄漏。线程共享,需要加锁保护。六. 逃逸分析 Go 语言的编译器会进行逃逸分析,确定变量是分配在栈上还是堆上。如果一个变量在函数返回后仍然被引用,则它会被分配到堆上,否则会被分配到栈上。逃逸分析可以帮助编译器优化代码,减少堆内存的分配,提高程序性能。七. Go 内存管理组件 Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件:runtime.mspanruntime.mcacheruntime.mcentralruntime.mheap八. 内存管理思想 Go 内存管理核心思想可以分为以下几点:每次从操作系统申请一大块儿的内存,由 Go 对这块儿内存做分配,减少系统调用。内存分配借鉴了 Google 的 TCMalloc(Thead-Caching Malloc)算法。TCMalloc 是由 Google 开发的一种内存分配器,主要用于优化多线程环境下的内存分配和释放性能。TCMalloc 是Thread-Caching Malloc 的缩写,即线程缓存分配器。TCMalloc 比 glibc 中的 malloc 还要快很多。Go 的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心思想是使用多级缓存并将对象根据大小分类,按照类别实施不同的分配策略。TCMalloc 中将内存分成三类,即小对象,小于256K的,中型对象,介于256K到1M的,大于1M的为大对象。TCMalloc 不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,分为线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)。TCMalloc 的核心思想是:(1)内存切分,减少碎片。采用了 span 机制来减少内存碎片。多个连续的内存页(8KB)组成 span,每个 span 又划分成大小固定的多个 slot。slot size 有 67 种,每个 size 有两种类型,scan 和 noscan,表示分配的对象是否包含指针。(2)分级管理,无锁并降低锁的粒度。多层次的分配 Cache,每个 P 上有一个 mcache,mcache 会为每个 size 最多缓存一个 span,用于无锁分配。全局每个 size 的 span 都有一个 mcentral,锁的粒度相对于全局的 mheap 小很多。每个 mcentral 可以看成是每个 size 的 span 的一个全局后备 cache。获取不到再上升到全局的 mheap。mheap 获取不到再向系统申请。从无锁到全局 1/(67*2)力度的锁,再到全局锁,再到系统调用。(3)回收复用内存由 GC 进行释放。回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,sysmon 协程会定时把 mheap 空余的内存归还给操作系统,降低整体开销。九. 其他内存分配方法除了上面讲的TCMalloc内存方法还有其他两种内存分配方法,一种是线性分配器,另一种是空闲链表分配器。 线性分配器 线性分配器(Bump Allocator)是一种高效的内存分配方法,但是有较大的局限性。当我们使用线性分配器时,只需要在内存中维护一个指向内存特定位置的指针,如果用户程序向分配器申请内存,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针:空闲链表分配器 空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表:
2023年07月25日
8 阅读
0 评论
0 点赞
2023-07-24
Golang的特性四(Garbage Collection, GC)----垃圾回收机制详解
一. 垃圾回收概述 在编程世界中,内存管理一直是一个重要且复杂的话题。手动管理内存,如 C/C++ 中的 malloc/free 或 new/delete,虽然灵活高效,但也容易带来内存泄漏、悬空指针等问题,增加开发难度和风险。为了解决这些问题,垃圾回收(Garbage Collection, GC) 应运而生。首先,Golang的垃圾回收机制并不像C语言那样让我们自己手动管理内存,也不需要像C++那样通过智能指针来控制内存的生命周期。它自带的垃圾回收机制(GC)会自动帮我们清理那些不再使用的内存,避免了内存泄漏的麻烦。二. go语言垃圾回收机制详解 Go 语言的垃圾回收机制是一个并发的、三色的、标记-清除(mark-sweep)垃圾回收器。它主要包含以下几个关键点: 并发 : Go 的垃圾回收器与用户程序并发运行,只在必要时进行短暂的停止(STW, Stop The World),最大限度地减少对程序性能的影响。三色标记 : Go 使用三色标记法来追踪对象之间的引用关系,将对象标记为白色、灰色或黑色,以确定哪些对象可以被回收。标记-清除 : Go 的垃圾回收器采用标记-清除算法,首先标记所有可达对象,然后清除所有未被标记的对象,释放其占用的内存。三. 垃圾回收机制详解 三色标记法三色标记法是一种基于追踪的垃圾回收算法,它将内存中的对象分为三种颜色:白色 : 表示对象尚未被垃圾回收器访问过,可能是垃圾。灰色 : 表示对象已经被垃圾回收器访问过,但其引用的对象还没有被访问。黑色 : 表示对象已经被垃圾回收器访问过,并且其引用的对象也已经被访问。垃圾回收过程如下:初始阶段 : 将所有对象标记为白色。标记阶段 : 从根对象(如全局变量、栈上的变量等)开始,递归地遍历所有可达对象,将其标记为灰色,然后逐步将其标记为黑色。清除阶段 : 所有仍然为白色的对象都被视为垃圾,将其回收。写屏障由于 Go 的垃圾回收器与用户程序并发运行,在标记阶段,用户程序可能会修改对象的引用关系,导致错误地回收仍然被引用的对象。为了解决这个问题,Go 引入了写屏障(write barrier) 机制。写屏障会在用户程序修改指针时,将新引用的对象标记为灰色,确保垃圾回收器能够正确地追踪所有可达对象。GC 触发时机Go 的垃圾回收器并不是一直运行的,它会在以下情况下触发: 堆内存达到一定阈值 : Go 会根据当前堆内存的使用情况动态调整 GC 触发阈值。手动触发 : 可以通过调用 runtime.GC() 函数手动触发 GC。四. 其他垃圾回收机制 除了 Go 语言使用的标记-清除算法,还有其他一些常见的垃圾回收算法,例如:引用计数 : 每个对象维护一个引用计数器,当引用计数为 0 时,对象被回收。Python 使用了引用计数机制。分代收集 : 将对象按照存活时间分为不同的代,对不同代采用不同的回收策略。Java 使用了分代收集机制。五.总结 Go 语言的垃圾回收机制是其语言设计的一大亮点,它通过并发、三色标记、写屏障等技术,实现了高效、低延迟的自动内存管理,极大地简化了开发者的工作,提高了程序的稳定性和可靠性。
2023年07月24日
8 阅读
0 评论
0 点赞
2023-07-23
Golang的特性三(互斥锁)----sync.Mutex包的用法
1. sync.Mutex 的实现原理概念sync.Mutex 是 Go 语言标准库中用于实现互斥锁的类型,用于保护共享资源,确保在同一时刻只有一个 goroutine 可以访问被保护的资源。sync.Mutex是一个结构体对象,适用于读写不确定的场景(即读写次数没有明显区别,且只允许有一个读或写的场景),该锁也称为全局锁。内部状态 - sync.Mutex 内部有一个 state 字段,用于表示锁的状态。这个状态使用位操作来表示不同的信息,包括锁是否被持有、是否有等待的 goroutine 等。 - 例如,低 1 位可能表示锁是否被持有,其他位可能表示等待队列的信息。锁定操作(Lock 函数) - 当调用 Lock 函数时,会尝试将 state 置为锁定状态。如果锁已经被其他 goroutine 持有,当前 goroutine 会进入等待状态,可能会被阻塞。 - 这通常涉及到自旋等待和排队等待两种机制。 - 自旋等待:在某些情况下,sync.Mutex 会先进行一定次数的自旋等待,尝试快速获取锁,避免立即进入阻塞状态,以提高性能。 - 排队等待:如果自旋等待失败,会将当前 goroutine 加入到等待队列中,等待唤醒。解锁操作(Unlock 函数): - 当调用 Unlock 函数时,会将 state 置为未锁定状态,并唤醒等待队列中的一个 goroutine,让它可以获取锁。2. 代码示例package main import ( "fmt" "sync" "time" ) var ( mu sync.Mutex count int ) func increment() { mu.Lock() count++ mu.Unlock() } func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() increment() }() } wg.Wait() fmt.Println(count) } 代码解释:变量声明: - mu 是 sync.Mutex 类型的互斥锁,用于保护 count 变量。 - count 是一个共享变量,多个 goroutine 会对其进行操作。increment 函数: - 在 increment 函数中,首先调用 mu.Lock() 来获取锁,确保在操作 count 时不会有其他 goroutine 同时操作。 - 然后对 count 进行加 1 操作。 - 最后调用 mu.Unlock() 释放锁,让其他 goroutine 可以获取锁并操作 count。main 函数: - 使用 sync.WaitGroup 来等待所有的 goroutine 完成。 - 启动 1000 个 goroutine,每个 goroutine 调用 increment 函数 总结通过以上代码运行我们发现当加了互斥锁之后,代码每次都会输出1000,这是因为无论我们会开多少个协程count值都会依次加1直到循环完成,而且每个groutine必须等锁释放之后才能操作count值,所以值就是循环的次数。当我们把互斥锁代码注释掉后,我们会发现输出的结果值不固定而且都小于1000,这是因为多个 goroutine 同时并发地访问和修改 count,导致了数据竞争(Race Condition),在并发环境下,如果没有锁的保护,多个 goroutine 可能会同时读取 count 的旧值并进行加 1 操作,导致部分加 1 操作被覆盖,最终结果小于预期值。3. 使用 sync.Mutex 的注意事项 正确的锁操作顺序确保在修改共享资源之前获取锁,在修改完成后释放锁。避免在持有锁的情况下进行长时间的操作或可能阻塞的操作,如 I/O 操作,这可能导致其他 goroutine 长时间等待。避免死锁:确保锁的获取和释放顺序是一致的,避免出现循环等待的情况。避免在已经持有锁的情况下再次获取锁,除非使用 sync.RWMutex 的 RLock 等功能。性能考虑:对于频繁访问的共享资源,使用 sync.Mutex 可以保证数据一致性,但会带来一定的性能开销。以下是一个可能导致死锁的错误示例:func wrongLock() { mu.Lock() defer mu.Lock() // 错误:这里应该是 mu.Unlock() count++ }在这个错误示例中,defer 语句错误地再次调用了 Lock 而不是 Unlock,导致死锁。
2023年07月23日
10 阅读
0 评论
0 点赞
2023-07-21
Golang的特性二(通道)---channel的用法
1. 什么是channel? channel的中文意思就是通道。在go中channel主要是用于协程之间的通信。channel可以在同一时间保证只有一个携程在操作,从而避免了数据的竞争,实现的同步操作。2. channel使用在go中,channel需要使用make函数进行内存分配。可以使用make函数,指定缓冲区的大小。ch := make(chan int,6)如果不指定缓冲区,则成为无缓冲区通道。该通道只有sender和receiver都准备好时,才能进行通信。func pump(ch chan int) { for i := 1;;i++ { ch <- i } } ch1 := make(chan int) // 无缓冲通道 go pump(ch1) 以上程序没有receiver,程序会报错。因为通道是没有缓冲区的。当sender发送一个数据的时候,必须有receiver接收数据,这样sender才能继续发送数据。类似于生产者->消费者问题func pump(ch chan int) { for i := 1;;i++ { ch <- i } } ch1 := make(chan int) // 无缓冲通道 go pump(ch1) fmt.Println(<-ch1) // 这里只接收的到发送的第一个数据1。 发送、接收数据根据 <- 的流向,来判断是向通道发送数据,还是读取通道中的数据 ch := make(chan int,1) ch <- 10 // 向ch通道发送数据 ch = make(chan int,1) ch <- 10 x := <- ch // 读取ch通道中的数据,并复制给x变量。 注意: 不能向已经满了的通道发送数据,否则会造成阻塞。 ch = make(chan int,2) ch <- 10 ch <- 20 ch <- 30 // 通道缓冲区大小为2,已满,不能再发送数据 该错误告诉我们,所有的线程都已经休眠。则没有一个线程在运行,发生死锁。关闭通道close(ch) // 使用该方法关闭通道 只有发送者可以关闭通道。 关闭的通道,在无数据的情况下,会返回false。否则是true。读取通道中的数据for range 可以读取通道中的数据,直到通道关闭for range 例如: for v := range ch1 { fmt.Printf("%v",v) } 3. 单通道 顾名思义,就是单向的通道,也就是只能接受或者发送的通道。只能接收var ch <- chan int只能发送var ch chan <- int4. select 不同于switch,select只能用于io操作,一般配合channel使用。例如: select { case msg1 := <-c1: fmt.Println("c1 received:",msg1) case msg2 := <-c2: fmt.Println("c2 received",msg2) default: fmt.Println("No data received") } 以上例子中,只要一个通道里有数据,就会执行case。如果没有匹配的case,且没有default语句。那么select会阻塞,直到某一case可以执行与switch的主要区别:switch的case是顺序匹配执行的。而select是随机执行的。select中表达式必须是和通道有关的操作。
2023年07月21日
8 阅读
0 评论
0 点赞
2023-07-20
Golang的特性一(并发等待组)----sync.WaitGroup包的用法
1. 介绍WaitGroup包 sync.WaitGroup 用于阻塞等待一组 Go 程的结束。WaitGroup 是 Go 语言标准库 sync 包中的一个并发同步工具,它用于协调主 goroutine 与多个工作 goroutine 的执行。通过计数器跟踪还未完成的工作 goroutine 的数量,WaitGroup 能够确保主 goroutine 在所有工作 goroutine 完成之前不会继续执行。主 Go 程调用 Add() 来设置等待的 Go 程数,然后该组中的每个 Go 程都需要在运行结束时调用 Done(), 递减 WaitGroup 的 Go 程计数器 counter。当 counter 变为 0 时,主 Go 程被唤醒继续执行。type WaitGroup struct { // contains filtered or unexported fields } // 设置需要等待的 Go 程数量。 func (wg *WaitGroup) Add(delta int) // Go 程计数器减 1。 func (wg *WaitGroup) Done() // 阻塞等待所有 Go 程结束(等待 Go 程计数器变为 0)。 func (wg *WaitGroup) Wait() WaitGroup 有三个方法,其中 Done() 调用了 Add(-1)。标准用法:step 1 启动 Go 程时调用 Add()。step 2 在 Go 程结束时调用 Done()。step 3 最后调用 Wait()。完整示例代码如下:package main import ( "fmt" "sync" "time" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 确保每个goroutine在结束时调用Done() fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) // 模拟耗时任务 fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup // 启动3个goroutine,每个都执行worker函数 for i := 1; i <= 3; i++ { wg.Add(1) // 为每个goroutine增加计数器 go worker(i, &wg) // 启动一个goroutine来执行worker函数,并传递WaitGroup的地址 } wg.Wait() // 等待所有goroutine完成 fmt.Println("All workers done") }在这个示例中,我们启动了3个goroutine,每个都执行一个简单的耗时任务,并在完成后通过调用 wg.Done() 来通知主goroutine它们已经完成。主goroutine在调用 wg.Wait() 后继续执行,直到所有子goroutine都完成了它们的任务。输出结果如下:
2023年07月20日
11 阅读
0 评论
0 点赞
1
2
3
4
...
11