Threading

In computing, a thread is a sequence of instructions being managed by an operating system. Lasso has integrated support for running multiple threads, allowing it to handle many application requests at the same time. Threading in Lasso is designed to be easy to use and safe. Lasso does not feature global variables, so all data is local to individual threads. Threads can communicate with one another by sending object messages back and forth. These objects are copied as they are transmitted to ensure that data structures remain consistent.

Lasso supports creating or splitting a new thread given a block of code. It also supports creating thread objects which run in their own thread.

Splitting Threads

A new thread can be created by calling the split_thread method, which requires a capture block. The capture given to split_thread will be run in a new thread. This new thread will contain copies of the local variables that are active at the time the new thread is created. Changing the value of a variable in the new thread will not affect the variables that were active at the creation point. Additionally, the current self is cleared for the new thread.

split_thread() → pair

Takes a capture assigned as a capture block and runs that capture in a separate thread. Any local variables that would normally be available to that capture are copied and available in the new thread. It also returns a pair object with file descriptors for writing and reading messages to and from the newly created thread.

Thread with Capture

The following example shows a new thread being created. The new thread simply prints a message to the console. This illustrates how split_thread is used and how a new capture (between curly braces { ... }) is given to split_thread which will be run in a new thread.

split_thread => {
   stdoutnl("I'm alive in a new thread!")
}

Thread Communication

When a new thread is created by calling split_thread, the return value of that method call is a pair of filedesc objects. Similarly, the parameter given to the new thread is a pair of filedesc objects. (This can be accessed in the new thread by the pseudo-local variable #1.) The filedesc type represents a file descriptor or pipe over which data can be sent or received. These objects provide the means for the new thread and the creator thread to communicate. Two filedesc objects are required for thread communication, one representing the write end of the pipe and the other representing the read end. Objects are written to the write filedesc and read from the read filedesc.

Within this context of the given pair of filedescs, the write filedesc is always the first member of the pair while the read filedesc is always the second member. The creator thread writes objects to the new thread using the write filedesc, and reads objects from the new thread using the read. The newly created thread operates in the same manner, writing and reading objects to and from its creator thread.

Send and Receive Objects Between Threads

The next example creates a new thread and illustrates how objects can be sent and received:

// Create the new thread, saving the filedesc pair in #creatorPipes
local(creatorPipes) = split_thread => {

   // Save the filedescs sent to this new thread
   local(
      writePipe = #1->first,
      readPipe = #1->second
   )

   // Loop indefinitely, reading messages and sending replies
   while(true) => {

      // Read an object
      local(o) = #readPipe->readObject

      // Print a message
      stdoutnl("I read an object: " + #o)

      // Write a reply object
      #writePipe->writeObject("Reply from the new thread")
   }
}

// Write an object to our new thread
#creatorPipes->first->writeObject("Sent from the creator!")

// Read the reply from the new thread
stdoutnl(#creatorPipes->second->readObject)

// Do it again
#creatorPipes->first->writeObject("Sent from the creator 2!")
stdoutnl(#creatorPipes->second->readObject)

// =>
// I read an object: Sent from the creator!
// Reply from the new thread
// I read an object: Sent from the creator 2!
// Reply from the new thread

Threads created with split_thread exit when they reach the end of their code body. If the example thread above did not loop reading/writing messages, it would read one message, write one reply, reach the end of its code, and then exit.

Thread Objects

Thread objects represent a second way to create new threads in Lasso. A thread object is an object that exists in its own thread. This means that any method calls to a thread object run serially in the object’s thread. Thread objects exist as singletons, which means that only one instance of a particular thread type can exist. Thread objects permit data to be globally available, yet forces access to that data to be synchronized.

Thread objects are created and begin running at the point where they are defined. Thread types are defined similarly to how normal types are defined, except that in such a definition, the word type is replaced with the word thread.

Simple Counter Thread

The following example creates a simple thread object. This object maintains a counter that can be advanced and retrieve its current value. Because this is a thread object, it is globally available and other threads can safely advance the counter.

define counter_thread => thread {
   data private val = 0

   public advanceBy(value::integer) => {
      .val += #value
      return .val
   }
}

The above example defines a counter_thread object. This object exists and begins running as soon as it is defined. Clients can access the thread object by calling it by name; in this case by calling the counter_thread method:

counter_thread->advanceBy(40)
// => 40

counter_thread->advanceBy(10)
// => 50

Note that each time counter_thread is called, the same thread object is retrieved. Hence, after the second call to counter_thread->advanceBy, the “val” data member has a value of “50”.

Thread objects can be composed of the same elements as a regular type, including public and private data members, and can have any other (non-thread) object type as a parent.

Simple Map Thread

This next example creates a thread type that inherits from type map. This results in creating a global map of values that can be safely accessed by other threads.

define map_thread => thread {
   parent map
   public onCreate() => ..onCreate
}

map_thread->insert('one'=1) & insert('two'=2)

map_thread->get('two')
// => 2

Thread objects cannot be copied. Additionally, thread objects will continue to run indefinitely, though they can terminate themselves by calling abort. Also, all parameter values given to a thread object method are copied, as well as any return value of a thread object method. This ensures that no two threads are ever operating on the same data at the same time, a situation that can have catastrophic results.

Thread Objects and onCreate

Because thread objects are created as soon as they are defined, a thread object must have a zero parameter onCreate method, or no onCreate methods at all. If a thread object requires further configuration, as would normally be done at the point of object creation, it should be done immediately following the thread object’s definition. For example, the counter_thread could be defined to permit its “val” data member to have an initial value set, as shown in the following code:

define counter_thread => thread {
   data private val = 0

   // Default zero-parameter onCreate
   public onCreate() => {}

   // Additional onCreate, letting val be initialized
   public onCreate(initValue::integer) => {
      .val = #initValue
   }

   public advanceBy(value::integer) => {
      .val += #value
      return .val
   }
}

// Initialize the counter
counter_thread->onCreate(900)

// Now it can be used
counter_thread->advanceBy(20)
// => 920

active_tick

Thread objects can define a method named active_tick. If defined, this method will be called periodically by the system. This lets a thread object carry out periodic activity regardless of any methods called by clients. The active_tick method should accept no parameters and return an integer value. The integer value tells the system how many seconds at the latest the active_tick method should be called again. The active_tick method may be called sooner than the specified time as it provides the timeout value for reading messages for that thread. Threads requiring precise timing for events should not rely on the active_tick calls only being called after the timeout value.

The next example defines a thread object that prints a message to the console every 2 seconds:

define lazy_ticker => thread {
   public active_tick() => {
      stdoutnl("Hello, from lazy ticker")
      return 2
   }
}

The active_tick method can be one of several member methods, can reference and call other member methods, and the tick timer (return value) can be programmatically manipulated so that it does not have to be a hard-coded value. In this way, a single active_tick-enabled thread can manage multiple tasks and conditionally perform additional tasks based on the results of its basic task, can put itself to sleep or adjust the sleep timer, and have methods that are called completely separately from the active_tick method. In short, any thread type can also contain an active_tick method to perform periodic maintenance or time-sensitive tasks.