如何以编程方式证明此代码具有竞争条件?
How to programatically prove this code has a race condition?
有人告诉我这段代码在设计上存在竞争条件,尽管我尽了最大努力,但我无法证明它确实如此。
func (h *handler) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.Log = log.WithFields(log.Fields{
"method": r.Method,
"requestURI": r.RequestURI,
})
next.ServeHTTP(w, r)
})
}
我尝试了 go build -race
,然后 运行 二进制文件:PORT=3000 ./main
并加载像 hey -n 10000 -c 200 http://localhost:3000
.
这样的创建者
此处的其余代码:https://raw.githubusercontent.com/kaihendry/context-youtube/master/5/main.go
或
type handler struct{ Log *log.Entry }
func New() (h *handler) { return &handler{Log: log.WithFields(log.Fields{"test": "FAIL"})} }
func (h *handler) index(w http.ResponseWriter, r *http.Request) {
h.Log.Info("index")
fmt.Fprintf(w, "hello")
}
func (h *handler) about(w http.ResponseWriter, r *http.Request) {
h.Log.Info("about")
fmt.Fprintf(w, "about")
}
func main() {
h := New()
app := mux.NewRouter()
app.HandleFunc("/", h.index)
app.HandleFunc("/about", h.about)
app.Use(h.loggingMiddleware)
if err := http.ListenAndServe(":"+os.Getenv("PORT"), app); err != nil {
log.WithError(err).Fatal("error listening")
}
}
如果我不能证明它有竞争条件,我可以假设设置 h.Log
是安全的吗?
假设您几乎同时收到两个命中同一个处理程序的入站连接。第一个连接开始 运行:
h.Log = log.WithFields(log.Fields{
"method": rFirst.Method,
"requestURI": rFirst.RequestURI,
})
但是等等!第二个连接出现了!也许运行时想要暂停这个 goroutine 并启动第二个连接。那么...
h.Log = log.WithFields(log.Fields{
"method": rSecond.Method,
"requestURI": rSecond.RequestURI,
})
next.ServeHTTP(wSecond, rSecond)
呼...到此结束。让我们回到我们的第一个 goroutine。
// What's in h.Log now, with this sequence of events?
next.ServeHTTP(wFirst, rFirst)
Or...
您的第二组示例不会更改 h.Log
的值,但会对其调用方法。在大多数情况下,这可能安全也可能不安全。 documentation for log.Logger
contains the magic phrase: "A Logger can be used simultaneously from multiple goroutines". (If you've actually imported "github.com/sirupsen/logrus"
as log
, that has a similar statement in its documentation.)
can I assume setting h.Log is safe?
没有 sync.Mutex
或类似的东西保护它,不是真的。你绝对不能保证,如果你在第 1 行设置它,如果其他 goroutine 可能正在更改它,它在第 2 行将具有相同的值。 The Go Memory Model 对什么时候保证可见的副作用有更精确的定义。
有一种编程方式,为此你必须做两件事:
- 重现活泼的条件
- 并在启动
go
工具时使用 -race
选项
最好是为它编写单元测试,这样测试也是可重现的,并且 运行/在每次构建/部署时自动检查。
好的,那么如何重现呢?
只需编写一个启动 2 个 goroutine 的测试,一个调用 index
处理程序,一个调用 about
处理程序,故意不同步,这就是触发竞争检测器的原因。
使用 net/http/httptest
package to easily test handlers. httptest.NewServer()
为您准备好的服务器,"armed" 使用您传递给它的处理程序。
这是一个简单的测试示例,它会触发竞争条件。把它放在一个名为 main_test.go
的文件中,在 main.go
文件旁边:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/gorilla/mux"
)
func TestRace(t *testing.T) {
h := New()
app := mux.NewRouter()
app.HandleFunc("/", h.index)
app.HandleFunc("/about", h.about)
app.Use(h.loggingMiddleware)
server := httptest.NewServer(app)
defer server.Close()
wg := &sync.WaitGroup{}
for _, path := range []string{"/", "/about"} {
path := path
wg.Add(1)
go func() {
defer wg.Done()
req, err := http.NewRequest(http.MethodGet, server.URL+path, nil)
fmt.Println(server.URL + path)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
}()
}
wg.Wait()
}
你必须 运行 它
go test -race
示例输出为:
http://127.0.0.1:33007/
http://127.0.0.1:33007/about
==================
WARNING: DATA RACE
Write at 0x00c000098030 by goroutine 17:
play.(*handler).loggingMiddleware.func1()
/home/icza/tmp/gows/src/play/main.go:16 +0x1ce
net/http.HandlerFunc.ServeHTTP()
/usr/local/go/src/net/http/server.go:1964 +0x51
github.com/gorilla/mux.(*Router).ServeHTTP()
/home/icza/tmp/gows/src/github.com/gorilla/mux/mux.go:212 +0x12e
net/http.serverHandler.ServeHTTP()
/usr/local/go/src/net/http/server.go:2741 +0xc4
net/http.(*conn).serve()
/usr/local/go/src/net/http/server.go:1847 +0x80a
Previous write at 0x00c000098030 by goroutine 16:
play.(*handler).loggingMiddleware.func1()
/home/icza/tmp/gows/src/play/main.go:16 +0x1ce
net/http.HandlerFunc.ServeHTTP()
/usr/local/go/src/net/http/server.go:1964 +0x51
github.com/gorilla/mux.(*Router).ServeHTTP()
/home/icza/tmp/gows/src/github.com/gorilla/mux/mux.go:212 +0x12e
net/http.serverHandler.ServeHTTP()
/usr/local/go/src/net/http/server.go:2741 +0xc4
net/http.(*conn).serve()
/usr/local/go/src/net/http/server.go:1847 +0x80a
Goroutine 17 (running) created at:
net/http.(*Server).Serve()
/usr/local/go/src/net/http/server.go:2851 +0x4c5
net/http/httptest.(*Server).goServe.func1()
/usr/local/go/src/net/http/httptest/server.go:280 +0xac
Goroutine 16 (running) created at:
net/http.(*Server).Serve()
/usr/local/go/src/net/http/server.go:2851 +0x4c5
net/http/httptest.(*Server).goServe.func1()
/usr/local/go/src/net/http/httptest/server.go:280 +0xac
==================
2019/01/06 14:58:50 info index method=GET requestURI=/
2019/01/06 14:58:50 info about method=GET requestURI=/about
--- FAIL: TestRace (0.00s)
testing.go:771: race detected during execution of test
FAIL
exit status 1
FAIL play 0.011s
测试失败,表明存在数据竞争。
备注:
与sync.WaitGroup
的同步是为了等待2个启动的goroutines,而不是为了同步访问handler的logger(导致data race)。如果您修复数据竞争,测试将 运行 并正确结束(等待 2 个启动的测试 goroutines 完成)。
有人告诉我这段代码在设计上存在竞争条件,尽管我尽了最大努力,但我无法证明它确实如此。
func (h *handler) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.Log = log.WithFields(log.Fields{
"method": r.Method,
"requestURI": r.RequestURI,
})
next.ServeHTTP(w, r)
})
}
我尝试了 go build -race
,然后 运行 二进制文件:PORT=3000 ./main
并加载像 hey -n 10000 -c 200 http://localhost:3000
.
此处的其余代码:https://raw.githubusercontent.com/kaihendry/context-youtube/master/5/main.go
或
type handler struct{ Log *log.Entry }
func New() (h *handler) { return &handler{Log: log.WithFields(log.Fields{"test": "FAIL"})} }
func (h *handler) index(w http.ResponseWriter, r *http.Request) {
h.Log.Info("index")
fmt.Fprintf(w, "hello")
}
func (h *handler) about(w http.ResponseWriter, r *http.Request) {
h.Log.Info("about")
fmt.Fprintf(w, "about")
}
func main() {
h := New()
app := mux.NewRouter()
app.HandleFunc("/", h.index)
app.HandleFunc("/about", h.about)
app.Use(h.loggingMiddleware)
if err := http.ListenAndServe(":"+os.Getenv("PORT"), app); err != nil {
log.WithError(err).Fatal("error listening")
}
}
如果我不能证明它有竞争条件,我可以假设设置 h.Log
是安全的吗?
假设您几乎同时收到两个命中同一个处理程序的入站连接。第一个连接开始 运行:
h.Log = log.WithFields(log.Fields{
"method": rFirst.Method,
"requestURI": rFirst.RequestURI,
})
但是等等!第二个连接出现了!也许运行时想要暂停这个 goroutine 并启动第二个连接。那么...
h.Log = log.WithFields(log.Fields{
"method": rSecond.Method,
"requestURI": rSecond.RequestURI,
})
next.ServeHTTP(wSecond, rSecond)
呼...到此结束。让我们回到我们的第一个 goroutine。
// What's in h.Log now, with this sequence of events?
next.ServeHTTP(wFirst, rFirst)
Or...
您的第二组示例不会更改 h.Log
的值,但会对其调用方法。在大多数情况下,这可能安全也可能不安全。 documentation for log.Logger
contains the magic phrase: "A Logger can be used simultaneously from multiple goroutines". (If you've actually imported "github.com/sirupsen/logrus"
as log
, that has a similar statement in its documentation.)
can I assume setting h.Log is safe?
没有 sync.Mutex
或类似的东西保护它,不是真的。你绝对不能保证,如果你在第 1 行设置它,如果其他 goroutine 可能正在更改它,它在第 2 行将具有相同的值。 The Go Memory Model 对什么时候保证可见的副作用有更精确的定义。
有一种编程方式,为此你必须做两件事:
- 重现活泼的条件
- 并在启动
go
工具时使用-race
选项
最好是为它编写单元测试,这样测试也是可重现的,并且 运行/在每次构建/部署时自动检查。
好的,那么如何重现呢?
只需编写一个启动 2 个 goroutine 的测试,一个调用 index
处理程序,一个调用 about
处理程序,故意不同步,这就是触发竞争检测器的原因。
使用 net/http/httptest
package to easily test handlers. httptest.NewServer()
为您准备好的服务器,"armed" 使用您传递给它的处理程序。
这是一个简单的测试示例,它会触发竞争条件。把它放在一个名为 main_test.go
的文件中,在 main.go
文件旁边:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/gorilla/mux"
)
func TestRace(t *testing.T) {
h := New()
app := mux.NewRouter()
app.HandleFunc("/", h.index)
app.HandleFunc("/about", h.about)
app.Use(h.loggingMiddleware)
server := httptest.NewServer(app)
defer server.Close()
wg := &sync.WaitGroup{}
for _, path := range []string{"/", "/about"} {
path := path
wg.Add(1)
go func() {
defer wg.Done()
req, err := http.NewRequest(http.MethodGet, server.URL+path, nil)
fmt.Println(server.URL + path)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
}()
}
wg.Wait()
}
你必须 运行 它
go test -race
示例输出为:
http://127.0.0.1:33007/
http://127.0.0.1:33007/about
==================
WARNING: DATA RACE
Write at 0x00c000098030 by goroutine 17:
play.(*handler).loggingMiddleware.func1()
/home/icza/tmp/gows/src/play/main.go:16 +0x1ce
net/http.HandlerFunc.ServeHTTP()
/usr/local/go/src/net/http/server.go:1964 +0x51
github.com/gorilla/mux.(*Router).ServeHTTP()
/home/icza/tmp/gows/src/github.com/gorilla/mux/mux.go:212 +0x12e
net/http.serverHandler.ServeHTTP()
/usr/local/go/src/net/http/server.go:2741 +0xc4
net/http.(*conn).serve()
/usr/local/go/src/net/http/server.go:1847 +0x80a
Previous write at 0x00c000098030 by goroutine 16:
play.(*handler).loggingMiddleware.func1()
/home/icza/tmp/gows/src/play/main.go:16 +0x1ce
net/http.HandlerFunc.ServeHTTP()
/usr/local/go/src/net/http/server.go:1964 +0x51
github.com/gorilla/mux.(*Router).ServeHTTP()
/home/icza/tmp/gows/src/github.com/gorilla/mux/mux.go:212 +0x12e
net/http.serverHandler.ServeHTTP()
/usr/local/go/src/net/http/server.go:2741 +0xc4
net/http.(*conn).serve()
/usr/local/go/src/net/http/server.go:1847 +0x80a
Goroutine 17 (running) created at:
net/http.(*Server).Serve()
/usr/local/go/src/net/http/server.go:2851 +0x4c5
net/http/httptest.(*Server).goServe.func1()
/usr/local/go/src/net/http/httptest/server.go:280 +0xac
Goroutine 16 (running) created at:
net/http.(*Server).Serve()
/usr/local/go/src/net/http/server.go:2851 +0x4c5
net/http/httptest.(*Server).goServe.func1()
/usr/local/go/src/net/http/httptest/server.go:280 +0xac
==================
2019/01/06 14:58:50 info index method=GET requestURI=/
2019/01/06 14:58:50 info about method=GET requestURI=/about
--- FAIL: TestRace (0.00s)
testing.go:771: race detected during execution of test
FAIL
exit status 1
FAIL play 0.011s
测试失败,表明存在数据竞争。
备注:
与sync.WaitGroup
的同步是为了等待2个启动的goroutines,而不是为了同步访问handler的logger(导致data race)。如果您修复数据竞争,测试将 运行 并正确结束(等待 2 个启动的测试 goroutines 完成)。