RealThread Object

class RealThread extends Object

Runs callbacks on a real operating-system thread, each with its own event loop. This is distinct from the cooperative script threads that the rest of the language schedules, which are described by the Thread object.

This class is exported by the KS module.

#Import "Ks" { RealThread, Await }
worker := RealThread(Sum, 1, 100)
MsgBox Await(worker.Task)

Sum(from, to) {
    total := 0
    Loop to - from + 1
        total += from + A_Index - 1
    return total
}

Two completions

A worker has two completions, and they are not the same event. Each is a Task, so Await and Then apply to both.

PropertySettles when
TaskThe entry function's body leaves, carrying its return value, its error, or cancellation if Exit ended it first.
TerminatedThe operating-system thread is gone.

Kinds of real thread

Three kinds of object share this class:

Obtained fromDescription
RealThread(Callback, Arguments*)A worker this class started. The only kind with a body to wait for or a lifetime this class ends.
RealThread.MainThe script's main thread.
A_RealThreadThe thread the calling code is on: one of the above, or an adopted thread when script code runs on some other thread.

On the main and adopted kinds, Task, Terminated and Exit throw TargetError; everything else behaves the same.

Exactly one object exists per real thread, so == identifies which kind one is: A_RealThread == RealThread.Main is true on the main thread.

Call

Worker := RealThread(Callback , Arguments*)
Worker := RealThread.Call(Callback , Arguments*)

Starts Callback on a new real thread and returns its RealThread object. Any Arguments are passed to Callback.

The thread ends when its body returns and it has nothing left to serve. A body that registers a timer, hotkey, callback or window event keeps its thread alive to serve them; use Exit to shut such a thread down.

Main

MainThread := RealThread.Main

The script's main thread. Use it to move work onto the main thread from a worker:

RealThread.Main.Post(() => MsgBox("Shown on the main thread"))

Post

TaskObj := Worker.Post(Callback , Arguments*)

Queues Callback on the thread and returns immediately with the Task carrying its result. An error raised by the callback fails that Task without calling OnError at the throw site. Ignore that task for fire-and-forget, Then it to react when the work finishes, or Await it to wait for the value.

worker.Post(Log, "done")
worker.Post(Compute, a, b).Then(Handle)
value := Await(worker.Post(Compute, a, b))

The work is queued as a thread launch on the target, so a target which is Critical or has reached #MaxThreads defers it until launches are admitted again. Work discarded at the target's shutdown fails its task.

Posting to a thread which is no longer alive raises an error.

Send

Result := Worker.Send(Callback , Arguments*)

Runs Callback on the thread, waits for it, and returns its result. An error raised there is carried to and re-thrown on the calling thread without calling OnError at the original throw site. The calling thread keeps processing its own events while it waits.

A call to the calling thread's own real thread runs directly. The callback runs ahead of queued work whose launch is deferred, but a target which is Critical or has reached #MaxThreads refuses it and this raises an error, where Post would defer it.

Use Send when a busy target should fail fast and Await(worker.Post(...)) when it should be waited for. Sending to a thread which is no longer alive raises an error.

Exit

TaskObj := Worker.Exit(ExitCode)

Asks the thread to shut down and returns the same task as Terminated, so Await(worker.Exit()) stops the worker and waits for it. Work already queued on it is abandoned, a script thread running on it unwinds when it next processes events, and its event loop then stops. This is cooperative and does not asynchronously abort running code. On a thread which has already ended, it only returns that task.

Called on the thread itself (A_RealThread.Exit()), the current script thread exits immediately and the call does not return, so its task is unobservable there.

ExitCode

Type: Integer

If omitted, it defaults to 0. Otherwise, the code the script threads being exited end with: each one's ExitCode, and the script's exit code as Exit describes.

Properties

Id

Type: Integer

The managed ID of the backing operating-system thread.

IsAlive

Type: Boolean

True while the operating-system thread has not terminated.

Task

Type: Task

The entry function's eventual result, settling the moment the body leaves, whether or not the thread stays up afterwards. It succeeds with the body's return value, fails with the body's error, or is canceled if Exit ended the body first; Status and the Is* predicates report which.

A body's error is carried by this task without calling OnError at the throw site, rethrows when the task is awaited, and is reported as an unobserved failure if nothing ever observes it.

Terminated

Type: Task

Completes when the operating-system thread is gone, carrying no value. Await it to wait for a worker which ends on its own.

Threads

Type: Array

The active Thread objects of this real thread, oldest first, so Threads[1] is the one that has been running longest. The array is a snapshot taken when the property is read, and is empty while the thread sits idle in its event loop.

Remarks

State shared between real threads requires synchronization. Use a Lock when callbacks mutate shared objects.

Warning: Do not hold a Lock across Worker.Send(Callback) or Await(Worker.Post(Callback)) when Callback wants that same lock. The two threads then wait on each other: until the Await's timeout, or indefinitely for a Send, which has no timeout.

Settings that are per script thread — CoordMode, SendMode, SetKeyDelay, A_LastError and the rest — are also per real thread, so a worker starts from the script's defaults and its changes do not affect the main thread.

Examples

Doing slow work off the main thread and reporting back to it.

#Import "Ks" { RealThread, A_RealThread }

worker := RealThread(Crunch, 30000000)

Crunch(iterations) {
    total := 0
    Loop iterations
        total += A_Index
    ; Post returns immediately; the main thread shows the result when it next processes events.
    RealThread.Main.Post(Report, total)
    return total
}

Report(total) {
    MsgBox "Total: " total
}

A long-lived worker that serves requests until it is shut down. Its body returns at once, so Task settles long before Terminated does.

#Import "Ks" { RealThread, A_RealThread, Await }

service := RealThread(Serve)

Await(service.Task)         ; the body has returned
MsgBox service.IsAlive      ; 1, the thread is still serving its timer

; Send runs on the worker and returns its value.
MsgBox service.Send(() => "answered by thread " A_RealThread.Id)

Await(service.Exit())       ; shut it down and wait for the thread to go

Serve() {
    ; Keeps the worker's event loop alive so Post and Send have somewhere to land.
    SetTimer(() => 0, 1000)
}

Task, Await, Lock, Thread object, Threads, KS module variables