Thread Control API
Generally, using single-threaded, event-driven programming (registering callbacks to be called by an event handling loop running in a single thread) is more efficient than using multiple threads. With single-threaded, event driven designs:
- there's no CPU time spent switching between threads.
- there's only one copy of thread-specific memory objects, like the procedure call stack.
- there's no need to use thread synchronization mechanisms, like mutexes, to prevent race conditions between threads.
Sometimes, this style doesn't fit well with a problem being solved, so you're forced to implement workarounds that severely complicate the software design. In these cases, it is far better to take advantage of multi-threading to simplify the design, even if it means that the program uses more memory or more CPU cycles. In some cases, the workarounds required to avoid multi-threading will cost more memory and/or CPU cycles than using multi-threading would.
But you must be careful with multi-threading. Some of the most tenacious, intermittent defects known to humankind have resulted from the misuse of multi-threading. Ensure you know what you are doing.
Creating a Thread
To create a thread, call le_thread_Create()
.
All threads are named for two reasons:
- To make it possible to address them by name.
- For diagnostics.
Threads are created in a suspended state. In this state, attributes like scheduling priority and stack size can use the appropriate "Set" functions. All attributes have default values so it is not necessary to set any attributes (other than the name and main function address, which are passed into le_thread_Create() ). When all attributes have been set, the thread can be started by calling le_thread_Start().
- Warning
- It is assumed that if a thread T1 creates another thread T2 then only thread T1 will set the attributes and start thread T2. No other thread should try to set any attributes of T2 or try to start it.
Terminating a Thread
Threads can terminate themselves by:
- returning from their main function
- calling le_thread_Exit().
Threads can also tell other threads to terminate by "canceling" them; done through a call to le_thread_Cancel()
.
If a thread terminates itself, and it is "joinable", it can pass a void*
value to another thread that "joins" with it. See Joining for more information.
Canceling a thread may not cause the thread to terminate immediately. If it is in the middle of doing something that can't be interrupted, it will not terminate until it is finished. See 'man 7 pthreads' for more information on cancellation and cancellation points.
To prevent cancellation during a critical section (e.g., when a mutex lock is held), pthread_setcancelstate() can be called. If a cancellation request is made (by calling le_thread_Cancel() or pthread_cancel()
), it will be blocked and remain in a pending state until cancellation is unblocked (also using pthread_setcancelstate()), at which time the thread will be immediately cancelled.
Joining
Sometimes, you want single execution thread split (fork) into separate threads of parallel execution and later join back together into one thread later. Forking is done by creating and starting a thread. Joining is done by a call to le_thread_Join(). le_thread_Join(T) blocks the calling thread until thread T exits.
For a thread to be joinable, it must have its "joinable" attribute set (using le_thread_SetJoinable()) prior to being started. Normally, when a thread terminates, it disappears. But, a joinable thread doesn't disappear until another thread "joins" with it. This also means that if a thread is joinable, someone must join with it, or its resources will never get cleaned up (until the process terminates).
le_thread_Join() fetches the return/exit value of the thread that it joined with.
Thread-Local Data
Often, you want data specific to a particular thread. A classic example of is the ANSI C variable errno
. If one instance of errno
was shared by all the threads in the process, then it would essentially become useless in a multi-threaded program because it would be impossible to ensure another thread hadn't killed errno
before its value could be read. As a result, POSIX has mandated that errno
be a thread-local variable; each thread has its own unique copy of errno
.
If a component needs to make use of other thread-local data, it can do so using the pthread functions pthread_key_create(), pthread_getspecific(), pthread_setspecific(), pthread_key_delete(). See the pthread man pages for more details.
Thread Synchronization
Nasty multi-threading defects arise as a result of thread synchronization, or a lack of synchronization. If threads share data, they MUST be synchronized with each other to avoid destroying that data and incorrect thread behaviour.
- Warning
- This documentation assumes that the reader is familiar with multi-thread synchronization techniques and mechanisms.
The Legato C APIs provide the following thread synchronization mechanisms:
Thread Destructors
When a thread dies, some clean-up action is needed (e.g., a connection needs to be closed or some objects need to be released). If a thread doesn't always terminate the same way (e.g., if it might be canceled by another thread or exit in several places due to error detection code), then a clean-up function (destructor) is probably needed.
A Legato thread can use le_thread_AddDestructor()
to register a function to be called by that thread just before it terminates.
A parent thread can also call le_thread_AddChildDestructor()
to register a destructor for a child thread before it starts the child thread.
Multiple destructors can be registered for the same thread. They will be called in reverse order of registration (i.e, the last destructor to be registered will be called first).
A Legato thread can also use le_thread_RemoveDestructor() to remove its own destructor function that it no longer wants called in the event of its death. (There is no way to remove destructors from other threads.)
Using Legato APIs from Non-Legato Threads
If a thread is started using some other means besides le_thread_Start() (e.g., if pthread_create() is used directly), then the Legato thread-specific data will not have been initialized for that thread. Therefore, if that thread tries to call some Legato APIs, a fatal error message like, "Legato threading API used in non-Legato thread!" may be seen.
To work around this, a "non-Legato thread" can call le_thread_InitLegatoThreadData() to initialize the thread-specific data that the Legato framework needs.
If you have done this for a thread, and that thread will die before the process it is inside dies, then that thread must call le_thread_CleanupLegatoThreadData() before it exits. Otherwise the process will leak memory. Furthermore, if the thread will ever be cancelled by another thread before the process dies, a cancellation clean-up handler can be used to ensure that the clean-up is done, if the thread's cancellation type is set to "deferred". See 'man 7 pthreads' for more information on cancellation and cancellation points.
Copyright (C) Sierra Wireless Inc. Use of this work is subject to license.