Golang并发操作入门

sync.Mutex与sync.RWMutex

保证多个并发线程对共享资源的访问是串行的,否则很容易出现争用和冲突的情况,这时需要使用互斥量来保证在同一时刻只有一个goroutine访问共享资源,其中sync.Mutex与sync.RWMutex类型就是互斥量,也称互斥锁,

当有goroutine进入临界区时,我们对他进行锁定,当他离开时我们进行解锁操作,锁定操作可以用Lock方法实现,解锁操作可以用Unlock方法

var mu sync.Mutex //定义互斥锁
mu.Lock()

...             //临界区  

mu.Unlock()

1:不要重复锁定互斥锁

2:不要忘记解锁,适当使用defer语句

3:不要在多个函数传递互斥锁 (因为值传递时会产生副本,副本也是独立的互斥锁)

4:不要对未锁定的互斥锁解锁

sync.RWMutex就是读写互斥锁,比起sync.Mutex更加细致,读操作和写操作分别保护,多个写操作不能同时进行,读操作和写操作也不可以同时进行,但是多个读操作可以同时进行。

1:写锁已经锁定时,试图锁定读锁或者锁定写锁都会阻塞当前goroutine

2:读锁已经锁定时,试图锁定写锁时会阻塞goroutine

读锁写锁的锁定解锁分别用RLock(),RUnlock(),Lock(),Unlock().

sync.Cond

条件变量是基于互斥锁的,有互斥锁才能发挥作用,但是条件变量的主要作用并不是保护共享资源的,是用来通知给别的goroutine共享资源的状态。

条件变量的方法:wait(等待通知),signal(单发通知),broadcast(广发通知)

sync.Cond类型需要初始化才可以使用,传入的值需要是sync.Locker类型的参数,sync.Locker是一个接口,实现的方法为Lock(),UnLock(),sync.Mutex与sync.RWMutex都拥有这两个方法,但是是指针方法,所以传入的参数也要是指针类型

代码详见:Golang_Puzzlers/demo61.go at master · hyper0x/Golang_Puzzlers (github.com)

package main

import (
	"log"
	"sync"
	"time"
)

func main() {
	// mailbox 代表信箱。
	// 0代表信箱是空的,1代表信箱是满的。
	var mailbox uint8
	// lock 代表信箱上的锁。
	var lock sync.RWMutex
	// sendCond 代表专用于发信的条件变量。
	sendCond := sync.NewCond(&lock)
	// recvCond 代表专用于收信的条件变量。
	recvCond := sync.NewCond(lock.RLocker())

	// sign 用于传递演示完成的信号。
	sign := make(chan struct{}, 3)
	max := 5
	go func(max int) { // 用于发信。
		defer func() {
			sign <- struct{}{}
		}()
		for i := 1; i <= max; i++ {
			time.Sleep(time.Millisecond * 500)
			lock.Lock()
			for mailbox == 1 {
				sendCond.Wait()
			}
			log.Printf("sender [%d]: the mailbox is empty.", i)
			mailbox = 1
			log.Printf("sender [%d]: the letter has been sent.", i)
			lock.Unlock()
			recvCond.Signal()
		}
	}(max)
	go func(max int) { // 用于收信。
		defer func() {
			sign <- struct{}{}
		}()
		for j := 1; j <= max; j++ {
			time.Sleep(time.Millisecond * 500)
			lock.RLock()
			for mailbox == 0 {
				recvCond.Wait()
			}
			log.Printf("receiver [%d]: the mailbox is full.", j)
			mailbox = 0
			log.Printf("receiver [%d]: the letter has been received.", j)
			lock.RUnlock()
			sendCond.Signal()
		}
	}(max)

	<-sign
	<-sign
}

wait方法具体操作:

1:把当前goroutine加入到条件变量等待队列

2:把当前条件变量基于的锁解锁

3:收到通知时决定是否唤醒当前等待的goroutine,

4:唤醒goroutine之后重新锁定互斥锁

Signal与Broadcast方法不同之处在于前者只会唤醒一个等待的goroutine,后者会唤醒所有等待的goroutine,Signal唤醒的goroutine一般都是最早等待的那个。这两个方法不需要互斥锁保护的时候进行,相反解锁之后进行会对程序更有利

原子操作

go语言在开启多个goroutine时,同一时刻在底层支持的数量可能不会超过核心线程数,所以调度器会频繁的运行/停止这些goroutine,所以还是会影响运行的效率,因为会被打断。真正实现原子性执行的只有原子操作,原子操作进行时不会被打断,这代表执行速度更快,不过正因为不会被打断,所以他需要更简单更快速,因此支持的只有整数和二进制位的原子操作。

在sync/atomic包中的函数提供了修改、读取、写入、交换、比较并交换

