Multithreaded applications

Even though Tcl is designed to work efficiently in a single-threaded environment, it is possible to create separate threads in Tcl. While it can be used for performing any action, it is usually used for operations that take a lot of time to complete.

Threads in Tcl require that Tcl is built with threading enabled and has the package Thread installed, which is true for all ActiveTcl installations. Tcl builds from various operating system distributions may or may not be built with thread support enabled—in this case, the Thread package may not be present.

Managing threads

Tcl uses that approach that each thread is a separate entity and data is not normally shared across threads. It is possible to send commands to be evaluated in a thread, either waiting for them to finish or by having them performed asynchronously. In order to create a thread, we need to first load the Thread package and use the thread::create command:

package require Thread
set tid [thread::create]

This causes a new thread to be created and its ID stored in variable tid.

We can then use the thread::send command to tell a thread to evaluate Tcl code:

thread::send $tid {after 2000 ; puts "Printed after two seconds"}
thread::send -async $tid {after 2000 ; puts "Printed in the background"}

The first command tells the thread to wait for two seconds, and print text to standard output. The thread::send command causes the main thread to wait for the command to finish, actually performed in the other thread. The second command asks the thread to run the command, but it does not wait for the command to complete. Instead, command is run in the background. Asynchronous commands are mainly used for jobs that do not need to pass results back to the application or they that use thread::send to send results back to the original thread.

All calls to thread::send add the commands to event loop queue, so there is no possibility that a command's execution will cause a race condition and it does not need to be synchronized in any way. Internally, thread communication adds idle events to the target thread's event loop so threads need to be in the event loop for this to work. When a thread is created using thread::create, it goes into the event loop so unless the thread blocks at some operation, this is not an issue.

Threads in Tcl do not share any data, unlike most languages, including Java, C/C++, and Python. It is common to use a two-way messaging approach, where we tell a thread to do something using the thread::send -async command and the thread sends results back also using thread::send -async. This approach prevents many of the most common mult-ithreaded programming issues such as deadlocks, and has the benefit that it is much easier to maintain.

A typical example is that the main thread initializes the thread and then sends it commands:

namespace eval example {}
proc example::initializeThread {} {
variable childtid
set childtid [thread::create]
thread::send $childtid {source childscript.tcl}
thread::send $childtid [list 
set example::mainthreadid [thread::id]]
}
proc example::parseDataDone {result} {
# handling of parsed data
}
proc example::parseData {data} {
variable childtid
thread::send async $childtid [list 
example::childParseData $data]
}
example::initializeThread
example::parseData {data goes here}

And childscript.tcl, which is then loaded by child thread, should look like this:

namespace eval example {}
proc example::parseDataActual {data} {
# this actually does time-consuming calculations
# and returns newly created data
}
proc example::childParseData {data} {
variable mainthreadid
set result [parseDataActual $data]
thread::send async $mainthreadid [list 
example::parseDataDone $result]
}

The example will work in the following way—when somebody invokes parseData in main thread, it sends that data to the previously created thread in an asynchronous way and the main thread continues. The child thread receives it in childParseData. It then passes it to parseDataActual procedure, which performs the actual calculations and returns the results. When that happens, an asynchronous event is sent to the main thread with the result, and the child thread can then either parse the next events in its queue or will wait for new tasks to be sent. The main thread will then save the calculated data or pass it to the remaining parts of the application.

Shared variables

Even though variables are not shared in Tcl, the Thread package offers a mechanism for keeping data that is shared across threads, called thread shared variables (tsv). This works in a similar way to Tcl arrays—there are multiple tsvs and each of them can contain multiple keys. A tsv array does not need to be setup— the first operation that sets a key in an array automatically creates this array.

The command tsv::set can be used to set an item and needs to be invoked with the array name, key name, and a new value to set. Getting current value can be done using tsv::get command. The command tsv::unset is used to unset either an entire array or a specified key for this array—if only the array name is specified, the entire array is deleted; if both the array name and key name are specified, only a specific key is deleted. Increasing integer numbers can be done using tsv::incr, specifying array and key name. For example:

package require Thread
tsv::set myarray counter 1
tsv::incr myarray counter
puts "Counter: [tsv::get myarray counter]"

This will cause 2 to be printed out, as after incrementing, this will be the new counter value. As multiple threads can potentially be accessing your tsv variables at the same time, the Thread package uses a locking mechanism when performing operations. For this reason, tsv::incr is safer to use than a combination of tsv::get, incrementing using expr and tsv::set—it might happen that both threads get the current value, and if so, increase it by one and save it. In this case, both will get 1 as the input value, increment it to 2 and set this value—while after both increment it, the value of those operations should be 3.

In order to make sure that for your entire operation on a tsv array, no other thread will be able to access it, you can use tsv::lock. The first argument should be name of the array to lock and the second should be the command to execute. For example, a secure alternative to tsv::incr is:

tsv::lock {
set value [tsv::get myarray counter]
incr value
tsv::set myarray counter $value
}

While basic operations for tsv arrays are provided by the Thread package, locking is useful in cases where your application performs non-basic operations and concurrent access to data should be prohibited while your operation takes place.

It is possible to operate on particular keys for tsv arrays as on regular Tcl lists using the same commands in the tsv namespace—tsv::lappend, tsv::lindex, tsv::llength, tsv::lreplace, tsv::lsearch, and tsv::lset. The only difference with their Tcl lists counterparts is that tsv::lreplace saves results back to the tsv array instead of returning a new list. In addition to these commands, tsv::lpush can be used to insert a new element to a list. It requires specifying an array and key name, followed by the item to insert and optionally the position to insert the element at. Here 0 means the first element in the list, 1 the second element, and so on. If a position is skipped, it is inserted as first element of the list. Command tsv::lpop can be used to retrieve and remove an element from a list. When invoked with only array and key name, it retrieves first element. If the element index is specified, the element at this position is retrieved—where 0 means the first element in the list, 1 the second element, and so on.

It is also possible to retrieve information about array names, key names within a specified array. Command tsv::names returns a list of all arrays and tsv::array names returns a list of all keys for a specified array. For example, to list all arrays and their keys, run:

foreach arrayname [tsv::names] {
foreach keyname [tsv::array names $arrayname] {
puts "Array $arrayname, key $keyname"
}
}

Transferring channels

Tcl channels are associated with an interpreter, which means that a channel created in the main thread will not be accessible to other threads. It is possible to transfer a channel using thread::transfer command. It is invoked with the target thread id and channel name as arguments. For example, to create a thread each time a connection is accepted, the following code could be used:

proc acceptConnection {channel} {
set threadid [thread::create]
initializeThread $threadid
thread::transfer $threadid $channel
thread::send $threadid [list handleChannel $channel]
}

After a channel has been passed to a thread, the channel will no longer be accessible to the originating thread. In order to make it accessible again in the main thread, it needs to be transferred back from the thread that currently owns it to originating thread, also using the thread::transfer command in the exact same way, but using thread the id of originating thread.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.136.84