[译文]Go线程安全 - 一个被忽略的问题

2023-01-31 15:11:36 浏览数 (1)

When you first start tinkering with concurrency in any language, what’s the most sought after goal is extracting the maximum output from the hardware you have available.


当您第一次开始摆弄任何语言中的并发性时,最受欢迎的目标是发挥出你的硬件的最大输出能力。

That’s why we use it and study it in the first place – to achieve optimal parallelism & get some significant speed-up for our apps out of the box.


这就是我们首先要研究线程安全的目的—以实现最佳的并行性,并使我们的应用程序的运行速度大大加快

However, a not so glamorous part of studying the subject is understanding how to write thread-safe code. The techniques and principles which will enable you to keep your application well-behaved, even after scaling it to dozens of threads.


然而,学习这个主题比较困难但又很重要的部分就是要理解如何编写线程安全的代码。线程安全的代码将使你的应用程序即使面对大量并发的时候,依然保持良好的性能。

Even though this is an important thing to consider while writing concurrent code, it is often overlooked in most learning resources about concurrency. This problem is even more relevant in the Go community due to two common misunderstandings I will cover in the article.


尽管这是编写并发代码时需要考虑的一个重要问题,但在大多数关于并发的学习资源中,它常常被忽略。这个问题在go语言中尤为重要,因此我将在本文中讨论两个常见的误区。

What does thread-safety mean anyways?


线程安全到底意味着什么?

Let’s first explore what thread-safety is exactly. One reasonable way to define the term is to think of it in terms of correctness.


让我们首先来探讨什么是线程安全。一种定义这个术语的合理方式是从正确性 来考虑。

If a component conforms to its specification, it is correct. Say you have an Interval struct which defines two limits – lower and higher. The important invariant instances of the struct have to maintain is lower < higher. If at any point, this invariant is not true, then this class is incorrect.


如果一个组件符合它的规则,那么这个组件是正确的。假设你有个内部结构体,定义了两个限制:lower 和higher。在该结构中,这个实体最重要的一个特性是要保持lower < higher。如果在任何时候,这个实体对象的lower > higher了,则该类就不正确了。

A thread-safe class, on the other hand is able to sustain its correctness even after being bombarded by a dozen of threads, accessing it concurrently.


从另一方面来说,一个线程安全的类,即使被几十个线程同时并发访问,也能够保持其正确性。

Now, when designing thread-safe components, there are also other concerns one has to consider. One such concern is achieving reasonable performance – if a component performs worse in a multi-threaded environment, instead of a single-threaded one, it’s obviously not a great solution.


现在,在设计线程安全的组件时,还有另一个问题需要考虑。那就是还要获取合理的性能。如果一个组件在多线程环境中性能表现不佳,甚至还不如单线程的性能优异,那这显然不是一个较好的解决方案。

However, for the scope of this article, we will just worry about making a component thread-safe – i.e. correct in a multi-threaded environment.


但是,本文只关注组件在多线程环境中的正确性。

Why should you care about thread-safety?


为什么要关心线程安全?

Most people understand that thread-safety is an issue. When exploring the topic of concurrency in any language, they’ve probably seen at least a single thread-safety issue.


大多数人都知道线程安全是一个问题。在探讨任何语言中关于并发的主题时,他们至少看到过一个线程安全的问题。

Consider this example:


考虑这个例子:

代码语言:javascript复制
package main
import (
    “fmt"
    “time"
)
func main() {
    counter := 0
    for I := 0; I < 10000; I   {
        go func() {
            counter  
        }()
    }
    time.Sleep(1000)
    fmt.Println(counter)
}

When I run this code on my laptop, I get as result 9303. The second time, it was a different number. When you run it, it might even be correct.


当我运行这个代码的时候,我得到的结果是9303.第二次运行的时候,结果是和之前不同的数字。当你运行这段代码的时候,可能会是正确的。

This is an example of non-thread-safe code. The expected behaviour for this routine is to print 10000 when I increment the counter that many times, albeit concurrently.


这是一个线程不安全的例子。对于这段程序期望的结果是无论执行多少次,应该都是打印出10000。

Instead, when I ran it the first time I got 9303. The second time was a different number which was still incorrect. Try running it on your end and see what’s your lucky number.


相反,当我第一次运行时,打印的结果是9303。第二次运行时却得到另外一个不同的数字,仍然不是10000。试着在你的终端上运行这段程序,看看你将得到什么样的结果。

