| [ Team LiB ] |
|
Managing I/O ChannelsChannels are shared resources in most programming languages. But in Tcl, channels are implemented as a per-interpreter resource. Only the standard I/O channels (stdin, stdout, and stderr) are shared. When running wish on Windows and Macintosh prior to OS X, you don't have real standard I/O channels, but simulated stdout and stderr channels direct output to the special console window. As of Thread 2.5, these simulated channels appear in the main thread's channel list, but not in any other thread's channel list. Therefore, you'll cause an error if you attempt to access these channels from any thread other than the main thread. Accessing Files from Multiple ThreadsIn a multi-threaded application, avoid having the same file open in multiple threads. Having the same file open for read access in multiple threads is safe, but it is more efficient to have only one thread read the file and then share the information with other threads as needed. Opening the same file in multiple threads for write or append access is likely to fail. Operating systems typically buffer information written to a disk on a per-channel basis. With multiple channels open to the same file, it's likely that one thread will end up overwriting data written by another thread. If you need multiple threads to have write access to a single file, it's far safer to have one thread responsible for all file access, and let other threads send messages to the thread to write the data. Example 21-9 shows the skeleton implementation of a logging thread. Once the log file is open, other threads can call the logger's AddLog procedure to write to the log file. Example 21-9 A basic implementation of a logging thread
set logger [thread::create {
proc OpenLog {file} {
global fid
set fid [open $file a]
}
proc CloseLog {} {
global fid
close $fid
}
proc AddLog {msg} {
global fid
puts $fid $msg
}
thread::wait
}]
Transferring Channels between ThreadsAs long as you're working with Tcl 8.4 or later, the Thread extension gives you the ability to transfer a channel from one thread to another with the thread::transfer command. After the transfer, the initial thread has no further access to the channel. The symbolic channel ID remains the same in the target thread, but you need some method of informing the target thread of the ID, such as a thread-shared variable. The thread::transfer command blocks until the target thread has incorporated the channel. The following shows an example of transferring a channel, and simply duplicating the value of the channel ID in the target thread rather than using a thread-shared variable: set fid [open myfile.txt r] # ... set t [thread::create] thread::transfer $t $fid # Duplicate the channel ID in the target thread thread::send $t [list set fid $fid] Another option for transferring channels introduced in Thread 2.5 is thread::detach, which detaches a channel from a thread, and thread::attach, which attaches a previously detached channel to a thread. The advantage to this approach is that the thread relinquishing the channel doesn't need to know which thread will be acquiring it. This is useful when your application uses thread pools, which are described on page 342. The ability to transfer channels between threads is a key feature in implementing a multi-thread server, in which a separate thread is created to service each client connected. One thread services the listening socket. When it receives a client connection, it creates a new thread to service the client, then transfers the client's communication socket to that thread. A complication arises in that you can't perform the transfer of the communication socket directly from the connection handler, like this:
socket -server ClientConnect 9001
proc ClientConnect {sock host port} {
set t [thread::create { ... }]
# The following command fails
thread::transfer $t $sock
}
The reason is that Tcl maintains an internal reference to the communication socket during the connection callback. The thread::transfer command (and the thread::detach command) cannot transfer the channel while this additional reference is in place. Therefore, we must use the after command to defer the transfer until after the connection callback returns, as shown in Example 21-10. Example 21-10 Deferring socket transfer until after the connection callback
proc _ClientConnect {sock host port} {
after 0 [list ClientConnect $sock $host $port]
}
proc ClientConnect {sock host port} {
# Create the client thread and transfer the channel
}
One issue in early versions of Tcl 8.4 was a bug that failed to initialize Tcl's socket support when a socket channel was transferred into a thread. The work-around for this bug is to explicitly create a socket in the thread (which can then be immediately closed) to initialize the socket support, and then transfer the desired socket. This bug has been fixed, but Example 21-11 illustrates how you can perform extra initialization in a newly created thread before it enters its event loop: Example 21-11 Working around Tcl's socket transfer bug by initializing socket support
set t [thread::create {
# Initialize socket support by opening and closing
# a server socket.
close [socket -server {} 0]
# Now sockets can be transferred safely into this thread.
thread::wait
}]
Example 21-12 integrates all of these techniques to create a simple multi-threaded echo server. Note that the server still uses event-driven interaction in each client thread. Technically, this isn't necessary for such a simple server, because once a client thread starts it doesn't expect to receive messages from any other thread. If a thread needs to respond to messages from other threads, it must be in its event loop to detect and service such messages. Because this requirement is common, this application demonstrates the event-driven approach. Example 21-12 A multi-threaded echo server
package require Tcl 8.4
package require Thread 2.5
if {$argc > 0} {
set port [lindex $argv 0]
} else {
set port 9001
}
socket -server _ClientConnect $port
proc _ClientConnect {sock host port} {
# Tcl holds a reference to the client socket during
# this callback, so we can't transfer the channel to our
# worker thread immediately. Instead, we'll schedule an
# after event to create the worker thread and transfer
# the channel once we've re-entered the event loop.
after 0 [list ClientConnect $sock $host $port]
}
proc ClientConnect {sock host port} {
# Create a separate thread to manage this client. The
# thread initialization script defines all of the client
# communication procedures and puts the thread in its
# event loop.
set thread [thread::create {
proc ReadLine {sock} {
if {[catch {gets $sock line} len] || [eof $sock]} {
catch {close $sock}
thread::release
} elseif {$len >= 0} {
EchoLine $sock $line
}
}
proc EchoLine {sock line} {
if {[string equal -nocase $line quit]} {
SendMessage $sock \
"Closing connection to Echo server"
catch {close $sock}
thread::release
} else {
SendMessage $sock $line
}
}
proc SendMessage {sock msg} {
if {[catch {puts $sock $msg} error]} {
puts stderr "Error writing to socket: $error"
catch {close $sock}
thread::release
}
}
# Enter the event loop
thread::wait
}]
# Release the channel from the main thread. We use
# thread::detach/thread::attach in this case to prevent
# blocking thread::transfer and synchronous thread::send
# commands from blocking our listening socket thread.
thread::detach $sock
# Copy the value of the socket ID into the
# client's thread
thread::send -async $thread [list set sock $sock]
# Attach the communication socket to the client-servicing
# thread, and finish the socket setup.
thread::send -async $thread {
thread::attach $sock
fconfigure $sock -buffering line -blocking 0
fileevent $sock readable [list ReadLine $sock]
SendMessage $sock "Connected to Echo server"
}
}
vwait forever
|
| [ Team LiB ] |
|