分类 golang 下的文章

在 Go 语言中,协程是轻量级的用户态线程,也称为 goroutine,运行在操作系统的线程之上,是 Go 语言的核心特性之一。协程的调度是由 Go 语言运行时(runtime)实现的,下面是协程调度的基本流程:

当一个 goroutine 被创建时,它的执行是由主 goroutine 控制的。

如果该 goroutine 遇到了一个 I/O 操作、系统调用、时间延迟等需要阻塞的事件,则该 goroutine 会将自己从内部的队列中移除,直到该事件完成。

运行时系统会从运行队列中调度一个可运行的 goroutine 继续执行。

当一个 goroutine 执行完毕时,它也会自动从队列中退出。

如果主 goroutine 卡住了,运行时系统会尝试捕获 panic 以便程序能够正常退出。

当所有 goroutine 都结束并且 main 函数退出后,程序退出。

总的来说,Go 语言中的协程调度采用了 M:N 模型,即运行时系统会将多个 goroutine 映射到少量的操作系统线程上进行执行。这种方式有效地利用了多处理器系统的资源,并且可以轻松地处理大量的并发请求,提高了程序的执行效率和性能。

在普通的编程中,编写并发程序时需要特别注意并发安全性,以避免数据竞争和其他并发问题。在Go语言中,下面是一些可能会出现并发不安全问题的情况:

共享变量:如果多个 goroutine 对一个共享变量进行读写,而至少有一个 goroutine 执行写操作时,就有可能会出现并发问题。为了避免这种情况,可以使用 sync 包或通道来同步访问共享变量。

非原子操作:如果一个变量的更新操作不是原子的,就可能会导致并发问题。例如对于 32 位的整数类型,在一个 goroutine 更新该变量的高 16 位,而在另一个 goroutine 更新低 16 位时,就有可能出现错误结果。为了避免这种情况,可以使用原子操作进行变量更新。

无序写入:如果多个 goroutine 向同一个切片(slice)同时写入数据,而没有采取任何措施来同步访问该切片,就有可能出现并发问题。为了避免这种情况,可以使用 sync 包或通道来同步访问共享数据。

共享 map:如果多个 goroutine 对同一个 map 进行读写操作,而至少有一个 goroutine 执行写操作时,就可能会出现并发问题。为了避免这种情况,可以使用 sync 包或者采用只读方式访问 map。

总的来说,Go语言具有强大的并发性能,而并发不安全问题主要是由于多个 goroutine 访问共享数据而引起的。开发人员在编写并发代码的同时,应该采取一些正确地同步机制,保证每个共享变量的访问被合理的同步以避免并发不安全问题。

func (db *DB) Unscoped() (tx *DB) {
    tx = db.getInstance()
    tx.Statement.Unscoped = true
    return
}

Unscoped办法设置tx.Statement.Unscoped为true

gorm的Select、Delete方法都是软删除,Select会自动筛选delete_at为空的记录,Delete会更新delete_at。

如果需要获取已删除的记录,可以tx.Unscoped().Select()。
如果需要物理删除,可以tx.Unscoped().Delete().

先上源码:

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
翻译过来:
Growslice处理附加过程中的切片增长。  
//传递slice元素类型、旧slice和所需的新最小容量,  
//它返回一个至少有相同容量的新片,包含旧的数据  
//复制到它里面。  
//新切片的长度设置为旧切片的长度,  
//没有到新的请求容量。  
//这是为了方便代码生成。 旧切片的长度立即被使用  
//计算在追加过程中写入新值的位置。  
// TODO:当旧的后端消失时,重新考虑这个决定。  
// SSA后端可能更喜欢新的长度,或者只返回ptr/cap,以节省堆栈空间。

