The nn::os::Thread
class defined in the SDK only defines the basic functions for dealing with threads, such as how to start them, join them, get parameters, and change parameters. Threads used by applications must inherit from this thread class.
9.1. Initializing and Starting
After the constructor creates a thread, the thread has yet to be either initialized or started. If you are managing the stack in your application, call the Start()
function to initialize and start a thread; to have the library automatically manage stack memory, call StartUsingAutoStack()
. Calling the member functions that start with Try
(TryStart()
and TryStartUsingAutoStack()
) returns an error if the functions fail, such as due to insufficient resources.
If the application manages stack memory, a GetStackBottom
member function (that returns the bottom of the stack as an uptr
value) must be defined for the object passed as a stack region to the member functions. Both the nn::os::StackMemoryBlock
class, which allocates stack memory blocks, and the nn::os::StackBuffer
class, which can be located within static memory and structures, satisfy this condition and can be safely passed as arguments. Make sure that the application does not allow the stack memory to become invalid (released) until after the threads have terminated.
9.1.1. Running Application Threads in the System Core
You can allocate some of the system core’s CPU time for applications to run application threads in the system core.
To start a thread in the system core, use the nn::os::SetApplicationCpuTimeLimit()
function to specify the percentage of CPU time to allocate, and then specify 1
for coreNo
. You must allocate CPU time before you start a thread on the system core; failure to do so results in an error.
nn::Result nn::os::SetApplicationCpuTimeLimit(s32 limitPercent); s32 nn::os::GetApplicationCpuTimeLimit();
Use the limitPercent
parameter to specify the percentage of the system core’s CPU time to allocate to your application. You can specify values in the range from 5 to 30. The specified percentage of CPU time is allocated from the beginning of each 2-millisecond cycle. In other words, specifying a value of 25 causes 0.5 milliseconds (2 * 25 / 100) to be allocated to the application and the remaining 1.5 milliseconds to be allocated to the system.
You can use the nn::os::GetApplicationCpuTimeLimit()
function to get the current ratio of CPU allocation. This returns 0 by default because no CPU time is initially allocated to the application.
After you have allocated CPU time for your application, you cannot restore this setting to 0.
Even if no application threads are running, the system will not use the time allocated to the application. As a result, wireless communication and other system core processes slow down after CPU time is allocated to the application.
9.1.2. ManagedThread Class
The ManagedThread
class adds functionality to the Thread
class. Basic operation for the two classes is the same, except that the functions for initialization and execution are separated. The following table details the distinction.
Added Features |
Associated Functions |
---|---|
Retrieving information related to the stack |
|
Name retention |
|
Acceleration of ID retrieval |
|
Retrieving the instance corresponding to the current thread |
|
Enumuration of the thread |
|
Searching the thread |
|
When sharing resources with the Thread
class, the total number of ManagedThread
class threads is subject to the restriction of the number of threads that can be created. Also, twice the local storage is consumed to use ManagedThread
when the nn::os::ManagedThread::InitializeEnvironment()
function is called.
9.2. Thread Functions
The operations performed by a thread are implemented in the thread function passed to the member function that initializes and starts the thread. Declare a thread function that takes one or no arguments and does not return a value (a void
return type).
Some of the Start()
functions of the Thread
class can be used by a thread function that does not take any arguments, and there is an overloaded version that allows you to use a template to specify the type of argument that is passed to a thread function. Note that because the arguments passed in are copied to the thread's stack, the template must specify arguments of types that can be copied and the amount of space available in the stack will be reduced by an equivalent amount.
9.3. Completion and Destruction
A thread completes when its thread function completes or when it is released from the blocked state by the parent nn::os::WaitObject
class's Wait*()
functions. The Join()
function waits for thread completion unconditionally. If you need more fine-grained control (for example, to wait with a timeout), wait for the thread to exit using the nn::os::WaitObject::WaitOne()
function and then call Join
. Follow this same approach if you need to avoid thread blocking due to the Join()
function.
Call the IsAlive()
function to check whether a thread is still alive (not yet completed).
Call the Finalize()
function to destroy an unneeded thread. When doing so, you must use the following procedure.
If the thread was started using the Start
or TryStart()
functions, you must explicitly call the Join()
function before destroying the thread. Do not call Detach
.
If the thread was started using the StartUsingAutoStack
or TryStartUsingAutoStack()
functions, you must explicitly call the Join
or Detach()
functions before destroying the thread. After calling Detach
, you can only call the Finalize()
function or the destructor for that thread.
9.4. Scheduling
Threads are scheduled according to their priority, and you can set the priority of any thread. Thread priorities can be specified as integers between 0 and 31, with 0 indicating the highest priority and 31 the lowest. Standard threads specify the DEFAULT_THREAD_PRIORITY
of 16.
Call the Yield()
function to yield execution to other threads of the same priority. This has no effect if there are no threads with the same priority.
Call the Sleep()
function to put threads into a sleep state for a specified time.
When scheduling occurs as a result of an interrupt to a currently executing thread, the interrupted thread is placed at the top of the thread queue and scheduling is controlled so there is as little thread switching as possible. If you try to create and execute a new thread with a priority lower than or equal to the currently executing thread, thread switching may not occur if an interrupt from a system process prevents immediate scheduling. We recommend using events to wait for thread switching if you need to ensure thread switching.
Specifying a short time and repeatedly calling Sleep()
places a heavy load on the system core, and reduces the overall system performance.
9.5. Getting and Setting Parameters
Each thread has its own parameters: a thread ID and a priority.
To get a parameter, call the Get*
member function to get the instance parameter; to get the parameter for the current thread, call the GetCurrent*
member function. Similarly, call the Change*
or ChangeCurrent*()
functions to set a parameter.
Call the GetMainThread()
function to get the current thread (main thread) object.
Thread ID
Each thread is assigned its own ID as a bit32
type. You can get these IDs by calling the GetId
or GetCurrentId()
functions, but the IDs cannot be changed.
Priority
You can set the priority for each thread as an s32
type. Use the GetPriority
or GetCurrentPriority()
functions to get the current priority. Call the ChangePriority
or ChangeCurrentPriority()
functions to change the priority.
9.6. Thread-Local Storage
Each thread for storing uptr
types has 16 slots of thread-local storage. You can reserve thread-local storage for use by generating an instance of the nn::os::ThreadLocalStorage
class, but attempting to reserve more than 16 slots will fail, and the application will be forcibly halted by a PANIC
.
To set a value in thread-local storage, call the SetValue()
function; to get a value, call the GetValue()
function. All thread-local storage slots are set to 0 when a thread is started.
Use the nn::os::ThreadLocalStorage
class constructor with parameters to register a callback function to be called when ending a thread. The thread-local storage value is passed as an argument when the callback function is called.
9.7. Synchronization Objects
Access to non-thread-safe libraries or shared resources must be handled by means of the application synchronizing between threads. The SDK provides synchronization objects such as the CriticalSection
class.
9.7.1. Critical Sections
A CriticalSection
is a synchronization object used to provide mutual exclusion. Allowing only one thread at a time to enter a CriticalSection
object effectively prohibits multiple threads from accessing the same resource at the same time. CriticalSection
objects require more memory than mutexes (described below), but are faster in most cases. There is no upper limit on how many can be created as long as there is memory available.
CriticalSection
objects are defined by the nn::os::CriticalSection
class. Generate an instance and call the Initialize
or TryInitialize()
function to initialize, and then call the Enter
or TryEnter()
functions to enter the CriticalSection
and lock its resources. If you call Enter
to lock a CriticalSection
object that is already locked, execution of the thread that called the function is blocked until the existing lock is released. These objects allow recursive locks. A lock request from the thread that has the original lock will be nested (the nesting level will be incremented) without the thread being blocked. If you call TryEnter
instead, the function returns only whether it succeeded in entering the object, and the thread is not blocked.
Call the Leave()
function to release a lock on a CriticalSection
object. If this function is called from the locking thread, the nesting level is decremented and the lock is released when the nesting level reaches 0.
Call the Finalize()
function to explicitly destroy an instance.
Use the nn::os::CriticalSection::ScopedLock
object to lock a CriticalSection
object. The lock is automatically released when the object goes out of scope.
9.7.1.1. Thread Priority Inversion
CriticalSection
objects do not inherit the priority of the thread that generates them. Consequently, if low-priority thread A has a lock on a CriticalSection
object and then high-priority thread B requests a lock on that object, thread B may be blocked by some thread C, which has a priority higher than A, but lower than B. In other words, B's priority is effectively lowered, with B's and C's relative priority levels being inverted.
9.7.2. Mutexes
Mutexes are synchronization objects used for providing mutual exclusion. Much like CriticalSection
objects, these objects effectively prohibit multiple threads from accessing the same resource at the same time. Unlike CriticalSection
objects, they implement thread priority inheritance, and there is a limit of 32 on how many can be created. Due to priority inheritance, if a high-priority thread requests a lock on a mutex object that is already locked by a low-priority thread, the locking thread's priority is temporarily increased to match the priority of the thread that requested a lock.
Mutex objects are defined by the nn::os::Mutex
class. Generate an instance and call the Initialize
or TryInitialize()
function to initialize, and then call the Lock
or TryLock()
functions to lock a mutex object. If you call Lock
to lock a mutex object that is already locked, execution of the thread that called the function will be blocked until the existing lock is released. Mutex objects allow recursive locks. Requesting another lock from the same thread causes the new lock request to be nested. If you call TryLock
instead, the function returns only whether it succeeded in locking the object within the timeout period, and the thread will not be blocked.
Call the Unlock()
function to release the lock on a locked mutex object. Unlock
can only be called from the thread that has the current lock. The object will not be unlocked until all recursive locks have been released.
Call the Finalize()
function to explicitly destroy an instance.
Use the nn::os::Mutex::ScopedLock
class to lock a mutex object. The lock begins when the ScopedLock
object is created. The lock is automatically released when the object goes out of scope.
9.7.3. Events
Events are simple synchronization objects that send notification that an event has occurred. An event object may be in the signaled or non-signaled state, and transitions from the non-signaled to the signaled state when the specified event has occurred. You can synchronize threads by having them wait for an event to occur. You can only have 32 instances of the Event
class at any one time.
Event objects are defined by the nn::os::Event
class. Generate an instance and then call the Initialize()
function to initialize. During initialization, you can choose whether the event is a manually resetting event or an automatically resetting event. When manually resetting events enter the signaled state, they remain in that state until manually cleared, during which time all threads waiting for that event are released. When automatically resetting events enter the signaled state, only the highest priority thread of all those threads waiting for it to enter the signaled state is released, after which the object resets itself to the non-signaled state.
Call the Wait()
function to cause threads to wait until an event enters the signaled state. You can specify the length of the timeout period and also check to determine whether an event has occurred during the timeout period. If you set the timeout period to 0, control returns immediately, allowing you to check for event occurrence without execution being blocked.
Call the Signal()
function to put an event in the signaled state. When a manually resetting event enters the signaled state, it remains in that state until manually cleared by calling the ClearSignal()
function. If one or more threads are waiting when an automatically resetting event enters the signaled state, only the highest priority thread of all those threads waiting for it to enter the signaled state will be released, after which the object resets itself to the non-signaled state. If no threads are waiting for events, the object remains in the signaled state.
Call the Finalize()
function to explicitly destroy an instance.
9.7.3.1. Light Events
Light events are simple synchronization objects that send flag notifications between threads. They are functionally no different from standard events.
Light events are defined using the nn::os::LightEvent
class. The nn::os::LightEvent
class is better than the nn::os::Event
class in most respects. The only exception is that it cannot wait for multiple synchronization objects like nn::os::WaitObject::WaitAny()
can. We recommend the use of the nn::os::LightEvent
class whenever possible. There is no upper limit on how many can be created as long as there is memory available.
You must call the Initialize()
function on instances that were created using the constructor that has no arguments. During initialization, you can choose whether the event is a manually resetting event or an automatically resetting event. The behavior is the same as the nn::os::Event
class in either case. You can also specify the type of event using the non-default constructor. There is no need to call the Initialize()
function on instances that were created using the constructor that takes arguments.
With the exception that the Wait()
member function of the nn::os::LightEvent
class does not allow you to specify a timeout, the Wait()
, Signal()
, ClearSignal()
, and Finalize()
member functions of this class work just like the corresponding functions in the nn::os::Event
class.
The nn::os::LightEvent
class adds the following member functions.
You can check the flag using the TryWait()
member function. This function only returns the flag's status and does not block execution of the thread. For LightEvent
s that are configured as automatically resetting events, the flag is only cleared (that is, set to false
to indicate the "non-signaled" state) if the flag had previously been set (set to true
to indicate the "signaled" state). You can use the TryWait()
function to specify a timeout time.
The Pulse()
member function releases threads that are waiting for the flag to be set. For automatically resetting LightEvent
objects, it only releases the single highest-priority thread and clears the flag. Unlike the Signal()
member function, it clears the flag even if no threads are waiting. For manually resetting events, it releases all threads that are waiting, and then clears the flag.
9.7.4. Semaphores
Semaphores are synchronization objects that have counters. Every time there is a request to acquire a semaphore, the semaphore counter decrements by 1, while the thread that requested the semaphore waits for the counter to be greater than 0. When a semaphore is released, its counter is incremented by 1. After its counter is greater than 0, the semaphore is passed to the highest-priority thread of those threads that are waiting on the semaphore. Semaphores can be used to manage resources by limiting the number of threads that can access those resources at the same time. You can only have eight instances of the Semaphore
class at any one time.
Semaphore objects are defined by the nn::os::Semaphore
class. Generate an instance and then call the Initialize
or TryInitialize()
function to initialize. When initializing, specify the counter's initial and maximum values.
Call the Acquire
or TryAcquire()
functions to acquire a semaphore. If the counter is less than 0 when Acquire
is called, the calling thread blocks until the semaphore can be obtained. You can specify the length of the timeout period when calling TryAcquire()
and also check to determine whether the semaphore was obtained during the timeout period. If you set the timeout period to 0, control returns immediately, allowing you to check for semaphore acquisition without execution being blocked.
Call the Release()
function to release an obtained semaphore. You can specify either 1 or something other than 1 for the counter increment value; this value is used to assign the initial value of instances that were created with an initial value of 0. When releasing a semaphore, call the function with no arguments so that the counter increment is 1.
Call the Finalize()
function to explicitly destroy an instance.
9.7.4.1. Light Semaphores
Light semaphores are synchronization objects that have the same features as standard semaphores. Light semaphores are defined using the nn::os::LightSemaphore
class. The nn::os::LightSemaphore
class is better than the nn::os::Semaphore
class in most respects. The only exception is that it cannot wait for multiple synchronization objects like nn::os::WaitObject::WaitAny()
can. We recommend the use of nn::os::LightSemaphore
class whenever possible. There is no upper limit on how many can be created as long as there is memory available.
The difference between light semaphores and standard semaphores is that light semaphores do not define a TryInitialize
member function. The member functions of the nn::os::LightSemaphore
class work just like the corresponding functions in the nn::os::Semaphore
class.
9.7.5. Blocking Queues
Blocking queues are synchronization objects used for safely passing messages between threads. Thread execution is blocked when attempting to either insert more elements into a queue than the buffer size allows or extract an element from an empty queue. When a thread is waiting for messages from multiple other threads, blocking queues can be used to pass messages from one to many, or from many to many. These blocking queues are equivalent to the "message queue" feature in the NITRO, TWL, and Revolution SDKs.
Two classes define blocking queues: nn::os::BlockingQueue
and nn::os::SafeBlockingQueue
. nn::os::BlockingQueue
uses CriticalSection
objects internally for synchronizing between threads, which could lead to deadlocks in situations where priorities might be inverted. If this problem is a possibility, use nn::os::SafeBlockingQueue
, which uses mutexes to avoid such issues. The member variables and functions are the same for both classes.
Generate an instance and then call the Initialize
or TryInitialize()
function to initialize. When initializing, specify an array of type uptr
and the number of array elements to use as the queue buffer.
Call the Enqueue
or TryEnqueue()
functions to add an element (of type uptr
) to the end of the queue. If the queue is full when Enqueue
is called, the calling thread is blocked until an element can be added to the queue. When calling TryEnqueue
, control is returned whether or not an element was successfully added, allowing you to attempt to add an element without execution being blocked.
Call the Jam
or TryJam()
functions to insert an element at the beginning of the queue. If the queue is full when Jam
is called, the calling thread is blocked until an element can be added to the queue. When calling TryJam
, control is returned regardless of whether an element was successfully added, allowing you to attempt to add an element without execution being blocked.
Call the Dequeue
or TryDequeue()
functions to remove an element from the beginning of the queue. If the queue is empty when Dequeue
is called, the calling thread is blocked until an element can be removed from the queue. When calling TryDequeue
, control is returned regardless of whether an element was successfully removed, allowing you to attempt to remove an element without execution being blocked.
Call the GetFront
or TryGetFront()
functions to get the first element in a queue without removing the element. If the queue is empty when GetFront
is called, the calling thread is blocked until an element can be obtained from the queue (that is, until an element is added to the queue). When calling TryGetFront
, control is returned regardless of whether an element was successfully obtained, allowing you to attempt to remove an element without execution being blocked.
Call the Finalize()
function to explicitly destroy an instance.
9.7.6. Light Barriers
Light barriers are synchronization objects that wait for the arrival of multiple threads. Until the number of threads specified during initialization is met, any threads that arrive early are made to wait. However, this class cannot be used to wait for the arrival of M out of N threads (M < N).
Light barriers are defined using the nn::os::LightBarrier
class. You must call the Initialize()
function on instances that were created using the constructor that has no arguments. During initialization, you specify how many threads to wait for. You can also specify the number of threads to wait for using the version of the constructor that takes arguments. There is no need to call the Initialize()
function on instances that were created using the constructor that takes arguments. There is no upper limit on how many light barriers can be created as long as there is memory available.
The Await
member function waits for other threads to arrive. Thread execution is blocked until the number of threads specified when this function was called have arrived.
9.7.7. Deadlocks
You must make sure to avoid deadlocking when using critical sections, mutexes, and semaphores for mutual exclusion between multiple threads.
For example, assume two threads A and B, which need mutex objects X and Y to access resources. If A locks X and B locks Y, both threads A and B will be permanently blocked. This situation is known as being deadlocked.
One simple method to avoid deadlocks is to have all threads that need to access resources request mutex locks in the same predetermined order.
9.8. Upper Limit of Resources
The number of instances that can be generated at the same time for threads, synchronous objects, and timers is restricted.
The number of instances that can be generated is restricted for the following classes.
nn::os::Thread
class (see 9.1. Initializing and Starting through 9.6. Thread-Local Storage.)nn::os::Event
class (see 9.7.3. Events.)nn::os::Mutex
class (see 9.7.2. Mutexes.)nn::os::Semaphore
class (see 9.7.4. Semaphores.)nn::os::Timer
class (see 8.3. Timers.)
The following sample shows the member functions that are defined to get that number of resources and the upper limit.
static s32 GetCurrentCount(); static s32 GetMaxCount();
The GetCurrentCount()
function returns the number of instances currently being used.
The GetMaxCount()
function returns the upper limit value for the number of instances that can be generated.
Because the library and other functions begin generating these instances as soon as the application has started, further restrictions to the number of resources the application can use might occur. For more information, see 8. Upper Limit of Resources in the CTR System Programming Guide, and the explanations for each library.