为什么互斥锁代码会阻止另一个完整的Go协程?

huangapple go评论78阅读模式
英文:

Why the mutex code stops another whole go-routine?

问题

以下是代码的翻译结果:

var m *sync.RWMutex
func main() {
    m = new(sync.RWMutex)
    n := 100
    go func() {
        for i := 0; i < n; i++ {
            write("WA", i)
        }
    }()

    go func() {
        for i := 0; i < n; i++ {
            write("WB", i)
        }
    }()

    select {}
}
func write(tag string, i int) {
    m.Lock()
    fmt.Printf("[%s][%s%d]写入开始 \n", tag, tag, i)
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("[%s][%s%d]写入结束 \n", tag, tag, i)
    m.Unlock()

    // time.Sleep(1 * time.Millisecond)
}

控制台输出结果:

> go run mutex.go       
[WB][WB0]写入开始    
[WB][WB0]写入结束      
[WB][WB1]写入开始    
[WB][WB1]写入结束      
[WB][WB2]写入开始    
[WB][WB2]写入结束      
[WB][WB3]写入开始    
[WB][WB3]写入结束      
[WB][WB4]写入开始    
[WB][WB4]写入结束      
[WB][WB5]写入开始    
[WB][WB5]写入结束      
[WB][WB6]写入开始    
[WB][WB6]写入结束      
[WB][WB7]写入开始    
[WB][WB7]写入结束      
[WB][WB8]写入开始    
[WB][WB8]写入结束      
[WB][WB9]写入开始    
[WB][WB9]写入结束
...    

> go version
go version go1.5.2 windows/amd64

问题是:为什么"WA"的go例程没有机会执行?为什么互斥锁的代码会阻止另一个完整的go例程?

我知道这背后一定有一个故事或理论。请给我一个URL来阅读和学习。

英文:
var m *sync.RWMutex
func main() {
	m = new(sync.RWMutex)
	n := 100
	go func() {
		for i := 0; i &lt; n; i++ {
			write(&quot;WA&quot;, i)
		}
	}()

	go func() {
		for i := 0; i &lt; n; i++ {
			write(&quot;WB&quot;, i)
		}
	}()

	select {}
}
func write(tag string, i int) {
	m.Lock()
	fmt.Printf(&quot;[%s][%s%d]write start \n&quot;, tag, tag, i)
	time.Sleep(100 * time.Millisecond)
	fmt.Printf(&quot;[%s][%s%d]write end \n&quot;, tag, tag, i)
	m.Unlock()

	// time.Sleep(1 * time.Millisecond)
}

Result in console:

> go run mutex.go
[WB][WB0]write start
[WB][WB0]write end
[WB][WB1]write start
[WB][WB1]write end
[WB][WB2]write start
[WB][WB2]write end
[WB][WB3]write start
[WB][WB3]write end
[WB][WB4]write start
[WB][WB4]write end
[WB][WB5]write start
[WB][WB5]write end
[WB][WB6]write start
[WB][WB6]write end
[WB][WB7]write start
[WB][WB7]write end
[WB][WB8]write start
[WB][WB8]write end
[WB][WB9]write start
[WB][WB9]write end
...

&gt; go version
go version go1.5.2 windows/amd64

The question is:
why there is no chance for the go-routine of "[WA]"?
Why the mutex code stops another whole go-routine?

I know there must be a story or a theory about it.
Please give me a url to read and study.

答案1

得分: 2

这种情况被称为活锁。

当你调用m.Unlock()时,即使有两个goroutine(A和B)正在等待该锁被释放,调度器也可以自由地唤醒它们中的任何一个来继续执行。

目前Go语言调度器的实现似乎不能快速切换到goroutine A,以便它能够获取互斥锁。在这种情况下,goroutine B重新获取了互斥锁。

正如你可能已经发现的那样,如果你将time.Sleep调用放在m.Unlock调用之后,A和B goroutine将会同时运行。

希望这样说得清楚。

英文:

This situation is called live lock.

When you call m.Unlock() even though two goroutines (A and B) are waiting for this lock to be released the scheduler is free to wake up any of them to proceed.

It looks like the current implementation of scheduler in Go doesn't switch to goroutine A fast to enough for it to acquire the mutex. Before this happens goroutine B re-acquires the mutex.

As you probably found out if you move time.Sleep call after m.Unlock call both A and B goroutines will be running concurrently.

Hopefully this makes sense.

答案2

得分: 2

Go使用协作式多任务处理,而不是使用抢占式多任务处理:计算机多任务处理。你需要在锁之间给调度器一个运行的机会。例如,通过调用Gosched()函数。

package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

var m *sync.RWMutex