func growslice(et *_type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc()
        racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }

    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }

    if et.size == 0 {
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve old.array in this case.
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.cap < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
//检查0 < newcap检测溢出  
//和防止无限循环。 
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
//将newcap设置为请求的上限
//新上限计算溢出。
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    var overflow bool
    var lenmem, newlenmem, capmem uintptr
    // Specialize for common values of et.size.
    // For 1 we don't need any division/multiplication.
    // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
    // For powers of 2, use a variable shift.
//特化et.size的公共值。  
//对于1,我们不需要任何除法/乘法。  
//对于sys。 PtrSize,编译器会将除法/乘法优化为一个常数的移位。  
//对于2的幂,使用变量shift。 
    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == sys.PtrSize:
        lenmem = uintptr(old.len) * sys.PtrSize
        newlenmem = uintptr(cap) * sys.PtrSize
        capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
        newcap = int(capmem / sys.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if sys.PtrSize == 8 {
            // Mask shift for better code generation.
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }

    // The check of overflow in addition to capmem > maxAlloc is needed
    // to prevent an overflow which can be used to trigger a segfault
    // on 32bit architectures with this example program:
    //
    // type T [1<<27 + 1]int64
    //
    // var d T
    // var s []T
    //
    // func main() {
    //   s = append(s, d, d, d, d)
    //   print(len(s), "\n")
    // }
    if overflow || capmem > maxAlloc {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nil, false)
        // The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
        // Only clear the part that will not be overwritten.
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
        p = mallocgc(capmem, et, true)
        if lenmem > 0 && writeBarrier.enabled {
            // Only shade the pointers in old.array since we know the destination slice p
            // only contains nil pointers because it has been cleared during alloc.
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
        }
    }
    memmove(p, old.array, lenmem)

    return slice{p, old.len, newcap}
}


<!--more-->

扩容策略看其中这几行代码:

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.cap < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
  • 若新cap大于原cap的2倍,则新cap就是所需的cap。
  • 若新cap小于等于原cap的2倍:

    • 若原cap小于1024个字节,新cap为2倍的原cap
    • 若原cap大于等于1024字节,新cap为原cap的1.25倍;若溢出了,则为原cap的2倍。

- 阅读剩余部分 -

进程

进程是系统分配资源和调度的基本单位。一个应用程序为1个进程。地址独立。

内存管理

进程在内存主要分为5个区:
1.代码区 (.text) - 存放函数体的二进制代码
2.文字常量区 (.rodata) - 存放常量字符串
3.静态区 (static) - 存放全局变量、静态变量
4.堆区 (heap) - 开发者手动分配的内存空间,结构类似链表
5.栈去 (stack) - 存放函数参数、局部变量。由编译器自动管理,结构类似栈。

线程

线程是cpu调度的最小单位,一个进程内的线程间资源共享。
线程是由系统内核提供的服务,用户通过系统调用让内核启动线程,内核负责线程的调用和切换。

协程

协程是go自己管理的线程,比系统线程开销更少,速度更快。

1.context
2.channel
3.map
4.gmp模型
5.用多个协程交替打印abc
6.多个协程交替打印字符串和数字数组,直到字符串结束。
7.gin.Context
8.协程调度和线程有什么区别
9.sync.Map
10.sync.Mutex
11.sync.RwMutex
12.sync.WaitGroup
13.gc原理

1.context

Context可以控制一组树状结构的goroutine,相比于waitgroup,Context对派生的goroutine比waitgroup有着更强的控制能力。waitgroup适用于确定数量的goroutine,未知数量的goroutine,可采用context控制并发。Context可设置父子关系,父关闭,子也关闭。同时支持延时关闭和超时关闭。

2.channel

channel(通道)是go自带的、且唯一一个并发安全的类型。
一个通道相当于一个FIFO队列。

注意事项

1.向一个已关闭的通道发送操作,会引发panic。
2.试图关闭一个已经关闭的通道也会引发panic。

7.gin

8.协程、线程

9.sync.Map

引入原因

map类型不是并发安全的,并发读写会报fatal error

fatal error: concurrent map read and map write

case:

var testMap  = map[string]string{}
func main() {
   go func() {
      for{
         _ = testMap["bar"]
      }
   }()
   go func() {
      for  {
         testMap["bar"] = "foo"
      }
   }()
   select{}
}

map为何会出现并发异常

go通过flags的hashWriting字段来检测map是否并发异常。
查询操作
flags.hashWriting > 0,则抛出异常。
写操作
1.写入前检查一次标记位,通过后打上标记
2.写入完成后再检查标记位,通过后再打上标记


   //各类前置操作
   ....
   if h.flags&amp;hashWriting != 0 {
      //检查是否存在并发
      throw("concurrent map writes")
   }

   //赋值标记位
   h.flags ^= hashWriting
   ....
   //后续操作
  done:
   //完成修改后,再次检查标记位
   if h.flags&hashWriting == 0 {
      throw("concurrent map writes")
   }
   //还原标记位取消hashWriting标记
   h.flags &^= hashWriting

如何解决map并发问题

1.使用sync.RwMutex

type cocurrentMap = struct {
   sync.RWMutex
   m map[string]string
}

func main() {
   var testMap = &cocurrentMap{m:make(map[string]string)}
   //写
   testMap.Lock()
   testMap.m["a"] = "foo"
   testMap.Unlock()
   //读
   testMap.RLock()
   fmt.Println(testMap.m["a"])
   testMap.RUnlock()
}

由于锁开销较大,对并发量有影响,所以推荐使用sync.Map

2.sync.Map

sync.Map的实现

空间换时间思想,同时维护两份数据,readonly&dirty,read用来避免读写冲突。
结构如下:

type Map struct {
   mu Mutex //锁
   read atomic.Value //readOnly
   dirty map[interface{}]*entry //*entry
   misses int
}

type readOnly struct {
   m       map[interface{}]*entry
   amended bool // true if the dirty map contains some key not in m.
}

type entry struct {
   p unsafe.Pointer // *interface{}
}

case:

var m sync.Map
//write
m.Store("test", 1)
m.Store(1, true)

//read
val1, _ := m.Load("test")
val2, _ := m.Load(1)
fmt.Println(val1.(int))
fmt.Println(val2.(bool))

//遍历
m.Range(func(key, value interface{}) bool {
   //....
   return true
})

//删除
m.Delete("test")

//读取或写入
m.LoadOrStore("test", 1)

10.sync.Mutex


11.sync.RwMutex


12.sync.WaitGroup


13.GC原理

转自 https://mp.weixin.qq.com/s/niLk_n9Yp-iyl_RIie3Umw

在Go语言中,‌chan(‌通道)‌是一种用于在goroutines之间进行通信的机制。‌chan可以定义为以下几种类型:‌

  1. 不带缓冲的通道:‌这种通道在写入数据时,‌如果接收方没有准备好读取数据,‌写入操作会阻塞,‌直到有接收方准备好读取数据为止。‌不带缓冲的通道确保了数据的同步传输。‌
  2. 带缓冲的通道:‌带缓冲的通道允许在通道中存储一定数量的数据,‌每次向通道中写入数据时,‌如果通道未满,‌则写入操作会立即完成;‌当通道已满时,‌写入操作会阻塞,‌直到有数据被读取出来。‌带缓冲的通道提供了一定程度的异步通信能力,‌允许发送方和接收方在不同的时间点进行操作。‌

定义chan时,‌需要指定数据类型,‌只允许这个指定数据类型的变量通过这个通道。‌例如,‌可以定义一个整数类型的通道var intChan chan int,‌或者一个可以存储任意类型的通道var anyChan chan interface{}。‌后者特别有用,‌因为它允许在通道中传递任何类型的值,‌但需要注意的是,‌使用interface{}类型会带来一些类型安全的考虑,‌因为运行时类型检查可能会增加代码的复杂性。‌

通道的操作包括使用<-操作符进行数据的发送或读取,‌以及使用close函数关闭通道。‌关闭通道是一种重要的操作,‌用于指示通道不再发送任何数据,‌这有助于防止内存泄漏和错误地使用已关闭的通道.

人工回收

c语言由开发者来手动分配与回收内存,调用malloc或calloc分配内存,调用free清空这块内存。
手动回收会存在内存泄漏问题,即分配后没有释放。


- 阅读剩余部分 -