🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Standing atop a mountain of slain bugs

Published March 09, 2009
Advertisement
As predicted, solving the multithreading issues has proven very interesting.

The scenario is fairly intricate, but easy enough to understand in hindsight. Here's the rundown:

  • Each code block is attached to a lexical scope - every time a { } block appears, a new scope is created by the compiler

  • Each lexical scope contains some metadata: the types of each variable, what kind of structures are present, and so on

  • Included in this metadata is a set of pointers that indicate where each variable is stored. Typically this points to a location on the stack

  • In a single-threaded environment, this works fine: each time a lexical scope is entered during execution, we bind the variables to the stack, and proceed as normal

  • In a multi-threaded environment, this ceases to work, because one thread can clobber another thread's scope metadata


Now, solving this involved two primary requirements:

  • I do not want any kind of locking in the multiprocessing code

  • Threads must be able to intelligently recreate hierarchies of nested scopes


These goals are somewhat contradictory; how can we examine the scope hierarchy and copy scope metadata without locking? The solution is, as often happens, obvious in hindsight, but it took me a couple of hours to really figure it out and nail it down.

The solution is that we now have multiple copies of each scope metadata. There is one "master copy" which is never mutated by any code. Each time a scope is entered, a copy of the metadata is made. That copy is never passed to anyone outside the thread.

Since there is no change ever permitted to the master copy, we can read from it any time from a thread without needing to worry about locking. Since each thread has its own copy, and that copy is never shared outside the thread, we don't need to lock to use the copy of the metadata.

This leaves us with the question of rebuilding the scope hierarchy. For this, we take advantage of the fact that the lexical scope hierarchy is always traversed from top (most global scope) to bottom (most local scope). All we have to do is fork a copy of the scope metadata right before we enter that scope, and clean it up when we exit the scope. Since the traversal always follows this pattern, we always have a correct chain of parent/child relationships in the metadata.


Although the approach works, it kind of sucks. There's a need for many copies of the scope metadata, which increases memory consumption for comparatively little benefit. Worse, it costs performance - every function invocation or flow control block now has to do a scope copy on top of the other overhead.

I haven't really had a chance to think out ways to improve this, but I'm sure over the next week or so I'll have plenty of chances to refine the approach.


For now, my outstanding bug list has several issues related to this change; in particular, I broke an awful lot of scoping code, so I have to go check every permutation of code that involves lexical scopes, and ensure that things work correctly. Worse, I've introduced several memory leaks which will probably be ugly to hunt down across a multithreaded runtime.

On the plus side, I don't have to worry about being bored [grin]
Previous Entry Woohoo!
Next Entry Yawn
0 likes 4 comments

Comments

Telastyn
I'm (naively) not sure that solves your problems. You'll run into problems merging the copies back into the trunk if you've got multiple ones that mutate shared state (local vars).
March 09, 2009 05:03 PM
ApochPiQ
That's the beauty of it - mutated states are always contained to their host task. The only way to share state across task boundaries would be through the global, top-level scope, and that is forbidden (or, more accurately, will be forbidden once I get the checks implemented). A given scope's metadata block will never need to be used by any scope outside the task, so there's no need to merge the data back in to the master copy.

Transferring data between tasks will probably be done with an Erlang-style messaging model. Of course that introduces some potentially sticky situations regarding who owns the message data and when; but that's fairly straightforward to take care of.
March 09, 2009 05:40 PM
Telastyn
So something like this (ignoring syntactic mistakes) is forbidden?
Or will 2 always get printed?


entrypoint : () -> ()
{
        int(foo, 2)

	task(asyncjob1)
	{
		assign(foo, add(foo, foo))
	}

	task(asyncjob2)
	{
		assign(foo, mul(foo, foo))
	}

        debugwritestring(foo)
}



The second is perhaps sensible, but wouldn't that require every object to have copy-semantics?
March 09, 2009 05:50 PM
ApochPiQ
That specific case would throw undefined variable errors in each task block; the surrounding scope is not inherited by a task. Passing parameters into a task will be based on the message passing model, but at the moment I'm too bogged down in debugging and memory leak hunting to come up with a potential syntax for it [smile]
March 09, 2009 06:32 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement