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:
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.
To create a thread, call le_thread_Create()
.
All threads are named for two reasons:
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().
Threads can terminate themselves by:
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.
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.
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.
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.
The Legato C APIs provide the following thread synchronization mechanisms:
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.
Legato threads use le_thread_AddDestructor()
for clean-up functions. It registers a function to be called by a specified 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).
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.