Given this example, most people acknowledge that writing thread-safe code is important as otherwise, you are subject to sporadic bugs which would be very hard to trace, debug and reproduce.


在这个例子中,大多数人都承认编写线程安全的代码非常重要,否则,您会遇到零星的错误,这些错误很难跟踪,调试和重现。

Even so, the concern of writing thread-safe code is still quite overlooked by many due to two main reasons:

  • They think they don’t need to worry about it because they don’t directly spawn threads in their applications
  • They think they don’t need to worry because they use channels and there is no memory sharing when using them

即便如此,编写线程安全的代码的观点仍被很多人忽视,原因有两个:

  • 他们认为他们不需要担心这个问题,因为在他们的应用中他们不会直接产生线程
  • 他们认为他们不需要担心,因为他们使用channel通信并且使用channel时没有内存共享

Unfortunately both of these assumptions are incorrect for most people. Let’s see why.


不幸的是,这两种假设对大多数人来说都是不正确的。让我们看看为什么。

You are already writing multi-threaded applications


你正在写多线程的应用程序

Let’s first cover the subtle fact that you are already writing multi-threaded applications even if you haven’t spawned a single goroutine in it.


首先,让我们介绍一个微妙的事实,即即使您没有在其中生成单个goroutine,也已经在编写多线程应用程序了。

Have you ever created an http handler using Go’s standard http package?


您是否曾经使用Go的标准http包创建过http处理程序?

Congratulations, you are already writing multi-threaded software!


恭喜你,您已经在写多线程的软件了。

Although you aren’t spawning goroutines in your codebase directly, you are using a framework which spawns one for every incoming http request.


虽然在你的代码中你没有直接创建goroutines,您使用的框架会为每个传入的HTTP请求生成一个goroutines。

This means that there are already multiple threads, executing your code. Hence, it needs to be thread-safe to avoid subtle bugs, which would be quite hard to find.


这也就是说,在执行你的代码的时候,已经存在多线程了。因此,它必须是线程安全的,以避免难以发现的细微错误。

By the way, this is applicable to any multi-threaded framework or library you might use. Even if you’re simply spawning a timer to execute some background task, you’re already dealing with a multi-threaded application.


顺便说一下,这适用于您可能使用的任何多线程框架或库。即使您只是生成一个计时器来执行一些后台任务,您也已经在处理多线程应用程序了。

Go channels are great, but not always applicable


Go的channel非常棒,但不一定总是合适

There’s this notorious technique, applied in Go, of not communicating by sharing memory but sharing memory by communicating.


Go中使用了一种众人皆知的技术,即不通过共享内存进行通信,而是通过通信来共享内存

You can achieve this via Go’s channel primitive. It’s a data structure which enables one to achieve thread-safe code in a multi-threaded environment. It’s nice because you don’t need any synchronisation whatsoever as long as you use it to share data across threads.


您可以通过Go的channel原语来实现。它是一种数据结构,使人们可以在多线程环境中实现线程安全的代码。这很棒,因为您不需要任何同步,只要您使用它来跨线程共享数据即可

It, undoubtedly, is a great solution for avoiding thread-safety issues when writing concurrent code. However, it is not applicable in all situation.


毫无疑问,channel是在编写并发代码时避免线程安全问题的绝佳解决方案。但是,channel并非在所有情况下都适用。

Let’s explore an example


让我们再来看一个例子

Take this simple web chat implementation:


以这个简单的web聊天实现为例

代码语言:javascript复制
package main

import (
    “Net/http”
    “Strings"
)
func main() {
    http.HandleFunc(“/get”, getMsg)
    http.HandleFunc(“/send”, sendMsg)
    http.ListenAndServe(":8080", nil)
}
var chat = map[string][]string{}
func sendMsg(w http.ResponseWriter, r *http.Request) {
    user := r.URL.Query()["user"][0]
    msg := r.URL.Query()["msg"][0]
    chat[user] = append(chat[user], msg)
    w.WriteHeader(200)
}
func getMsg(w http.ResponseWriter, r *http.Request) {
    user := r.URL.Query()["user"][0]
    w.Write([]byte(strings.Join(chat[user], ", ")))
}

This looks like a perfectly reasonable application, where no goroutines are spawned. When you test it out via a rest client, it appears to work great!


这看起来是一个非常合理的应用程序,没有生成goroutine。当你通过少量客户端测试它,它似乎工作的很好!

However, if you deploy this code into production and enough users start using it, you will start receiving a flood of bug reports which you can hardly reproduce.