func main() {
	m = new(sync.RWMutex)
	n := 100
	go func() {
		for i := 0; i < n; i++ {
			write("WA", i)
		}
	}()

	go func() {
		for i := 0; i < n; i++ {
			write("WB", i)
		}
	}()

	select {}
}

func write(tag string, i int) {
	m.Lock()
	fmt.Printf("[%s][%s%d]write start \n", tag, tag, i)
	time.Sleep(100 * time.Millisecond)
	fmt.Printf("[%s][%s%d]write end \n", tag, tag, i)
	m.Unlock()
	runtime.Gosched()
}

输出:

[WB][WB0]write start 
[WB][WB0]write end 
[WA][WA0]write start 
[WA][WA0]write end 
[WB][WB1]write start 
[WB][WB1]write end 
[WA][WA1]write start 
[WA][WA1]write end 
[WB][WB2]write start 
[WB][WB2]write end 
[WA][WA2]write start 
[WA][WA2]write end 
[WB][WB3]write start 
[WB][WB3]write end 
[WA][WA3]write start 
[WA][WA3]write end 
英文:

Go uses cooperative multitasking; it doesn't use preemptive mutitasking: Computer multitasking. You need to give the scheduler an opportunity to run between locks. For example, by a call to Gosched(),

package main

import (
	&quot;fmt&quot;
	&quot;runtime&quot;
	&quot;sync&quot;
	&quot;time&quot;
)

var m *sync.RWMutex

func main() {
	m = new(sync.RWMutex)
	n := 100
	go func() {
		for i := 0; i &lt; n; i++ {
			write(&quot;WA&quot;, i)
		}
	}()

	go func() {
		for i := 0; i &lt; n; i++ {
			write(&quot;WB&quot;, i)
		}
	}()

	select {}
}

func write(tag string, i int) {
	m.Lock()
	fmt.Printf(&quot;[%s][%s%d]write start \n&quot;, tag, tag, i)
	time.Sleep(100 * time.Millisecond)
	fmt.Printf(&quot;[%s][%s%d]write end \n&quot;, tag, tag, i)
	m.Unlock()
	runtime.Gosched()
}

Output:

[WB][WB0]write start 
[WB][WB0]write end 
[WA][WA0]write start 
[WA][WA0]write end 
[WB][WB1]write start 
[WB][WB1]write end 
[WA][WA1]write start 
[WA][WA1]write end 
[WB][WB2]write start 
[WB][WB2]write end 
[WA][WA2]write start 
[WA][WA2]write end 
[WB][WB3]write start 
[WB][WB3]write end 
[WA][WA3]write start 
[WA][WA3]write end 

答案3

得分: 1

@peterSO的回答是正确的。稍微解释一下调度的问题,那个for循环是一个顺序紧密的循环。这意味着从该循环编译的指令将占用一个完整的线程来完成。同时,除非它们在循环中间提供了一些调度周期,否则低级指令的其他部分将被阻塞。这就是为什么它们实际上没有机会与sync.Mutex配合使用(顺便说一下,在声明和实例化时都应该是sync.Mutex):

go func() {
    for i := 0; i < n; i++ {
        runtime.Gosched()
        write("WA", i)
    }
}()

go func() {
    for i := 0; i < n; i++ {
        runtime.Gosched()
        write("WB", i)
    }
}()

而Go调度器在指令级别上不是抢占式的(像Erlang一样)。这就是为什么最好使用通道来编排执行路径的原因。

注意:我是通过吃了亏才学到这一点的(并不是一个低级Go编译器专家)。通道以更清晰的方式在Go协程(和那些额外的周期)上提供了编排。换句话说,sync.Mutex应该仅用于监督对资源的访问,而不是用于编排。

英文:

@peterSO 's answer is correct. Just to elaborate a bit on scheduling, that for loop is a sequential tight loop. Means that the compiled instructions from that loop, would occupy a whole thread to finish. Meanwhile other bits of low level instructions would block unless they have some schedule cycles provided by runtime.Gosched() or sleep, in the middle of the loop. That's why they actually have not a chance to catch on with the sync.Mutex (BTW both should be sync.Mutex at declaration and instantiation):

go func() {
	for i := 0; i &lt; n; i++ {
		runtime.Gosched()
		write(&quot;WA&quot;, i)
	}
}()

go func() {
	for i := 0; i &lt; n; i++ {
		runtime.Gosched()
		write(&quot;WB&quot;, i)
	}
}()

And Go scheduler is not preemptive at instruction level (like Erlang). That's why it's better to orchestrate execution path using channels.

Note: I've learnt this the hard way (not a low-level Go compiler expert). And channels provide that orchestration on Go-Routines (& those extra cycles) in a more clean manner. In other words sync.Mutex should be used just for supervising access to stuff; not for orchestration.

huangapple
  • 本文由 发表于 2016年3月11日 12:23:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/35932194.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定