6.3 Threads - naver/lispe GitHub Wiki

Threads

LispE provides many instructions to launch threads and handle data within threads. Each thread runs in its own environment, hence a thread cannot access any global variables or variables from any other threads.

Critical Sections

A critical section is where data is protected against concurrent access. It is usually protected with a mutex, which locks the section and unlocks it once it has been processed. Since, in LispE variables cannot be shared, there are almost no critical section in the code except for two cases:

  • The call to eval on a string
  • The creation of a new atom

For the rest, each thread possesses its own garbage and its data pools, again, without any interference with any other threads.

However, when you use an external library in threads, you might need to create your own critical section, hence the existence of lock (see below).

Launching a thread: dethread, wait

A thread is declared with the keyword: dethread. Its declaration is in no way different from a regular function. Launching a thread consists of calling the function with the right arguments. Note that a thread does not return any values.

When different threads are launched, the main thread may wait for their completion with the function: wait.

Example

; we define a thread
(dethread call(s i)
   (loop e (range 0 i 1)
      (+= s (string e))
   )
   (println s)
)

; We call 3 threads with different arguments
(call "l:" 10)
(call "x:" 20)
(call "y:" 30)

; we wait for these threads to end
(wait)

Locks

It is possible to protect some parts of a thread body with locks...

; we define a thread
(dethread call(s i)
   (lock "mylock"
      (loop e (range 0 i 1)
         (+= s (string e))
      )
   )
   (println s)
)

lock needs a specific key to be identified, then it executes the code inside.

Blocking a thread: sleep, waiton, trigger

If you want to put a thread on hold, you can use either: sleep or waiton.

(sleep tm)

sleep put a thread on hold for tm milliseconds.

(waiton key) / (trigger key)

waiton blocks a thread and waits for another thread to awake it with trigger. The same key can be shared by different threads and be triggered all at once.


; This thread is on hold and wait for a trigger
(dethread testwait(I j)
     (waiton "zzz")
)

; This thread will wake up every thread waiting on "zzz"
(dethread wakeitup(k)
    (trigger "zzz")
)

threadstore, threadretreive, threadclear

These instructions are the only way for threads to exchange data with their surroundings. They use an internal list, which is thread protected, in which a thread can store any kind of data.

You can define a specific namespace thanks to a key, in which your data will be stored...

(dethread call(i)
   (setq l (range 1 i 1))
   ;we store data in our namespace: here
   (threadstore "here" l)
)

(call 10)
(call 100)
(call 20)

; we wait for the threads to end
(wait)

; we display the content of what was stored
(println (threadretrieve "here"))

; we clear the internal list
(threadclear "here")

threadspace, seth

There is another way to handle variables in threads: threadspace.

Threadspace is a specific zone, in which variables can be shared across different threads. For this purpose, LispE provides seth, which creates a variable in this specific threadspace.

; We create in the main section two specific thread safe variables titi and toto
(threadspace
   (seth titi 10)
   (seth toto (rho 4 4 '(0)))
)

; titi and toto are accessed via threadspace in this thread
(dethread tst(x y)
   (threadspace
      (+= titi x)
      (set@ toto 0 y titi)
   )
)

; We call our threads
(tst 10 0)
(tst 20 1)
(tst 30 2)
(tst 40 3)

; we wait for their completion
(wait)

; again titi and toto are only available in threadspace
(threadspace
   (println titi)
   (println toto)
)