比较并交换操作,也叫CAS操作,与互斥锁不同的是,互斥锁在假设goroutine频繁更改共享资源而使用,而CAS操作恰恰相反,它往往是假设共享资源更改不频繁而使用,也被称为乐观锁。

代码举例:

var value int32
func AddValue(delta int32)  {
   for {
      v:= value
      if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
         break
      }
   }
}

由于原子操作类型过于局限,互斥锁往往更常用一些

sync/atomic.Value

它只有两个方法,Store和Load用来提供原子性的读写值操作,可以说是原子性的一个容器

注意:

1:我们不能用Store方法传入nil,会引发panic,(如果有一个接口类型的变量,它的动态值是nil,但动态类型却不是nil,那么它的值就不等于nil。这样一个变量的值是可以被存入原子值的。)

2:我们向这里存储的第一个值的类型决定了只能存储什么类型的值

还有,我们尽量不要传入引用类型的值,这样是不安全的,因为我们可以绕过原子值去修改原子值内部的引用值,比如:


var box6 atomic.Value
v6 := []int{1, 2, 3}
box6.Store(v6)
v6[1] = 4 // 注意,此处的操作不是并发安全的!

解决方法,我们可以把引用类型的值副本传入原子值


store := func(v []int) {
 replica := make([]int, len(v))
 copy(replica, v)
 box6.Store(replica)
}
store(v6)
v6[2] = 5 // 此处的操作是安全的。

sync.WaitGroup和sync.Once

sync包中的WautGroup类型更适合实现一对多的goroutine协作流程,他有三个指针方法:Add、Done、Wait

Add:一般时候用来记录需要等待的goroutine的数量,进行计数器加操作

Done:与Add相对进行计数器的减操作

wait:阻塞当前goroutine,直到计数器为0

代码对比:(Golang_Puzzlers/demo65.go at master · hyper0x/Golang_Puzzlers (github.com)

func coordinateWithChan() {
	sign := make(chan struct{}, 2)
	num := int32(0)
	fmt.Printf("The number: %d [with chan struct{}]\n", num)
	max := int32(10)
	go addNum(&num, 1, max, func() {
		sign <- struct{}{}
	})
	go addNum(&num, 2, max, func() {
		sign <- struct{}{}
	})
	<-sign
	<-sign
}

func coordinateWithWaitGroup() {
	var wg sync.WaitGroup
	wg.Add(2)
	num := int32(0)
	fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
	max := int32(10)
	go addNum(&num, 3, max, wg.Done)
	go addNum(&num, 4, max, wg.Done)
	wg.Wait()
}

// addNum 用于原子地增加numP所指的变量的值。
func addNum(numP *int32, id, max int32, deferFunc func()) {
	defer func() {
		deferFunc()
	}()
	for i := 0; ; i++ {
		currNum := atomic.LoadInt32(numP)
		if currNum >= max {
			break
		}
		newNum := currNum + 2
		time.Sleep(time.Millisecond * 200)
		if atomic.CompareAndSwapInt32(numP, currNum, newNum) {
			fmt.Printf("The number: %d [%d-%d]\n", newNum, id, i)
		} else {
			fmt.Printf("The CAS operation failed. [%d-%d]\n", id, i)
		}
	}
}

注意当WaitGroup中的计数器值小于0时会·引发panic,也不要用不同的goroutine去分别进行Add/Done和Wait,会有几率引发panic

go语言sync代码包中waitgroup_test.go部分代码,展示了异常情况的发生条件

func TestWaitGroupMisuse(t *testing.T) {
	defer func() {
		err := recover()
		if err != "sync: negative WaitGroup counter" {
			t.Fatalf("Unexpected panic: %#v", err)
		}
	}()
	wg := &WaitGroup{}
	wg.Add(1)
	wg.Done()
	wg.Done()
	t.Fatal("Should panic")
}

sync.Once类型也是开箱即用的,其中的Do方法只接受一个参数,类型必须是func(),是一个无参数声明和结果声明的函数,这个方法只会执行首次被调用时传入的函数,之后不会执行任何函数参数,Once内部包含的done字段用来判断Do方法是否调用完成,所以值只是0或1

1:Do方法会在参数函数执行完毕时把done字段置为1

2:done值的修改和读取都是原子操作,所以就算参数函数引发panic,程序也无法在用这个Once值去执行他了

sync.Pool

go语言中的临时对象池,被用来存储临时对象,可以针对数据的缓存使用,这个类型只有两个方法,Get和Put

Get:用于获取当前池中的临时对象

Put:用于存放临时对象

如果Get方法使用时,池中没有对象那么这个方法会用sync.Pool类型的New字段创建对象并返回

New的类型是func()  interface{}

New字段需要初始化对象池时就给定一个值,fmt包中就使用到了。

var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}

 有关临时对象池的清理,引用郝林老师的一段话:

