• No results found

The threads module and GC safety

In document Nim in Action (Page 173-176)

Package management

6.2 Using threads in Nim

6.2.1 The threads module and GC safety

In this section, we’ll look at the threads module. But before we start, I should explain how threads work in Nim. In particular, you need to know what garbage collector safety (GC safety) is in Nim. There’s a very important distinction between the way threads work in Nim and in most other programming languages. Each of Nim’s threads has its own isolated memory heap. Sharing of memory between threads is restricted, which helps to prevent race conditions and improves efficiency.

Efficiency is also improved by each thread having its own garbage collector. Other implementations of threads that share memory need to pause all threads while the gar-bage collector does its business. This can add problematic pauses to the application.

Let’s look at how this threading model works in practice. The following listing shows a code sample that doesn’t compile.

var data = "Hello World"

proc showData() {.thread.} = echo(data)

var thread: Thread[void]

createThread[void](thread, showData) joinThread(thread)

Listing 6.1 Mutating a global variable using a Thread Defines a new mutable global

variable named data and assigns the text "Hello World" to it

Defines a new procedure that will be executed in a new thread. The {.thread.} pragma must be used to signify this.

Attempts to display the value of the data variable

Defines a variable to store the new thread.

The generic parameter signifies the type of parameter that the thread procedure takes. In this case, the void means that the procedure takes no parameters.

The createThread procedure executes the specified procedure in a new thread.

Waits for the thread to finish

THE THREADS MODULE The threads module is a part of the implicitly imported system module, so you don’t need to import it explicitly.

This example illustrates what’s disallowed by the GC safety mechanism in Nim, and you’ll see later on how to fix this example so that it compiles.

Save the code in listing 6.1 as listing01.nim, and then execute nim c --threads:on listing01.nim to compile it. The --threads:on flag is necessary to enable thread support. You should see an error similar to this:

listing01.nim(3, 6) Error: 'showData' is not GC-safe as it accesses

'data' which is a global using GC'ed memory

This error describes the problem fairly well. The global variable data has been cre-ated in the main thread, so it belongs to the main thread’s memory. The showData thread can’t access another thread’s memory, and if it attempts to, it’s not considered GC safe by the compiler. The compiler refuses to execute threads that aren’t GC safe.

A procedure is considered GC safe by the compiler as long as it doesn’t access any global variables that contain garbage-collected memory. An assignment or any sort of mutation also counts as an access and is disallowed. Garbage-collected memory includes the following types of variables:

string

seq[T]

ref T

Closure iterators and procedures, as well as types that include them

There are other ways of sharing memory between threads that are GC safe. You may, for example, pass the contents of data as one of the parameters to showData. The fol-lowing listing shows how to pass data as a parameter to a thread; the differences between listings 6.2 and 6.1 are shown in bold.

var data = "Hello World"

proc showData(param: string) {.thread.} = echo(param)

var thread: Thread[string]

createThread[string](thread, showData, data) joinThread(thread)

Save the code in listing 6.2 as listing2.nim, and then compile it using nim c --threads:on listing2.nim. The compilation should be successful, and running the program should display "Hello World".

Listing 6.2 Passing data to a thread safely

A parameter of type string is specified in the procedure definition.

The procedure argument is passed to echo instead of the global variable data.

The void has been replaced by string to signify the type of parameter that the showData procedure takes.

The data global variable is passed to the createThread procedure, which will pass it on to showData.

The createThread procedure can only pass one variable to the thread that it’s cre-ating. In order to pass multiple separate pieces of data to the thread, you must define a new type to hold the data. The following listing shows how this can be done.

type

ThreadData = tuple[param: string, param2: int]

var data = "Hello World"

proc showData(data: ThreadData) {.thread.} = echo(data.param, data.param2)

var thread: Thread[ThreadData]

createThread[ThreadData](thread, showData, (param: data, param2: 10)) joinThread(thread)

EXECUTINGTHREADS

The threads created in the previous listings don’t do very much. Let’s examine the execution of these threads and see what happens when two threads are created at the same time and are instructed to display a few lines of text. In the following examples, two series of integers are displayed.

var data = "Hello World"

proc countData(param: string) {.thread.} = for i in 0 .. <param.len:

stdout.write($i) echo()

var threads: array[2, Thread[string]]

createThread[string](threads[0], countData, data) createThread[string](threads[1], countData, data) joinThreads(threads)

Save the code in listing 6.4 as listing3.nim, and then compile and run it. Listing 6.5 shows what the output will look like in most cases, and listing 6.6 shows what it may sometimes look like instead.

001122334455667788991010

Listing 6.3 Passing multiple values to a thread

Listing 6.4 Executing multiple threads

Listing 6.5 First possible output when the code in listing 6.4 is executed Iterates from 0 to the length of the param argument minus 1

Displays the current iteration counter without displaying the newline character Goes to

the next

line This time, there are two

threads stored in an array.

Creates a thread and assigns it to one of the elements in the threads array

Waits for all threads to finish

012345678910 012345678910

The execution of the threads depends entirely on the OS and computer used. On my machine, the output in listing 6.5 likely happens as a result of the two threads running in parallel on two CPU cores, whereas the output in listing 6.6 is a result of the first thread finishing before the second thread even starts. Your system may show different results. Figure 6.3 shows what the execution for both the first and second sets of results looks like.

The threads created using the threads module are considerably resource intensive.

They consume a lot of memory, so creating large numbers of them is inefficient.

They’re useful if you need full control over the threads that your application is using, but for most use cases the threadpool module is superior. Let’s take a look at how the threadpool module works.

In document Nim in Action (Page 173-176)