• How to use a global function in a thread

    From Mark Summerfield@[email protected] to comp.lang.tcl on Tue Sep 9 16:42:57 2025
    From Newsgroup: comp.lang.tcl

    Here are two cut-down examples which show what I'm trying to do:

    ```
    # eg #1
    package require thread 3

    proc main {} {
    set names [list one two three four]
    set tids [list]
    foreach name $names {
    set tid [thread::create -joinable]
    append tids $tid
    thread::send $tid [list process $name]
    }
    foreach tid $tids { thread::join $tid }
    }

    proc process name { puts "processing $name in thread [thread::id]" }

    main
    ```

    The error I get (Tcl 9.0.2) is:

    ```
    invalid command name "process"
    while executing
    "process one"
    invoked from within
    "thread::send $tid [list process $name]"
    (procedure "main" line 7)
    invoked from within
    "main"
    (file "./t1.tcl" line 17)
    ```

    I want the program to execute one custom global command with one argument but although I can pass the argument (I think), I can't access the global function called `process`. (In my real code the `process` command calls several other global commands.)

    I tried another way, which fails differently:

    ```
    # eg #2
    package require thread 3

    proc main {} {
    set names [list one two three four]
    set tids [list]
    foreach name $names {
    set tid [thread::create -joinable]
    append tids $tid
    tsv::set shared name $name
    thread::send $tid {
    puts "processing [tsv::get shared name] in thread [thread::id]"
    }
    }
    foreach tid $tids { thread::join $tid }
    }

    main
    ```

    This one outputs:

    ```
    processing one in thread tid0x7d0aad9ff640
    processing two in thread tid0x7d0aad1fe640
    processing three in thread tid0x7d0aac9fd640
    processing four in thread tid0x7d0a9ffff640
    ```

    much as expected but (1) it isn't using my own global `process` command and
    (2) it doesn't terminate! It just hangs at the end.
    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From et99@[email protected] to comp.lang.tcl on Tue Sep 9 14:31:52 2025
    From Newsgroup: comp.lang.tcl

    On 9/9/2025 9:42 AM, Mark Summerfield wrote:
    Here are two cut-down examples which show what I'm trying to do:

    ```
    # eg #1
    package require thread 3

    proc main {} {
    set names [list one two three four]
    set tids [list]
    foreach name $names {
    set tid [thread::create -joinable]
    append tids $tid
    thread::send $tid [list process $name]
    }
    foreach tid $tids { thread::join $tid }
    }

    proc process name { puts "processing $name in thread [thread::id]" }

    main
    ```

    The error I get (Tcl 9.0.2) is:

    ```
    invalid command name "process"
    while executing
    "process one"
    invoked from within
    "thread::send $tid [list process $name]"
    (procedure "main" line 7)
    invoked from within
    "main"
    (file "./t1.tcl" line 17)
    ```

    I want the program to execute one custom global command with one argument but although I can pass the argument (I think), I can't access the global function
    called `process`. (In my real code the `process` command calls several other global commands.)

    I tried another way, which fails differently:

    ```
    # eg #2
    package require thread 3

    proc main {} {
    set names [list one two three four]
    set tids [list]
    foreach name $names {
    set tid [thread::create -joinable]
    append tids $tid
    tsv::set shared name $name
    thread::send $tid {
    puts "processing [tsv::get shared name] in thread [thread::id]"
    }
    }
    foreach tid $tids { thread::join $tid }
    }

    main
    ```

    This one outputs:

    ```
    processing one in thread tid0x7d0aad9ff640
    processing two in thread tid0x7d0aad1fe640
    processing three in thread tid0x7d0aac9fd640
    processing four in thread tid0x7d0a9ffff640
    ```

    much as expected but (1) it isn't using my own global `process` command and (2) it doesn't terminate! It just hangs at the end.

    Threads run in their own interpreters, you need to define your proc "process" inside the thread itself. As you have it here, process is only defined in the main thread.

    If you don't need process to be called from both the main thread and the new threads, you can define it like so:

    set tid [thread::create {
    proc process {
    ...
    }
    thread::wait
    }
    ]

    And you need to define it in each of the threads you create.

    -e

    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From et99@[email protected] to comp.lang.tcl on Tue Sep 9 14:41:41 2025
    From Newsgroup: comp.lang.tcl

    On 9/9/2025 2:31 PM, et99 wrote:

    set tid [thread::create {
         proc process {
           ...
         }
         thread::wait
       }
     ]

    And you need to define it in each of the threads you create.

    -e


    That should have been proc process arglist {...}

    I don't use the -joinable option, so I can't help you with that.

    -e

    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From Mark Summerfield@[email protected] to comp.lang.tcl on Wed Sep 10 07:50:48 2025
    From Newsgroup: comp.lang.tcl

    I worked out how to use threads but…

    Here's the working threaded solution I found: ------------------------------------------------------------
    #!/usr/bin/env tclsh9

    if {![catch {file readlink [info script]} name]} {
    const APPPATH [file dirname $name]
    } else {
    const APPPATH [file normalize [file dirname [info script]]]
    }

    package require thread 3

    proc main {} {
    set names [list one two three four]
    set tids [list]
    foreach name $names {
    set tid [thread::create -joinable]
    append tids $tid
    tsv::set shared path $::APPPATH
    tsv::set shared name $name
    thread::send $tid {
    tcl::tm::path add [tsv::get shared path]
    package require threads
    threads::process [tsv::get shared name]
    }
    }
    foreach tid $tids { thread::join $tid }
    }

    main
    ------------------------------------------------------------
    # filename: threads-1.tm
    package require thread 3

    namespace eval threads {}

    proc threads::process name {
    puts "processing $name in thread [thread::id]"
    }
    ------------------------------------------------------------
    Using the two files above does work. However it has one problem:
    When I run threads.tcl it correctly does the work but it doesn't
    terminate at the end, just hangs so I have to press Ctrl+C.
    (I'm using Linux if that makes a difference.)

    Interestingly (to me at least:) I found a much simpler alternative that
    still uses multiple cores (when doing real work): ------------------------------------------------------------
    #!/usr/bin/env tclsh9

    proc main {} {
    if {!$::argc} {
    set names [list one two three four]
    foreach name $names {
    exec $::argv0 $name &
    }
    } else {
    process [lindex $::argv 0]
    }
    }

    proc process name {
    puts "processing $name in process [pid]"
    }

    main
    ------------------------------------------------------------
    This does terminate, although I have to press Enter at the end and I don't
    know why — or how to avoid the need to do so. This “multiprocessing” approach is what I'm using in practice.
    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From Ralf Fassel@[email protected] to comp.lang.tcl on Wed Sep 10 10:57:17 2025
    From Newsgroup: comp.lang.tcl

    * Mark Summerfield <[email protected]>
    | proc main {} {
    | set names [list one two three four]
    | set tids [list]
    | foreach name $names {
    | set tid [thread::create -joinable]
    | append tids $tid

    Use lappend here, not append, otherwise the 'foreach' below waiting for
    the threads will not work as expected.

    | tsv::set shared path $::APPPATH
    | tsv::set shared name $name
    | thread::send $tid {
    | tcl::tm::path add [tsv::get shared path]
    | package require threads
    | threads::process [tsv::get shared name]
    | }
    | }
    | foreach tid $tids { thread::join $tid }
    | }

    | main
    | ------------------------------------------------------------
    | # filename: threads-1.tm
    | package require thread 3

    | namespace eval threads {}

    | proc threads::process name {
    | puts "processing $name in thread [thread::id]"
    | }
    | ------------------------------------------------------------
    | Using the two files above does work. However it has one problem:
    | When I run threads.tcl it correctly does the work but it doesn't
    | terminate at the end, just hangs so I have to press Ctrl+C.
    | (I'm using Linux if that makes a difference.)

    Add some debugging prints in the main thread, then you will see that the 'join's do not finish. This is due to the fact that the threads are
    never told to finish. If you add a
    thread::send $tid thread::release
    just before the thread::join, they will finish (also of course if you thread::release somewhere in the thread code).

    Just keep in mind that each thread starts a completely new interpreter
    which knows nothing about what was defined in the main thread which
    started it. You will need to redefine every proc you need in the
    threads, set every variable etc. I would almost certainly put that
    startup code in a separate file and source that from every thread,
    including the main thread.

    R'
    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From Mark Summerfield@[email protected] to comp.lang.tcl on Wed Sep 10 09:17:34 2025
    From Newsgroup: comp.lang.tcl

    On Wed, 10 Sep 2025 10:57:17 +0200, Ralf Fassel wrote:

    * Mark Summerfield <[email protected]>
    | proc main {} {
    | set names [list one two three four]
    | set tids [list]
    | foreach name $names {
    | set tid [thread::create -joinable]
    | append tids $tid

    Use lappend here, not append, otherwise the 'foreach' below waiting for
    the threads will not work as expected.

    | tsv::set shared path $::APPPATH
    | tsv::set shared name $name
    | thread::send $tid {
    | tcl::tm::path add [tsv::get shared path]
    | package require threads
    | threads::process [tsv::get shared name]
    | }
    | }
    | foreach tid $tids { thread::join $tid }
    | }

    | main
    | ------------------------------------------------------------
    | # filename: threads-1.tm
    | package require thread 3

    | namespace eval threads {}

    | proc threads::process name {
    | puts "processing $name in thread [thread::id]"
    | }
    | ------------------------------------------------------------
    | Using the two files above does work. However it has one problem:
    | When I run threads.tcl it correctly does the work but it doesn't
    | terminate at the end, just hangs so I have to press Ctrl+C.
    | (I'm using Linux if that makes a difference.)

    Add some debugging prints in the main thread, then you will see that the 'join's do not finish. This is due to the fact that the threads are
    never told to finish. If you add a
    thread::send $tid thread::release
    just before the thread::join, they will finish (also of course if you thread::release somewhere in the thread code).

    Just keep in mind that each thread starts a completely new interpreter
    which knows nothing about what was defined in the main thread which
    started it. You will need to redefine every proc you need in the
    threads, set every variable etc. I would almost certainly put that
    startup code in a separate file and source that from every thread,
    including the main thread.

    R'

    Thank you. The append instead of lappend was a typo. I've now changed
    main to this and it terminates & works fine:

    proc main {} {
    set names [list one two three four]
    set tids [list]
    foreach name $names {
    set tid [thread::create -joinable]
    lappend tids $tid
    tsv::set shared path $::APPPATH
    tsv::set shared name $name
    thread::send $tid {
    tcl::tm::path add [tsv::get shared path]
    package require threads
    threads::process [tsv::get shared name]
    }
    }
    foreach tid $tids {
    thread::release $tid
    thread::join $tid
    }
    }

    However, although it works, when I use this technique for the real
    work I'm doing a run takes 0.312s but using the exec-based approach
    takes only 0.026s!
    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From Ralf Fassel@[email protected] to comp.lang.tcl on Wed Sep 10 12:13:54 2025
    From Newsgroup: comp.lang.tcl

    * Mark Summerfield <[email protected]>
    | Thank you. The append instead of lappend was a typo. I've now changed
    | main to this and it terminates & works fine:

    | proc main {} {
    | set names [list one two three four]
    | set tids [list]
    | foreach name $names {
    | set tid [thread::create -joinable]
    | lappend tids $tid
    | tsv::set shared path $::APPPATH
    | tsv::set shared name $name
    | thread::send $tid {
    | tcl::tm::path add [tsv::get shared path]
    | package require threads
    | threads::process [tsv::get shared name]
    | }
    | }
    | foreach tid $tids {
    | thread::release $tid
    | thread::join $tid
    | }
    | }

    | However, although it works, when I use this technique for the real
    | work I'm doing a run takes 0.312s but using the exec-based approach
    | takes only 0.026s!

    No surprise here, since the exec variant runs the processes in parallel (&), while your thread variant waits for each thread to finish the
    thread::send (no -async) (so it really isn't multi-threaded :-)

    If you change to [thread::send -async], the code contains at least one
    possible race condition for the 'name' variable:

    foreach name $names {
    ...
    tsv::set shared name $name
    ...
    thread::send $tid { ... threads::process [tsv::get shared name] }

    Here that you assume that 'name' is still set to the value that main had
    set after starting the thread, but this is not necessarily true when
    using -async. If the execution of the thread is delayed for any reason,
    the next loop iteration might have already set the next name in the
    tsv::set. Threads can be tricky and hard to debug...

    R'
    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From Mark Summerfield@[email protected] to comp.lang.tcl on Wed Sep 10 10:55:08 2025
    From Newsgroup: comp.lang.tcl

    On Wed, 10 Sep 2025 12:13:54 +0200, Ralf Fassel wrote:

    * Mark Summerfield <[email protected]>
    | Thank you. The append instead of lappend was a typo. I've now changed
    | main to this and it terminates & works fine:

    | proc main {} {
    | set names [list one two three four]
    | set tids [list]
    | foreach name $names {
    | set tid [thread::create -joinable]
    | lappend tids $tid
    | tsv::set shared path $::APPPATH
    | tsv::set shared name $name
    | thread::send $tid {
    | tcl::tm::path add [tsv::get shared path]
    | package require threads
    | threads::process [tsv::get shared name]
    | }
    | }
    | foreach tid $tids {
    | thread::release $tid
    | thread::join $tid
    | }
    | }

    | However, although it works, when I use this technique for the real
    | work I'm doing a run takes 0.312s but using the exec-based approach
    | takes only 0.026s!

    No surprise here, since the exec variant runs the processes in parallel (&), while your thread variant waits for each thread to finish the
    thread::send (no -async) (so it really isn't multi-threaded :-)

    If you change to [thread::send -async], the code contains at least one possible race condition for the 'name' variable:

    foreach name $names {
    ...
    tsv::set shared name $name
    ...
    thread::send $tid { ... threads::process [tsv::get shared name] }

    Here that you assume that 'name' is still set to the value that main had
    set after starting the thread, but this is not necessarily true when
    using -async. If the execution of the thread is delayed for any reason,
    the next loop iteration might have already set the next name in the
    tsv::set. Threads can be tricky and hard to debug...

    R'

    You're right that I forgot the -async. That reduces the time to 0.064s,
    still more than double the time it took via exec and as you rightly
    say much harder to debug. I'll stick with exec ... & for these cases.
    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From et99@[email protected] to comp.lang.tcl on Wed Sep 10 14:04:11 2025
    From Newsgroup: comp.lang.tcl

    On 9/10/2025 12:50 AM, Mark Summerfield wrote:
    Here's the working threaded solution I found: ------------------------------------------------------------
    #!/usr/bin/env tclsh9

    if {![catch {file readlink [info script]} name]} {
    const APPPATH [file dirname $name]
    } else {
    const APPPATH [file normalize [file dirname [info script]]]
    }

    package require thread 3

    proc main {} {
    set names [list one two three four]
    set tids [list]
    foreach name $names {
    set tid [thread::create -joinable]
    append tids $tid
    tsv::set shared path $::APPPATH
    tsv::set shared name $name
    thread::send $tid {
    tcl::tm::path add [tsv::get shared path]
    package require threads
    threads::process [tsv::get shared name]
    }
    }
    foreach tid $tids { thread::join $tid }
    }

    main
    ------------------------------------------------------------
    # filename: threads-1.tm
    package require thread 3

    namespace eval threads {}

    proc threads::process name {
    puts "processing $name in thread [thread::id]"
    }

    -snip-

    You're right that I forgot the -async. That reduces the time to 0.064s,
    still more than double the time it took via exec and as you rightly
    say much harder to debug. I'll stick with exec ... & for these cases.




    Each thread creation takes on the order of 10ms (on my system). I don't do a [package require Thread] in each thread, as that seems automatic.

    Not sure about [package require thread 3] which fails for me, so I'm a bit confused as to how your module finishes loading, perhaps a copy/paste posting error?

    Instead of doing a [package require threads] to load a module file in each thread, you can just do:

    set script {
    proc process args {
    ...
    }
    thread::wait
    }

    And then [thread::create $script] which should be faster than module loading or sourcing of files.

    If you do need to load packages inside your threads, you might be able to limit your thread initialization time by first setting up a minimum auto_path variable for only what you require in your thread.

    You can also use an introspection proc to retrieve the full text of a procedure, and use that to load proc's into your threads.


    proc getFullProcText procName {
    # Ensure the proc exists
    if {![llength [info procs $procName]]} {
    return -code error "no such procedure '$procName'"
    }

    # Build the argument list (with defaults if present)
    set argList {}
    foreach a [info args $procName] {
    if {[info default $procName $a defVal]} {
    lappend argList [list $a $defVal]
    } else {
    lappend argList $a
    }
    }

    # Get body
    set body [info body $procName]

    # Construct the definition (everything as a list to preserve quoting)
    return [list proc $procName $argList $body]
    }




    -e


    --- Synchronet 3.21a-Linux NewsLink 1.2
  • From et99@[email protected] to comp.lang.tcl on Wed Sep 10 16:34:50 2025
    From Newsgroup: comp.lang.tcl

    On 9/10/2025 2:04 PM, et99 wrote:


    Not sure about [package require thread 3] which fails for me,


    Ah, ok, it seems newer thread packages accept Thread and thread.

    --- Synchronet 3.21a-Linux NewsLink 1.2