用golang开发系统软件的一些细节( 五 )


channelchannel当然也算一种并发容器,其本质上是无锁队列 。
需要注意两点:

  • 为了在多读多写条件下维持队列的数据结构,通常通过CAS+自旋等待来操作关键数据 。
因此在大并发下,入队出队操作是串行化的,CAS失败+自旋重试又会带来cpu使用率升高 。
同样的,channel没有那么快 。要避免在剧烈竞争的环境下使用channel 。
  • 通常会使用channel来做生产者-消费者模式的并发结构 。数据数据可以按照一定的规律分区,则可以考虑每个消费者对应一个channel,然后生产者根据数据的key来决定放到哪个channel 。这样本质上减缓了锁的竞争 。
其他用sync.Once来懒惰初始化有的运算结果,有一定概率用到,但是又不必每次都计算 。这种情况下,使用sync.Once来懒惰初始化是个好办法:
var once sync.Oncevar globalXXX *XXXfunc GetXXX() *XXX{  once.Do(func(){    globalXXX = getXXX()  })  return globalXXX}不安全代码string与[]byte的转换string与slice的结构本质上是一样的,可以直接强制转换:
import ( "reflect" "unsafe")// copy from prometheus source code// NoAllocString convert []byte to stringfunc NoAllocString(bytes []byte) string { return *(*string)(unsafe.Pointer(&bytes))}// NoAllocBytes convert string to []bytefunc NoAllocBytes(s string) []byte { strHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) sliceHeader := reflect.SliceHeader{Data: strHeader.Data, Len: strHeader.Len, Cap: strHeader.Len} return *(*[]byte)(unsafe.Pointer(&sliceHeader))}上面的代码可以避免string和[]byte在转换的时候发生拷贝 。
注意:转换后的对象一定要立即使用,不要进一步引用到更深的层次中去 。牢记这是不安全代码,谨慎使用 。强制类型转换懂C的人,请绕过……
例如一个[]int64的数组要转换为[]uint64的数组,使用个指针强制转换就行了 。
package mainimport ( "testing" "unsafe")func TestConvert(t *testing.T) { int64Slice := make([]int64, 0, 100) int64Slice = append(int64Slice, 1, 2, 3) uint64Slice := *(*[]uint64)(unsafe.Pointer(&int64Slice)) t.Logf("%+v", uint64Slice)}还有一种使用场景,要比较两个大数组是否完全一样:可以把数组强制转换为[]byte,然后使用bytes.Compare() 。相当于C中的memcmp()函数 。
类似的操作还很多,推荐这篇文章:《深度解密Go语言之unsafe》
模糊记得一个golang(或是rust)的原则:普通开发者可以使用安全代码来无顾虑的使用,高手把不安全代码包装成安全代码来提供高性能组件 。数组越界检查的开销相比C的数组访问,为什么golang可以做到很安全?
答案是编译器加了两条越界检查的指令 。每次通过下标访问数组,就像这样:
if index<0 || index>=len(slice){  panic("out of index")}return slice[index]这两条越界检查指令是有开销的,请看我的测试:《golang中数组边界检查的开销大约是1.87%~3.12%》
所以,当某些位置使用类似查表法的时候,可以用不安全代码绕过越界检查:
slice := make([]byte, 1024*1024)offset = 100b := (*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + uintptr(offset))))编译/链接阶段使用尽量新的golang版本理论上,每个新版的golang,都有一定编译器优化的提升 。

经验总结扩展阅读