TOP | UP: Data Manipulation | NEXT: Stacks |
The Mac OS can run more than one process at the same time (multitasking), and one process can run more than one thread at the same time (multithreading). The UserTalk verbs discussed in this chapter allow Frontier to behave as a good Mac OS citizen by yielding time to other processes, and to control its own threads to accomplish several things at once. We also explain semaphores, a simple but effective device for preventing collisions among such simultaneous activities.
During a looping operation, especially when polling until some condition is met, you may wish to yield time to other processes, including giving the system a chance to update the screen; this is simply what all good citizen processes do from time to time on computers that lack true multitasking. To do so, call sys.systemTask() .
To pause for a fixed interval, call clock.waitSixtieths() or clock.waitSeconds() , depending on whether you'd like to specify the interval in sixtieths of a second or in seconds. These verbs are just like calling sys.systemTask() repeatedly until a certain time has elapsed.
A thread is a miniature process running within Frontier. Frontier is multithreaded, which means in essence that it can do several things at once. This feature is particularly valuable in connection with Frontier's ability to drive and be driven by other applications, because it lets Frontier maintain multiple active lines of communication. For instance, it makes Frontier a natural choice as a CGI application; Frontier can handle a server request while already processing other such requests.
Choosing the StatusMessage item from the agent selection popup at the left end of the Main Window displays a message which provides (among other things) a constantly updated count of the number of running threads. The "1 thread" present when Frontier is idle is the agent thread, in which Frontier's background processes are running. (For more about agents, see Chapter 27, Agents and Hooks.) Over and above this, Frontier automatically creates a new thread for every script initiated either by a human being or by another application; when the script finishes executing, its thread is destroyed.
For example, type clock.waitSeconds(10) into the Quick Script window and click the Run button. For the next 10 seconds or so, the Main Window's count of "1 thread" will be increased to "2 threads." Now let workspace.testing() go like this:
clock.waitSeconds(10)
Click the Run button in workspace.testing 's window, quickly switch to the Quick Script window and click its Run button. The Main Window now reports "3 threads." The thread count returns to 1 when both commands have terminated separately.
Next, start up another application that can speak to Frontier in AppleScript, such as the Script Editor or HyperCard. Suppose it's HyperCard. Make a new stack with a new button, open the button's script, use the popup at the top of the script edit window to change the language to AppleScript, and make the script read as follows:
on mouseUp()
tell application "UserLand Frontier?" to do script "clock.waitseconds(10)"
end mouseUp
Close and save the script. Press the button to start the script, and switch to Frontier; the thread count climbs to 2 while the script is running.1
To learn how many threads are running:
thread.getCount ()
Simply calling a script from within a script does not generate a new thread; the caller pauses until the called script returns, so execution is linear, within a single thread. To enable a script to call another script so that the latter runs in a thread of its own, a verb thread.evaluate() is provided. The parameter is a string; that string is evaluated as a UserTalk expression, just like evaluate(), but it is evaluated from within a new separate thread. For example, let workspace.testing() be this:
thread.evaluate("workspace.testing2()")
clock.waitseconds(10)
And let workspace.testing2() be this:
clock.waitseconds(20)
Press the Compile button in both windows, make sure the "1 thread" message is visible in the Main Window, and press the Run button in workspace.testing. The thread count goes up to 3 for 10 seconds, because both scripts are running as individual threads; then comes down to 2 for 10 seconds, after workspace.testing() has finished; and finally returns to 1.
Since the parameter of thread.evaluate() is most often a verb call, and since it can be tiresome to construct a string representing a verb call with parameters, there is a utility script, toys.threadCall() . It takes the name of a verb (as a string) and a list of the actual parameters to be fed to that verb; it constructs the verb call as a string for you, and feeds it to thread.evaluate().
The result of a call to thread.evaluate() is not the result of evaluating its parameter as a UserTalk expression, because the whole meaning of spawning a new thread is that the calling script doesn't wait for the new thread to return a result; the two scripts part company, as it were, and proceed simultaneously. To capture the result from the new thread, call instead thread.evaluateTo(), which takes the address of a database entry in which to store the result when the thread ultimately finishes.2
What, then, is the result of a call to thread.evaluate() or thread.evaluateTo()? It is a number identifying the new thread. The calling script will need this only if it intends to manage threads manually - which is the topic of the next section.
The easiest way to manage threads is not to do so. Frontier manages threads automatically; taking control of threads yourself is complicated, rather as if control of an unconscious, involuntary bodily function, such as your liver, were suddenly entrusted to your conscious self. Nevertheless, it is sometimes necessary to take charge of threads - typically, where one wishes to maintain efficiency while awaiting a response from elsewhere (the Internet, for instance).3
For most thread verbs, you'll need to supply the ID number of a particular thread. The script that creates a thread can learn its ID number by capturing the result of thread.evaluate() or toys.threadCall() or thread.evaluateTo(). For other situations, further verbs are provided.
To learn the ID of the thread within which one is:
thread.getCurrentID ()
To learn the ID of the nth thread:
thread.getNthID (n)
To learn whether any thread has a given ID:
thread.exists (ID)
Once you have the ID of a thread, there are two sorts of thing you can do with that thread. One is to abort it.
thread.kill (ID)
For example, let's spawn an endless thread and then kill it. Let workspace.testing() go like this:
loop
local (x)
for x = 0 to 9
msg(x + " I am thread " + thread.getCurrentID() + ".")
clock.waitSixtieths(10)
and click the Run button. In the Main Window, you can see the thread cycling, and also learn its ID. Open Quick Script, type thread.kill( ID) (putting in the actual number of the running thread's ID) and hit the Run button. The counting in the Main Window stops; the thread is dead.
The other thing to do to a thread is put it to sleep. A thread that is put to sleep pauses and waits to be woken before executing its next command.
thread.sleep (ID)
thread.wake (ID)
To put to sleep the thread in which one is, either until woken or until a certain number of seconds passes, whichever comes first:
thread.sleepFor (seconds)
To learn whether a given thread is sleeping:
thread.isSleeping (ID)
As an example, let's have workspace.testing() put itself to sleep. Modify workspace.testing() so it says:
loop
local (x)
for x = 0 to 9
msg(x + " I am thread " + thread.getCurrentID() + ".")
clock.waitSixtieths(10)
thread.sleep(thread.getCurrentID())
Now press Run. In the Main Window, we count from 0 to 9 and stop. Using the ID information from the Main Window, type into QuickScript thread.wake( ID) and press Quick Script's Run. The counting in the Main Window happens again, and pauses again until you hit Quick Script's Run again. Play this game as long as you like; when you tire of it, use Quick Script to kill the thread.
Threads are tracked in system.compiler.threads. Since the thread verbs provide a programming interface, you should have no reason to touch system.compiler.threads directly; but it is interesting to open it and watch the table change while playing with threads.
A semaphore is a device to enable cooperation among threads so that one thread doesn't try to access something in a way that could damage another thread's use of that same thing.
Here is a trivial example, just to illustrate the principle. Suppose workspace.testing() looked like this:
local (x)
workspace.ct = 0
thread.evaluate("workspace.testing2()")
loop
x = workspace.ct++
msg(x + ": I am thread " + thread.getCurrentID() + ".")
clock.waitSixtieths(30)
if x > 100
break
And suppose workspace.testing2() looked like this:
local (x)
loop
x = workspace.ct++
msg(x + ": I am thread " + thread.getCurrentID() + ".")
clock.waitSixtieths(40)
if x > 100
break
If we now run workspace.testing(), it spawns off workspace.testing2() as a separate thread. Both routines are looping simultaneously, and both routines will be repeatedly incrementing the value of the same database entry, workspace.ct. This is a bad thing. What if some of these accesses occur simultaneously? Will the results be messed up? Will Frontier crash?
Semaphores solve this kind of problem neatly and simply. There are two semaphore verbs.
To lock a semaphore, first waiting until it is unlocked:
semaphores.lock (name, timeOutTick)
semaphores.unlock (name)
To return to our earlier example, suppose workspace.testing() looks like:
local (x)
workspace.ct = 0
thread.evaluate("workspace.testing2()")
loop
semaphores.lock("workspace.ct", 600)
x = workspace.ct++
semaphores.unlock("workspace.ct")
msg(x + ": I am thread " + thread.getCurrentID() + ".")
clock.waitSixtieths(30)
if x > 100
break
loop
semaphores.lock("workspace.ct", 600)
x = workspace.ct++
semaphores.unlock("workspace.ct")
msg(x + ": I am thread " + thread.getCurrentID() + ".")
clock.waitSixtieths(40)
if x > 100
break
Compile both scripts and then press workspace.testing's Run button. In the Main Window, you will see that the count increments smoothly even though it is sometimes one thread, sometimes the other that increments it. The threads are making cooperative shared use of a single entity.
Any time you are using multiple threads in Frontier, you should consider whether you need a semaphore to keep them from trying to access the same entity simultaneously. Here, our example was a database entry. Another might be a file on disk. Another might be a line of communication to an application that isn't multithreaded.
Semaphores are purely a voluntary, cooperative device. There is nothing magical about semaphores.lock(); it does not in fact lock anything! When we say:
semaphores.lock("workspace.ct",600)
access to workspace.ct is unaffected. In fact, the string "workspace.ct" is chosen arbitrarily, just to give this semaphore a unique, consistent name; we could equally have called it "redFlannelUnderwear". The use of semaphores is nothing more than an agreement to be polite and safe; there will be politeness and safety so long as every running thread makes the same agreement.
Here is how semaphores really work. There is a semaphore lock registry, which is the table at system.compiler.semaphores ; locking or unlocking a semaphore consists merely of adding or deleting its name in this table. A call to semaphores.lock() looks in the lock registry to see if the name is already there; if so, it waits until it isn't.4 Then, it adds the name to the registry. Semaphores.unlock() removes the name from the registry.
The danger with semaphores is failure to unlock one, which will cause any other thread that tries to lock the same semaphore to be unable to proceed. To guard against this, use try (and possibly else) in such a way that regardless of what happens the semaphore will be unlocked. The following structure is fairly typical:
semaphores.lock("mySem", 600)
try
« ... do interesting but risky stuff here ...
else
« oops, an error occurred! release the semaphore and rethrow the error
semaphores.unlock("mySem")
scriptError (tryError)
semaphores.unlock("mySem")
In case of trouble, the semaphores table can also be cleaned out manually, by quitting and restarting Frontier, or by choosing Unlock Semaphores from the Web menu.
One final note about the names of the verbs. Some scripts exhibit a certain confusion about this, calling semaphore.lock() and semaphore.unlock(), the singular instead of the plural. This is not an error, but it is wrong behavior; it accesses the kernel directly. The correct interface is semaphores.lock() and semaphores.unlock().
1. When a shared menu item is chosen in another application, or when an OSA-savvy application runs a UserTalk script, things are more complicated. The action takes place in the other application's context, and Frontier's thread count will not appear to rise; nonetheless, if the running script asks for the number of running threads, the count will be one more than Frontier's count.
2. It is illegal for the address to be that of a local variable, because the new thread would have to interrupt the calling script to set the variable's value (assuming that the variable is even still in scope!).
3. For an ingenious and highly educational example, see http://siolibrary.ucsd.edu/Preston/scripting/root/suites/VerifyURL.html. One obstacle to managing threads manually is that they are difficult to debug. The only thread that Frontier's debugger can step through is the one initiated by the pressing of the Debug button; threads spawned by that thread will simply run independently, beyond the debugger's control.
TOP | UP: Data Manipulation | NEXT: Stacks |