sync包在被初始化的时候,会向 Go 语言运行时系统注册一个函数,这个函数的功能就是清除所有已创建的临时对象池中的值。我们可以把它称为池清理函数。
一旦池清理函数被注册到了 Go 语言运行时系统,后者在每次即将执行垃圾回收时就都会执行前者。
另外,在sync包中还有一个包级私有的全局变量。这个变量代表了当前的程序中使用的所有临时对象池的汇总,它是元素类型为*sync.Pool的切片。我们可以称之为池汇总列表。
通常,在一个临时对象池的Put方法或Get方法第一次被调用的时候,这个池就会被添加到池汇总列表中。正因为如此,池清理函数总是能访问到所有正在被真正使用的临时对象池。
更具体地说,池清理函数会遍历池汇总列表。对于其中的每一个临时对象池,它都会先将池中所有的私有临时对象和共享临时对象列表都置为nil,然后再把这个池中的所有本地池列表都销毁掉。
最后,池清理函数会把池汇总列表重置为空的切片。如此一来,这些池中存储的临时对象就全部被清除干净了。

sync.Map

用不同的goroutine操作原生字典是不安全的,所以诞生了并发安全字典

并发安全字典同样对键值类型有要求,不能是函数类型,字典类型,切片类型。我们可以用类型断言表达式或者反射来保证类型正确

引用郝林极客时间《Go语言核心三十六讲》:

代码:Golang_Puzzlers/demo72.go at master · hyper0x/Golang_Puzzlers (github.com)


type IntStrMap struct {
 m sync.Map
}

func (iMap *IntStrMap) Delete(key int) {
 iMap.m.Delete(key)
}

func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
 v, ok := iMap.m.Load(key)
 if v != nil {
  value = v.(string)
 }
 return
}

func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
 a, loaded := iMap.m.LoadOrStore(key, value)
 actual = a.(string)
 return
}

func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
 f1 := func(key, value interface{}) bool {
  return f(key.(int), value.(string))
 }
 iMap.m.Range(f1)
}

func (iMap *IntStrMap) Store(key int, value string) {
 iMap.m.Store(key, value)
}

编写了一个名为IntStrMap的结构体类型,它代表了键类型为int、值类型为string的并发安全字典。在这个结构体类型中,只有一个sync.Map类型的字段m。并且,这个类型拥有的所有方法,都与sync.Map类型的方法非常类似。

两者对应的方法名称完全一致,方法签名也非常相似,只不过,与键和值相关的那些参数和结果的类型不同而已。在IntStrMap类型的方法签名中,明确了键的类型为int,且值的类型为string。

显然,这些方法在接受键和值的时候,就不用再做类型检查了。另外,这些方法在从m中取出键和值的时候,完全不用担心它们的类型会不正确,因为它的正确性在当初存入的时候,就已经由 Go 语言编译器保证了。

稍微总结一下。第一种方案适用于我们可以完全确定键和值的具体类型的情况。在这种情况下,我们可以利用 Go 语言编译器去做类型检查,并用类型断言表达式作为辅助,就像IntStrMap那样。

第二种方案:

type ConcurrentMap struct {
	m         sync.Map
	keyType   reflect.Type
	valueType reflect.Type
}

func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
	if keyType == nil {
		return nil, errors.New("nil key type")
	}
	if !keyType.Comparable() {
		return nil, fmt.Errorf("incomparable key type: %s", keyType)
	}
	if valueType == nil {
		return nil, errors.New("nil value type")
	}
	cMap := &ConcurrentMap{
		keyType:   keyType,
		valueType: valueType,
	}
	return cMap, nil
}

func (cMap *ConcurrentMap) Delete(key interface{}) {
	if reflect.TypeOf(key) != cMap.keyType {
		return
	}
	cMap.m.Delete(key)
}

func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
	if reflect.TypeOf(key) != cMap.keyType {
		return
	}
	return cMap.m.Load(key)
}

func (cMap *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	if reflect.TypeOf(key) != cMap.keyType {
		panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
	}
	if reflect.TypeOf(value) != cMap.valueType {
		panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
	}
	actual, loaded = cMap.m.LoadOrStore(key, value)
	return
}

func (cMap *ConcurrentMap) Range(f func(key, value interface{}) bool) {
	cMap.m.Range(f)
}

func (cMap *ConcurrentMap) Store(key, value interface{}) {
	if reflect.TypeOf(key) != cMap.keyType {
		panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
	}
	if reflect.TypeOf(value) != cMap.valueType {
		panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
	}
	cMap.m.Store(key, value)
}

相比于第一种方案,第二种方案的键和值的类型更改更灵活,主要应用反射的知识,但是这样可能也会影响程序的性能,另外为了提高并发字典的性能,其中包含了read,和dirty字段,是两个原生字典,这里就不过多介绍了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值