Is it bad to create an array on the main thread, but edit it's elements on another thread?

408 views
Skip to first unread message

zachary...@gmail.com

unread,
Apr 2, 2016, 4:11:14 PM4/2/16
to golang-nuts
I'm making a server for a game, and I currently have an array of entities created on the main thread, looks like this
var entities = make(map[string]*Entity)
So my issue now is that I have to loop through every entity and call a function called entity.update() which will do stuff like update their health, energy, and position values. I'm also having to call a couple other update functions which gets pretty laggy. Could I put these functions on another goroutine? They aren't adding or removing elements from the array, just editing the elements' values.

Roberto Zanotto

unread,
Apr 2, 2016, 4:20:59 PM4/2/16
to golang-nuts, zachary...@gmail.com
Just make sure that two different goroutines don't edit the same Entity at the same time.
Run your code with "go run -race whatever.go" to enable the race detector.

zachary...@gmail.com

unread,
Apr 2, 2016, 4:29:24 PM4/2/16
to golang-nuts, zachary...@gmail.com
So what if one goroutine is editing the entities and one is just grabbing the data (to send to other players), would that be okay? 

And what is the race detector?

Roberto Zanotto

unread,
Apr 2, 2016, 7:00:46 PM4/2/16
to golang-nuts, zachary...@gmail.com
It is not ok to read some memory location (grabbing the data) while another goroutine is writing to the same memory location (editing the entities).
Also, it is not ok if two goroutines simultaneously write to the same memory location.
These are called "data races" and are bad, because, for example, the result of reading some memory while another thread is modifying it
is undefined.
The race detector is a tool that detects this kind of bugs. You turn it on by doing go run, build or test with the -race flag.
By the way, it is ok if multiple goroutines read from the same memory location, if no one is modifying the data.

When using multiple goroutines you typically have two options:
1) using shared data structures (your Entity map) and synchronize access with mutexes.
2) avoid shared data structures and have the goroutines communicate with channels.
Of course you can mix those things.

Note that these are issues that arise in any language with multithreaded programming, not just Go.
Go has been designed with concurrency in mind and has better tools than, for example, Java or C,
but concurrent programming is not trivial and you should know what you are doing.
Look for tutorials on the matter.

In your case, it would be ok, for example, if you could split your set of entities in multiple subsets and
assign each subset to a different goroutine. The goroutines would work in parallel, but each one would mind it's own business
(not mess with data belonging to the others).

Tim K

unread,
Apr 2, 2016, 8:47:14 PM4/2/16
to golang-nuts, zachary...@gmail.com
This is a good article to go through, though it may feel a bit scientific:
https://golang.org/ref/mem

Pay particular attention to "Happens Before", that is the key to understanding data races.

Sugu Sougoumarane

unread,
Apr 2, 2016, 9:05:30 PM4/2/16
to golang-nuts, zachary...@gmail.com
Performing CPU-bound operations typically happens in 10s of nanoseconds, maybe 100s. This means that you can perform something like 10M operations in 1s.
OTOH, trying to prove that your lock-free program is not racy can get very difficult, probably unmaintainable over time.

Are you trying to update memory at this scale, or is something else in your code time-consuming?

If it's the former (you're truly CPU-bound), I'd recommend adding a mutex to Entity and using that to update members.
For the latter, you should have a single lock for the entire map, perform your slow computation first, and obtain the global lock just for setting the necessary values.

zachary...@gmail.com

unread,
Apr 3, 2016, 4:30:15 AM4/3/16
to golang-nuts, zachary...@gmail.com
I'm just recapping here to get stuff straight in my head. Right now the way I have it set up is that one goroutine loops through and updates the entities, possibly removing ones that should be removed. Another goroutine will just loop through and send it's data to other players. So the issues your pointing out are that it's editing the same memory at the same time.

But how would I fix this? 

I tried using channels in the sense that I created a channel called funcChan
var funcChan = make(chan func())
And anything that edits the entities array I added to the channel
funcChan <- sendData //loops through and sends each entity's data to every player
and then in main I loop through the channel and call each function
for (alive) {
var my_recvd_value func()
my_recvd_value = <- funcChan
my_recvd_value()
}
But this basically makes using goroutines pointless if most of the stuff is just going to happen on the main thread.
This is the only way I could think of using channels to solve this problem. Is there a better way to do this?

I'm also not sure how I could implement something like your idea of having a goroutine manage certain sections of the entities array, because no matter what I do, I need at least one function that will loop through them all and send their data to all the players:(

zachary...@gmail.com

unread,
Apr 3, 2016, 4:38:26 AM4/3/16
to golang-nuts, zachary...@gmail.com
I think everything just adds up. At first everything was on goroutines which was fast but crashed a ton because I don't know how to sync everything. When I started adding functions back onto the main thread (via the channel method I posted earlier) it slowed down a bit when I added the "send" and "update" functions (which both loop through all the entities, updating and sending info), and then slowed down a ton when I added the physics ticker onto the main thread (which just calls physics.step) So I guess the physics ticker takes the most amount of time to be called.

Bakul Shah

