More Threads Explained

| | Comments (14)

Due to the number of questions I've gotten from people on the topic, I'm going to spend some time delving into this topic.

Let's start off by familiarizing ourselves with the structure of the Thread class. It has four properties, two of which are new to RB 2005, it has four methods, three of which are new to RB 2005, and finally, it has a single event.

Properties

Priority as Integer
This is one of the new RB 2005 features which allows you to control a thread's priority relative to the other threads in the application. The first thing to remember is that there's always one thread in an application -- the main thread. And it's priority is defaulted to 5 (and is immutable). So if you want your thread to have twice as much CPU time as the main thread, then you'd set your thread's priority to be 10. Now it gets 10 time slices, and the main thread only gets 5. In this fashion, you can determine which threads get how much time devoted to them.

The way priority works under the hood is simple. The entire application has X time slices, where X is the sum of the priorities of all running threads. So if you have just the main thread, then there are 5 time slices and the main thread executes during all of them. If you have a second thread with a priority of 7, then there are 12 time slices for the application, 5 for the main thread and 7 for the second thread. You cannot rely on the time slices being in any order -- the order could be:

Main Thread
ThreadA
Main Thread
Main Thread
ThreadA
ThreadB
Main Thread

We do not document any order for time slices -- just that we will honor the relative priorities of all threads. If you've got a priority of 7, you will get 7 time slices out of the total.

StackSize as Integer
This is a property that you won't need to use very often, but when you need to use it -- it's obvious. Each thread has its own local stack space. This is a place for things like local variables, static variables, etc. When you call new SomeObject, its memory is allocated from the heap (not the stack), however, the reference (which is 4 bytes) may be held in a local variable which is on the stack. Another, more common, item that resides on the stack are the stack frames created when you call a method.

By default (by specifying a StackSize of 0), REALbasic uses the OS default thread stack size. This means that threads on the Mac and Linux have 64k of stack space, and on Windows, it's 904k. But there are times when you'll have a lot of items on the stack, and you'll begin to get StackOverflowExceptions.

If you're going to be using a thread to do recursive method calls, then it's likely (depending on the level of recursion) that you'll need to raise the StackSize for the thread. How much you need to raise it by is mostly educated guess-work and depends entirely on how much data is going to be stored on the stack.

State as Integer
Threads can be in any one of a number of different states, depending on the application. The thread could be in the middle of execution, sleeping due to a call to Sleep, suspended because of a Semaphore, etc.

