【Go】fatal error: concurrent map writes
并发读写map,可能会报fatal。
并发读写map,可能会报fatal。
先上源码:
// 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倍:
gin 1.7.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原理
Context可以控制一组树状结构的goroutine,相比于waitgroup,Context对派生的goroutine比waitgroup有着更强的控制能力。waitgroup适用于确定数量的goroutine,未知数量的goroutine,可采用context控制并发。Context可设置父子关系,父关闭,子也关闭。同时支持延时关闭和超时关闭。
channel(通道)是go自带的、且唯一一个并发安全的类型。
一个通道相当于一个FIFO队列。
1.向一个已关闭的通道发送操作,会引发panic。
2.试图关闭一个已经关闭的通道也会引发panic。
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{}
}
go通过flags的hashWriting
字段来检测map是否并发异常。
查询操作:flags.hashWriting
> 0,则抛出异常。
写操作:
1.写入前检查一次标记位,通过后打上标记
2.写入完成后再检查标记位,通过后再打上标记
//各类前置操作
....
if h.flags&hashWriting != 0 {
//检查是否存在并发
throw("concurrent map writes")
}
//赋值标记位
h.flags ^= hashWriting
....
//后续操作
done:
//完成修改后,再次检查标记位
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
//还原标记位取消hashWriting标记
h.flags &^= hashWriting
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
空间换时间思想,同时维护两份数据,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)
在Go语言中,chan(通道)是一种用于在goroutines之间进行通信的机制。chan可以定义为以下几种类型:
定义chan时,需要指定数据类型,只允许这个指定数据类型的变量通过这个通道。例如,可以定义一个整数类型的通道var intChan chan int,或者一个可以存储任意类型的通道var anyChan chan interface{}。后者特别有用,因为它允许在通道中传递任何类型的值,但需要注意的是,使用interface{}类型会带来一些类型安全的考虑,因为运行时类型检查可能会增加代码的复杂性。
通道的操作包括使用<-操作符进行数据的发送或读取,以及使用close函数关闭通道。关闭通道是一种重要的操作,用于指示通道不再发送任何数据,这有助于防止内存泄漏和错误地使用已关闭的通道.
number, _ := strconv.Atoi(str) //number 为int类型
str := string(rune(number))
int64, err := strconv.ParseInt(string, 10, 64)
string:=strconv.FormatInt(int64,10)
package main
import s "strings"
import "fmt"
var p = fmt.Println
func main() {
p("Contains: ", s.Contains("test", "es")) //是否包含 true
p("Count: ", s.Count("test", "t")) //字符串出现字符的次数 2
p("HasPrefix: ", s.HasPrefix("test", "te")) //判断字符串首部 true
p("HasSuffix: ", s.HasSuffix("test", "st")) //判断字符串结尾 true
p("Index: ", s.Index("test", "e")) //查询字符串位置 1
p("Join: ", s.Join([]string{"a", "b"}, "-"))//字符串数组 连接 a-b
p("Repeat: ", s.Repeat("a", 5)) //重复一个字符串 aaaaa
p("Replace: ", s.Replace("foo", "o", "0", -1)) //字符串替换 指定起始位置为小于0,则全部替换 f00
p("Replace: ", s.Replace("foo", "o", "0", 1)) //字符串替换 指定起始位置1 f0o
p("Split: ", s.Split("a-b-c-d-e", "-")) //字符串切割 [a b c d e]
p("ToLower: ", s.ToLower("TEST")) //字符串 小写转换 test
p("ToUpper: ", s.ToUpper("test")) //字符串 大写转换 TEST
p("Len: ", len("hello")) //字符串长度
p("Char:", "hello"[1]) //标取字符串中的字符,类型为byte
}
go向上取整方法 math.ceil(),该方法接收一个浮点型(float64)参数,如果小数不为0则会将小数舍去,小数点前数字加一。
如果除数和被除数都是int类型,需要手动转一下类型。
错误写法
batch := int(math.Ceil(float64(count / BATCH)))
正确写法
batch := int(math.Ceil(float64(count) / BATCH))