但是,如果您将此代码部署到生产中,并且有足够多的用户开始使用它,您将开始接收大量的bug报告,而这些报告很难复现。

What’s more, changing this code to work with channels in quite impractical as the resulting code will be hard to understand and reason about. A much more practical solution is sticking to the good old synchronisation primitives, implemented in the sync package.


更重要的是,用channel更改此代码并让其工作非常不切实际,因为这样代码将会变的难以理解和解释。一个更实用的解决方案是使用sync包中实现的同步原语。

And this is where the classic, well-known practices for achieving thread-safety are applicable in Go, despite the advent of channels.


尽管在Go中引入了channel,但sync包中实现的同步原语依然是在Go语言中经典的、众所周知的实现线程安全的实践方案。

Are you curious if you have any non-thread-safe classes in your own web application?


您是否好奇自己的web应用程序中是否有任何非线程安全的类?

One common source of thread-safety bugs in web applications is having a struct field of type slice or map whose state you change in any function. If that struct instance is shared across multiple http requests and you don’t have any synchronisation, then that’s probably a non-thread-safe component.


Web应用程序中出现线程安全bug的一个常见来源是具有slice或map类型的结构体,并且该结构体可以在任何函数中更改其状态。如果该结构实例在多个http请求之间共享,而您没有任何同步机制,那么这可能是一个非线程安全的组件。

Some good resources on thread-safety & concurrency


一些关于线程安全和并发的好资源

Most existing resources cover the subject of thread-safety in a very shallow way. I know this as after consuming many of them, I’ve still felt that I don’t understand it at all.


现有的大多数资源在涉及到线程安全的主题时都非常的浅显。我知道这个主题是因为我做了很多的研究,但是我仍然觉得我不完全了解。

For example, one of the only books on concurrency in go – Concurrency in Go, dedicates a single chapter on covering the problems of thread-safety. And it doesn’t even go into enough detail exploring what options we have for solving them or even demonstrating all possible sources of thread-safety violations.


例如,在关于Go并发编程的唯一书籍-Go并发编程,有一章的内容专门介绍了有关线程安全性问题。而且并没有足够详细地探讨我们有什么解决方案,甚至也没有说明所有引起线程安全的根源。

Once you read that chapter you would, at best, have a shallow understanding of the fact that you need to be careful when writing concurrent code. Oh, and that there is something called a lock which appears to be helpful for resolving that issue.


一旦阅读了该章内容,您最多将对以下事实有一个浅显的了解:编写并发代码时需要小心。 哦,有一种叫做锁的东西似乎对解决该问题很有帮助。

So far, I’ve found a single resource on concurrency which goes into enough depth on the subject, exploring why thread-safety issues happen and how to avoid them – Java Concurrency in Practice.


到目前为止,我已经找到了一个深入讲解并发的资源,这个资源深入探讨了为什么会发生并发安全以及如何解决它们 - Java Concurrecny in Practice 。译者注:中文名叫 《Java9 并发编程实战》

If you want to explore concurrency and thread-safety beyond this article, then I highly suggest you check out that book. My book notes can also greatly aid you in digesting it.


如果你想更深入的了解并发和线程安全相关的内容,我强烈建议你阅读本书。

Oh, and I know it’s a Java book, not a Go one but trust me. It’s still better than all the Go books/courses on the subject.


我知道这是一本关于Java而非Go的书籍。但是,请相信我。它比任何一本Go相关主题的书籍都要好。

Additionally, I’m planning on releasing several additional articles on the subject, covering the subject in more detail. If you want to get a notification once I release the next article in the series, make sure to subscribe.


此外,我计划发布几篇关于这个主题的附加文章,更详细地涵盖这个主题。如果您想在我发布系列中的下一篇文章后获得通知,请确保订阅。

Conclusion


结论

Thread-safety is an important concern for stable & reliable applications, nowadays as most of us are already writing multi-threaded applications.

If you want to avoid the subtle bugs arising from thread-safety issues, you should make sure you sufficiently understand the topic.

In order to bridge the gaps in your knowledge, check out Java Concurrency in Practice. Also subscribe for the blog for the next articles in these series on thread-safety in Go.


线程安全是构建稳定可靠的应用程序的一个重要问题,现在我们大多数人已经在编写多线程应用程序。

如果您想避免线程安全问题引起的微妙错误,您应该确保您充分理解该主题。

为了弥补你的知识差距,请阅读《Java Concurrency in Practice》 。还可以订阅我的下一篇关于Go线程安全的文章

0 人点赞