- So let's bring everything together
with this really nasty program.
But it's kinda more practical in how data races really
end up in production, especially
if you're not using the race detector.
And in the past, how, without having something as beautiful
as the race detector, data races could get into
production-level code.
So, imagine this scenario, Ben and Jerry are fighting
with each other, they got into an argument
that one thinks they're more popular than the other.
Ben thinks he's the most popular.
Jerry thinks he's the most popular.
So they hire a marketing company to track likes,
and they start this like campaign,
where they wanna know who is more popular.
So, let's take a look at some code we might write
to solve this problem.
There it is on line 16, we have what's called the speaker
interface, when active behavior speak.
What we want is users to speak up when they like Ben
or speak up when they like Jerry.
Now normally what we'd be doing is incrementing a like
counter, but I wanna be able to detect
our bad data races here.
So we're gonna do something a little different,
but imagine, every time you call speak, we're really gonna
be speaking up and showing our support for Jerry or Ben.
So, the Ben data structure has a name, it's Ben.
And what we do in the speak, instead of right now,
incrementing our like against the Ben value,
what we do is we check Ben's name.
Now if Ben is Ben, life is okay, but if we had a data
race, we could end up in a situation where Ben is not
Ben, Ben might end up being Jerry.
We'll see that.
And when that happens, we'll spit that information out,
and we'll return fault so we can shut the program down.
So there's Benny, Ben sorry, and here's Jerry.
Jerry looks exactly like Ben, it just has a name,
and the code is implemented the same, except Jerry's
wanting to be Jerry all the time.
So there it is.
We have our speak, which traditionally, would increment
a like, but Ben is checking out that it's always Ben,
Jerry's always Jerry.
Now, we start the program up, and I create my Ben value,
here it is, there's Ben, here's Jerry, right.
We're really gonna have our likes in here.
And remember that we have implemented the speak methods
against these concrete data values.
Now, I start right out of the box with our person
interface, and if you notice there, I'm storing,
using pointer semantics, Ben inside of there.
So when we call speak against the interface,
we can either call speak against Ben or Jerry,
depending on which data, in this case Ben, is in there.
Call speak, we would be incrementing that to one, right.
Okay, great.
So we start this campaign, now imagine this is really
a web service, right, with tens of thousands
of requests coming in from Jerry and Ben's support.
And here's a go routine that simulates maybe one request
or a bunch of requests coming in.
And you can see here that what this go routine does
is it loads Ben into the interface, and then calls speak.
So, that line on 65 is our write, it's a two-word right,
in order to get Ben inside the interface.
And then the call on 66 is our read.
We got a write, a two-word write, and a read, okay.
And then what we have is another go routine simulating
other requests coming in that's saying no, no, no, no
Jerry is the write, is who I support.
Which now means we gotta do the two word-write,
which says Jerry, and then we can call speak.
So, I've got a data race here, right.
I wanna show you how quickly this code
suddenly has data corruption.
Let's build it.
(types on keyboard)
And let's run it.
Doesn't take long for the program to say Jerry says,
"Hello my name is Ben."
This is an integrity issue, and understand the code
didn't blow up, the very worst thing that can ever happen
to you in a data race is your code keeps running.
We'll explain why this code doesn't blow up in a second.
But for it to say Jerry says, means that the first word
says that there is a Jerry pointer inside the interface.
Because, this word is telling us which
concrete implementation to call, which is Jerry.
But what's happened is only half this interface
has been written to before the other go routine calls speak.
So the pointer was still pointing to Ben, remember it takes
two operations here, you gotta change out what type
of data is in the interface, and then you gotta
change out the pointer to the concrete data.
Well only half of the write operation took place,
before the go routine called SP, or speak.
And so now, suddenly, guess what?
Ben got a like for Jerry.
We wouldn't see this in code, because it didn't blow up.
The reason it didn't blow up is because these two data
structures that we're using are identical,
they have the same exact memory layout.
So nothing's gonna cause this code to blow up.
Incrementing here is gonna be the same as incrementing here.
In our case, the names are the same.
So since the data models are the same against
all the concrete types, guess what, this code doesn't blow
up, it keeps running.
And we wouldn't even know about it until somebody
in marketing said, you know what, look I like Ben
as much as the next person, but there's no way Ben
is that much more popular than Jerry,
it just statistically doesn't make sense.
And now you're being asked, hey you got a bug in your
program and I need you to find it.
Yet, there is no stack trace, there is no corruption,
I mean there's no code blowing up, there's no way
to indicate where the data corruption is.
This is why data races are nasty, nasty.
Now, if I build this program using the race detector.
There it is.
When you use the race detector on a build,
you have to build the binary and then run it.
You can see how quickly the race detector identified
that there is a race, right.
Go routine seven, go routine six, and there in this case
was two writes that happened at the same time
that were not synchronized.
Line 76 and line 65.
So we're seeing here that line 76 and line 65, we could see
here that these two calls happened to happen
at the same time on this pass of execution.
Yay for the race detector.
Now you might say, oh Bill, you know what, forget about
the race detector, I would have found this regardless.
And I'm gonna say to you, no you wouldn't have.
What if I use go maxprocks to only use a single core.
I am now a single-threaded go program, maybe I'm running
this stuff in Docker on my local machine, and I've
restricted my containers to just use one core,
because you know, I'm trying to use resources cleanly,
and I've put this all into my Docker containers.
Now, I've run the race detector, which is really funny.
Notice that the race detector still found a race
with this, but let's do something.
Let's rebuild the binary, okay, without the race detector.
Do this again, and look what happens.
The race is technically never happening because
since we're running on one thread,
all of these reads and writes are happening
in a synchronous way, but it's an accident.
It's an accident.
We're getting very lucky that every one of these two calls
are happening atomically right now,
when I'm single threaded.
We don't see anything, our tests are working
in single-threaded environment, our code is working
in a single-threaded environment.
But probably what's gonna happen is when this code
moves to production, then we really start having a problem.
Because in production, we're not doing single-threaded,
maybe we're doing multi-threaded.
So without the race detector, we're gonna be in a lot
of trouble here.
And this again is a very classic, historical way
of how data races have entered production code.
And how nasty they are to find, why it could take years
to find the data race, because there's no indication
in the code, there's no indication from stack traces,
or things blowing up, it's purely a problem
of data corruption, and it becomes very difficult
to find data corruption sometimes just by looking at code.