C H A P T E R21 Multi-Threaded Tcl Scripts
This chapter describes the Thread extension for creating multi-threaded Tcl scripts.
This Chapter is from Practical Programming in Tcl and Tk, 4th Ed.
Copyright 2003 © Brent Welch, Ken Jones
/book/
T hread support, a key feature of many
languages, is a recent addition to Tcl. That’s because the Tcl event loop supports features implemented by threads in most other languages, such as graphical user interface management, multi-client servers, asynchronous communication, and scheduling and timing operations. However, although Tcl’s event loop can replace the need for threads in many circumstances, there are still some instances where threads can be a better solution:
•Long-running calculations or other processing, which can “starve” the event loop
•Interaction with external libraries or processes that don’t support asynchro-nous communication
•Parallel processing that doesn’t adapt well to an event-driven model •Embedding Tcl into an existing multi-threaded application
What are Threads?
Traditionally, processes have been limited in that they can do only one thing at a time. If your application needed to perform multiple tasks in parallel, you designed the application to create multiple processes. However, this approach has its drawbacks. One is that processes are relatively “heavy” in terms of the resources they consume and the time it takes to create them. For applications that frequently create new processes — for example, servers that create a new
321
322 Multi-Threaded Tcl Scripts    Chap. 21 process to handle each client connection — this can lead to decreased response time. And widely parallel applications that create many processes can consume so many system resources as to slow down the entire system. Another drawback is that passing information between processes can be slow because most inter-process communication mechanisms — such as files, pipes, and sockets —involve intermediaries such as the file system or operating system, as well as requiring a context switch from one running process to another.
Threads were designed as a light-weight alternative. Threads are multiple flows of execution within the same process. All threads within a process share the same memory and other resources. As a result, creating a thread requires far fewer resources than creating a separate process. Furthermore, sharing informa-tion between threads is much faster and easier than sharing information between processes.
The operating system handles the details of thread creation and coordina-tion. On a single-processor system, the operating system allocates processor time to each of an application’s threads, so a single thread doesn’t block the rest of the application. On multi-processor systems, the operating system can even run threads on separate processors, so that threads truly can run simultaneously.
The drawback to traditional multi-threaded programming is that it can be difficult to design a thread-safe application — that is, an application in which one thread doesn’t corrupt the resources being used by another thread. Because all resources are shared in a multi-threaded application, you need to use various locking and scheduling mechanisms to guard against multiple threads modifying resources concurrently.
Thread Support in Tcl
Tcl added support for multi-threaded programming in version 8.1. The Tcl core was made thread-safe. Furthermore, new C functions exposed “platform-neutral”thread functionality. However, no official support was provided for multi-threaded scripting. Since then, the Thread extension — originally written by Brent Welch and currently maintained by Zoran Vasiljevic — has become the accepted mechanism for creating multi-threaded Tcl scripts. The most recent version of the Thread extension as this was being written was 2.5. In general, this version requires Tcl 8.3 or later, and several of the commands provided require Tcl 8.4 or later.
At the C programming level, Tcl’s threading model requires that a Tcl interpreter be managed by only one thread. However, each thread can create as many Tcl interpreters as needed running under its control. As is the case in even a single-threaded application, each Tcl interpreter has its own set of variables and procedures. A thread can execute commands in another thread’s Tcl inter-preter only by sending special messages to that interpreter’s event queue. Those messages are handled in the order received along with all other types of events.
Thread Support in Tcl323 Obtaining a Thread-Enabled Tcl Interpreter
Most binary distributions of Tcl are not thread-enabled, because the default
options for building the Tcl interpreters and libraries do not enable thread sup-
port. Thread safety adds overhead, slowing down single-threaded Tcl applica-
tions, which constitute the vast majority of Tcl applications. Also, many Tcl
extensions aren’t thread safe, and naively trying to use them in a multi-threaded
application can cause errors or crashes.
Unless you can obtain a thread-enabled binary distribution of Tcl, you must
compile your own from the Tcl source distribution. This requires running the configure command with the --enable-threads option during the build pro-cess. (See Chapter 48, “Compiling Tcl and Extensions” for more information.)
You can test whether a particular Tcl interpreter is thread-enabled by
checking for the existence of the tcl_platform(threaded) element. This ele-
ment exists and contains a Boolean true value in thread-enabled interpreters,
whereas it doesn't exist in interpreters without thread support.
Using Extensions in Multi-Threaded Scripts
Because each interpreter has its own set of variables and procedures, you
must explicitly load an extension into each thread that wants to use it. Only the Thread extension itself is automatically loaded into each interpreter.
You must be careful when using extensions in multi-threaded scripts. Many
Tcl extensions aren’t thread-safe. Attempting to use them in multi-threaded
scripts often results in crashes or corrupted data.
Tcl-only extensions are generally thread-safe. Of course, they must make
no use of other commands or extensions that aren’t thread-safe. But otherwise,
multi-threaded operation doesn’t add any new issues that don't already affect
single-threaded scripts.
You should always assume that a binary extension is not thread-safe unless
its documentation explicitly says that it is. And even thread-safe binary exten-
sions must be compiled with thread support enabled for you to use them in
multi-threaded applications. (The default compilation options for most binary
extensions don’t include thread support.)
Tk isn’t truly thread-safe.
Most underlying display libraries (such as X Windows) aren’t thread safe —
or at least aren’t typically compiled with thread-safety enabled. However, signif-
icant work has gone into making the Tk core thread-safe. The result is that you
can safely use Tk in a multi-threaded Tcl application as long as only one thread
uses Tk commands to manage the interface. Any other thread that needs to
update the interface should send messages to the thread controlling the inter-
face.
324 Multi-Threaded Tcl Scripts    Chap. 21 Getting Started with the Thread Extension
You start a thread-enabled tclsh or wish the same as you would a non-threaded tclsh or wish. When started, there is only one thread executing, often referred to as the main thread, which contains a single Tcl interpreter. If you don’t create any more threads, your application runs like any other single-threaded applica-tion.
Make sure that the main thread is the last one to terminate.
The main thread has a unique position in a multi-threaded Tcl script. If it exits, then the entire application terminates. Also, if the main thread terminates while other threads still exist, Tcl can sometimes crash rather than exiting cleanly. Therefore, you should always design your multi-threaded applications so that your main thread waits for all other threads to terminate before it exits.
Before accessing any threading features from your application, you must load the Thread extension:
package require Thread
The Thread extension automatically loads itself into any new threads your application creates with thread::create. All other extensions must be loaded explicitly into each thread that needs to use them. The Thread extension creates commands in three separate namespaces:
•The thread namespace contains all of the commands for creating and man-aging threads, including inter-thread messaging, mutexes, and condition variables.
•The tsv namespace contains all of the commands for creating and managing thread shared variables.
•The tpool namespace contains all of the commands for creating and manag-ing thread pools.
Creating Threads
The thread::create command creates a new thread containing a new Tcl interpreter. Any thread can create another thread at will; you aren’t limited to starting threads from only the main thread. The thread::create command returns immediately, and its return value is the ID of the thread created. The ID is a unique token that you use to interact with and manipulate the thread, in much the same way as you use a channel identifier returned by open to interact with and manipulate that channel. There
are several commands available for introspection on thread IDs: thread::id returns the ID of the current thread; thread::names returns a list of threads currently in existence; and thread::exists tests for the existence of a given thread.
The thread::create command accepts a Tcl script as an argument. If you provide a script, the interpreter in the newly created thread executes it and then terminates the thread. Example 21–1 demonstrates this by creating a thread to perform a recursive search for files in a directory. For a large directory structure,
Getting Started with the Thread Extension325 this could take considerable time. By performing the search in a separate thread, the main thread is free to perform other operations in parallel. Also note how the “worker” thread loads an extension and opens a file, completely independent of any extensions loaded or files opened in other threads.
Example 21–1Creating a separate thread to perform a lengthy operation.
package require Thread
# Create a separate thread to search the current directory
# and all its subdirectories, recursively, for all files
# ending in the extension ".tcl". Store the results in the
# file "".
thread::create {
# Load the Tcllib fileutil package to use its
# findByPattern procedure.
package require fileutil
set files [fileutil::findByPattern [pwd] *.tcl]
set fid [ w]
puts $fid [join $files \n]
close $fid
}
# The main thread can perform other tasks
If you don’t provide a script argument to thread::create, the thread’s interpreter enters its event loop. You then can use the thread::send command, described on page 328, to send it scripts to evaluate. Often though, you’d like to perform some initialization of the thread before having it enter its event loop. To do so, use the thread::wait command to explicitly enter the event loop after per-forming any desired initialization, as shown in Example 21–2. You should always use thread::wait to cause a thread to enter its event loop, rather than vwait or tkwait, for reasons discussed in “Preserving and Releasing Threads” on page 330.
Example 21–2Initializing a thread before entering its event loop.
set httpThread [thread::create {
package require http
thread::wait
}]
After creating a thread, never assume that it has started executing.
There is a distinction between creating a thread and starting execution of a thread. When you create a thread, the operating system allocates resources for
326 Multi-Threaded Tcl Scripts    Chap. 21 the thread and prepares it to run. But after creation, the thread might not start execution immediately. It all depends on when the operating system allocates execution time to the thread. Be aware that the thread::create command returns when the thread is created, not necessarily when it has started. If your application has any inter-thread timing dependencies, always use one of the thread synchronization techniques discussed in this chapter.
Creating Joinable Threads
Remember that the main thread must be the last to terminate. Therefore you often need some mechanism for determining when it’s safe for the main thread to exit. Example 21–3 shows one possible approach: periodically checking thread::names to see if the main thread is the only remaining thread.
Example 21–3Creating several threads in an application.
package require Thread
puts "*** I'm thread [thread::id]"
# Create 3 threads
for {set thread 1} {$thread <= 3} {incr thread} {
set id [thread::create {
# Print a hello message 3 times, waiting
# a random amount of time between messages
for {set i 1} {$i <= 3} {incr i} {
after [expr { int(500*rand()) }]
puts "Thread [thread::id] says hello"
}
}] ;# thread::create
puts "*** Started thread $id"
} ;# for
puts "*** Existing threads: [thread::names]"
# Wait until all other threads are finished
while {[llength [thread::names]] > 1} {
after 500
}
puts "*** That's all, folks!"
A better approach in this situation is to use joinable threads, which are supported in Tcl 8.4 or later. A joinable thread allows another thread to wait upon its termination with the thread::join command. You can use

更多推荐