unread,
Apr 3, 2016, 5:31:58 AM4/3/16
to zachary...@gmail.com, golang-nuts
Can't you use something like the following?

// pull off an entity from the in chan, process it and push it on the out chan
func update(in, out chan *Entity) {
for {
e := <- in
e.update()
out <- e
}
}

func updateAll(out, in chan* Entity, entities map[string]*Entity) {
for _, e := range entities {
out <- e
}
// wait till equal number of items are received
for i := len(entities); i > 0; i-- {
<- in
}
}

func main() {
...
in := make(chan *Entity, 100)
out := make(chan *Entity, 100)

// start up a few update goroutines
for i := 0; i < N; i++ {
go update(in, out)
}

// example: update all entities every 100ms
ticker := time.Tick(100 * time.Millsecond)
for now := range ticker {
updateAll(in, out, entities)
}
}

Multiple goroutines read from the in chan but only one
goroutine will get an entity to work on. The key point is to
have only one goroutine access/update an entity at any given
point in time. You can achieve this using mutexes or channels.

Roberto Zanotto

unread,
Apr 3, 2016, 7:56:49 AM4/3/16
to golang-nuts, zachary...@gmail.com
It's already a good thing if you identified the bottleneck. Do you implement your own physics or are you using a dedicated library?

zachary...@gmail.com

unread,
Apr 3, 2016, 3:16:27 PM4/3/16
to golang-nuts, zachary...@gmail.com

zachary...@gmail.com

unread,
Apr 3, 2016, 3:25:19 PM4/3/16
to golang-nuts, zachary...@gmail.com
Wow thank you so much for this sample code it's super helpful!! I'm trying to understand it and this part is confusing me...
 for i := len(entities); i > 0; i-- {
         <- in
 }
What exactly does <-in do? I thought that popped out an object from the channel, but it looks like you are popping an object but not doing anything with it? 

Bakul Shah

unread,
Apr 3, 2016, 4:32:47 PM4/3/16
to zachary...@gmail.com, golang-nuts
On Sun, 03 Apr 2016 12:25:03 PDT zachary...@gmail.com wrote:
>
> Wow thank you so much for this sample code it's super helpful!! I'm trying
> to understand it and this part is confusing me...
> for i := len(entities); i > 0; i-- {
> <- in
> }
> What exactly does <-in do? I thought that popped out an object from the
> channel, but it looks like you are popping an object but not doing anything
> with it?

updateAll just wants to check that all the entities were
processed so the actual received value is not useful and
thrown away. This is just some sample code to give you an
idea.

Roberto Zanotto

unread,
Apr 4, 2016, 2:48:39 AM4/4/16
to golang-nuts, zachary...@gmail.com
The physics step needs to read and update all entities at once I suppose, so either chipmunk gives you some way of parallelizing the work, or there's noting you can do about it.

About the "update" function, it depends on what you are doing there. Does each entity have to interact with every other? Are you using an n^2 algorithm?

About the "send" function, it should be possible to parallelize it easily. I mean, _after_ the data has been updated, you can have as many goroutines as you want reading the data concurrently and sending it over the network.

Also, it may help to use a map[string]Entity instead of a map[string]*Entity, or even a plain array, so that the memory you are working on is contiguous and you pay less cache faults.
If you want performance, go for a data-oriented design instead of an object-oriented design.

Andreas Nilsson

unread,
Apr 6, 2016, 5:28:38 PM4/6/16
to golang-nuts, zachary...@gmail.com
Just wanted to add that you could do that, using a sync.RWMutex to protect the data; or even using atomic ops. But I'd save that as a last resort. Sharing data between threads is always a last resort and Golang has excellent tools to cut problems other ways. It's not always obvious how though. What generally works best for me is moulding these kinds of problems into jobs and stuffing jobs into channels/work queues, possibly with a feedback loop to know when the job is finished. That way you can at least get a guarantee that no two threads start working on the same job, no matter how many queue processors you add. Using that fact to your advantage and selecting the most suitable jobs to maximize concurrency is more art than science. Good luck.


Den lördag 2 april 2016 kl. 22:29:24 UTC+2 skrev Zac Diericx:
So what if one goroutine is editing the entities and one is just grabbing the data (to send to other players), would that be okay? 
th

Zac Diericx

unread,
Apr 6, 2016, 7:09:00 PM4/6/16
to golang-nuts, zachary...@gmail.com
Alright I think I figured out the problem, thanks to everyone's help her!!
Here is my model now:
var playerInput = make(chan PlayerDataObject, 100)
var serverOutput = make(chan PlayerDataObject, 100)
var entityIn = make(chan *Entity, 100)
var entityOut = make(chan *Entity, 100)

for
{
processServerInput() //loops through player input channel and processes info
physics.stepPhysics() //steps the physics
updateEntities() //loops through entityIn/Out channels processing entities using Bakul Shah's method
processServerOutput() //creates all of the packets and puts them in the server output channel
sendServerOutput() //sends all of the server output
}

This way everything happens in order and the slices aren't every being accessed simultaneously:) 
Reply all
Reply to author
Forward
0 new messages