When you create a thread (but haven't called Run yet), it starts out in the Thread.NotRunning (which is integer value 4). Once you call the Run method and the thread begins executing in its own stack space, it will be in the Running (integer value 0) state. There are three different state values for the different reasons a thread is not running: Waiting, Suspended and Sleeping.

A thread is in the Waiting state (integer value 1) when it's been suspended because it's been blocked by a locking mechanism (such as a Semaphore or a CriticalSection). This is a temporary state -- the thread will go back to running once the locking mechanism has determined that it's safe to do so.

The other temporary not-running state is when a thread is Sleeping (integer value 3). A thread will be in this state when you call Thread.Sleep and it will automatically go back to running when the sleep conditions have been satisfied.

Finally, there's the Suspended state (integer value 2). Once a thread is in this state, it will not be automatically resumed. You must call the Resume method on the thread in order to start the execution again.

One state that the State property does not track is whether a thread is currently executing within its time slice or waiting to be executed because another thread has control of the CPU. We do not track this state because of the amount of overhead it would add to context switches, and it's not state information that's particularly useful under normal circumstances.

ThreadID as Integer
The only point to this property is for tracking and reporting purposes. It's not a Handle property that you can use in any calls to the OS. It's just a unique integer so that you can differentiate one thread from another when logging.

Methods

Run()
When an application calls Thread.Run, what really happens is that the framework sets up the thread's stack frame and transfers execution into the thread's entrypoint. It just so happens that the thread's entrypoint is its Run event. So, in basic terms, when you call Run, the underlying thread is created and it begins to execute, starting at the Run event. Once you leave the Run event of a thread, it's done executing and its final stack frame is torn down.
Suspend()
This method halts the execution of a running thread (or starts a thread off in a suspended state if you call it before calling the Run method) and puts it into the Suspended state. When you want the thread to go back to executing, then you call Resume on it.

This method is very handy when you know for certain that the thread doesn't need to be running. For example, if your thread's job is to begin processing data when the user has entered 50 characters into an EditField, then you can suspend the thread if Len( EditField.Text ) is less than 50 since there's no reason for the thread to be running.

Suspending threads instead of just sleeping them for long periods of time also saves you some cycles since it's easier for the thread scheduler to determine that the thread shouldn't run. It's a minor thing though.

Typically, external input decides to suspend a thread. Threads don't usually suspend themselves because they would have no automatic way to be resumed.

Sleep( milliseconds as Integer, WakeEarly as Boolean )
This method suspends the execution of a thread for the specified amount of time (at most) and puts it into the Sleeping state. Basically, this is a way for you to tell the thread scheduler "this thread doesn't need to do anything for X amount of time." The second parameter to this method makes it truly useful -- you can tell the thread scheduler "if you have no other threads running, you can go ahead and start this one up before X milliseconds have elapsed." Basically, you can wake this thread up early.

In contrast to Suspend, a thread usually puts itself to sleep when it has nothing better to be doing. It can do this because it knows that at some point in the future, the thread scheduler will wake it back up so that it can continue executing. For example, let's say you have a thread which checks spelling in an EditField. The thread can put itself to sleep if the text of the EditField hasn't changed recently. Whenever it wakes up, it can check to see if there's any changes to the text so that it needs to spell check. This way your thread isn't taking up CPU time and just sitting idle for it.

I should also note that you can wake a sleeping thread up manually by calling Resume on it.

Resume()
As the name implies, this method resumes a suspended (or sleeping) thread. However, it does not cause an immediate context switch -- it just flags the thread so that the scheduler can resume it later.

That's it for today, but next time I'll discuss some of the thread support functions and what they're used for.

14 Comments

So what state is a Thread that has run it's full course and is complete? Does it go back to NotRunning?

It goes back to the NotRunning state. Basically, a thread is in that state before you've called the Run method and after you've exited the Run event.

What are the "initialparent" and "tabpanelIndex" properties?

They only seem to show up in the properties panel after the thread control has been dragged and dropped. Drag a thread onto the canvas next to your window and drop it... Then grab it again and drag it a bit and let go - voila` you will see two additional properties in the properties pane (XPpro RB05r2). I would 'bug it' but I'm not sure if the error is that the properties appear after being dragged or don't appear after being initially set (or even after losing focus on the thread control).

They're things that can safely be ignored -- they shouldn't be showing up for Threads in the first place. Those are properties for visible controls, and I'm not certain they even should be showing at design time at all. Feel free to file a bug report against it.

These articles about threads is really helping a lot... thanks!

Maybe RS could use this article for the Users Guide and also for the LR :)

Glad to hear they're helping! I'm going to try to churn out at least one more article on the topic, then compile them into a whitepaper. Perhaps I'll do a REAL World talk or something about the topic. :-)

The articles are GREAT! I'm not sure if it's appropriate but operating under the "it's better to ask forgiveness than ask permission" philosophy I'm going to note that I have filed the bug report - mzhmcmzx

There's one thing missing from the threads (IMHO), a reset method. The only other way to do this is overwrite your thread instance with a new instance -- which isn't doable for threads in a window without refactoring.

-- SirG3

I'm not certain I understand -- what's a Reset method supposed to do?

Reset the thread back to the state where the run method can be called again, without having to have the code in the run event finished executing.

-- SirG3

I can honestly say that I've never (in any language) needed to do such a thing. In what situation is this sort of thing needed? And what's wrong with just exiting the Run event?

Well, exiting the run event requires you to place things in the event to check for exit conditions. For example, if you're doing calculation on text pasted in an editfield. You'd want to be able to reset the thread when the text in the editfield changes (because if you're dealing with a huge ammount of text, it might still be processing).

-- SirG3

You're welcome to feature request it, but I have a hard time believing it's a needed API change, especially given that I've never seen a toolkit that allows that.

Taking your example, the proper way to do that would be to suspend the thread on paste, clear its state information and give it the new state information:

EditField.SpiffyPasteHandler()
SpiffyThread.Suspend
SpiffyThread.SetBuffer( me.Text )
SpiffyThread.Resume
End Sub

SpiffyThread.SetBuffer( newText as String )
mCurPos = 0
mNeatStateData = 0

mBuffer = newText
End Sub

Doing something like this means that your thread stops running (on Suspend), sets all its needed state information, buffers, etc, and then gets put back into the queue of runnable threads. It's much cleaner, and more efficent than a Reset method would be (because Reset would have to tear down all the current stack frames, constuct and entirely new stack, allocate the StackSize amount of memory, and then transfer execution into the thread).

I should also point out something (thanks to Stephen Tallent for the off-list mention of this topic) that I don't think I made clear enough.

All events fire on the main thread except the Thread.Run event. All. Events. So if you make a TCPSocket subclass in code within a Thread's Run event, the socket's events will only fire when the main thread has its time slice.

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.