IMPORTANT: The following article on threads, especially a discussion on library scan may change in a future add-on release (I'm experimenting with a new routine for library scan that will use timers instead of a threaded while loop; I'll provide an update via an addendum if timer routine experiment is successful).
Of all the work done on Studio add-on, one of them stands out the most: background tasks. I spent many hours and months perfecting this concept, read documentation on this feature and learned a lot through this experience. Today, this work is used in various parts of the Studio app module and beyond, and we'll take a look at two most important results of this work: library scan and microphone alarm.
Brief feature overview
When you are producing a live show, you may forget that your microphone is active. The microphone alarm feature lets NVDA notify you if microphone has been active for a while. This happens even if you are using another program.
Library scan comes in handy when you want to see the progress of a background library scan. Typically, you would initiate library scans from Insert Tracks dialog (press Control+Shift+R). NvDA will then tell you how the scan is going, and if you close Insert Tracks dialog, NVDA will continue to monitor library scans in the background.
But there's more to it than a simple description when it comes to looking at internals of these features. As you'll see, these features use a concept that is gaining traction: running multiple tasks at once, or at least try to emulate it. We'll visit this concept first before returning to our regularly scheduled program of describing the internals of the two features above.
Recent trends in computing: more and more processors in a single computer
A decade ago, people thought a single core CPU was enough to run multiple programs. This involved the processor spending small fraction of a second devoted to each program. Nowadays, it has become common to see desktops, laptops, smartphones and other small devices using at least two cores (termed multi-core; two cores is dubbed "dual core"). As of August 2015, many computers use processors with four cores (dubbed "quad core"), while enthusiasts prefer more cores (the current record holder for desktop computers (as of August 2015) is 8 (octa core), held by Intel Core I7-5960X, a 3 GHz unlocked processor which costs about a thousand dollars; for servers where more cores are used, the current record holder is a package of eight processors (octa processor) called Intel Xeon E7-8895V3, with each processor boasting eighteen cores with base speed of 2.6 GHz).
Despite the fact that many computers come equipped with multi-core processors, not all programs take advantage of this. Python interpreter is one of those programs, and since NVDA is a Python-based screen reader and due to its operational architecture, many of its operations cannot take advantage of multiple processors. Fortunately, Python provides a way to simulate this - run certain tasks in the background, and this is utilized by NVDA and some of its add-ons as you'll see in this article on library scan and microphone alarm.
A gentle introduction to threads: multiple tasks at once
During normal business hours, a program will run from beginning to end with some interuptions (keyboard input, switching to a different part of the program and so on). However, there are times when the program will need to work with many things simultaneously, such as calculating distance between many points, adding multiple numbers at once, comparing many pairs of strings and so on. Fortunately, a mechanism called threads allow a program to do multiple things simultaneously.
A thread is a procedure independent of other tasks. If one thread is busy with something, other threads can work on other tasks. The best analogy is multiple bank tellers in a bank: customers can talk to different tellers, with one teller working on updating customer records for a customer while another customer discusses fraudulent credit card charges with a different teller.
A thread can be involved with parts of a task, devoted to a single task or multiple tasks. For example, an antivirus program could have multiple threads (workers) working independently of each other. One worker can display the overall progress of a scan, while other threads can scan multiple drives at once, with each thread devoted to scanning files and folders on separate drives. In NVDA world, multiple workers are involved to perform various tasks, including making sure NVDA is responsive, handling browse mode in different web browsers and so on.
Threads: a more geeky introduction
A thread (sometimes termed "thread of execution) is an independent path of execution. A single process (app) can have as many threads as it desires (minimum is one for the main thread). Each thread can be asked to perform certain operations with other threads in parallel, which can range from a single, repetative task (part of a function) to being responsible for an entire module or a significant part of the program. In case of antivirus example above, each scanner thread is responsible for scanning an entire drive, with each of them reporting its progress to a manager thread which displays overall progress of a virus scan.
Using threads means each thread can execute on a processor core on a multi-core system. Because of this, many people would want many programs to take advantage of this and finish their jobs faster. However, threads introduce disadvantages, namely many days spent designing careful coordination routines between threads, preventing attempts by multiple threads to change a critical value that a manager thread depends on (called race condition) and so forth.
Python's way of managing threads and the threading module
Python interpreter (and programs which uses them, including NVDA) is not exactly multithreaded. Because of internal issues, Python uses so-called global interpreter lock to prevent multiple threads from messing with each other. One way to bring true parallelism in Python is use of multiprocessing module (multiple Python interpreters, each one devoted to a single task), which has its own advantages and drawbacks (NVDA does not ship with multiprocessing module in the first place).
To manage threads, Python programs (including NVDA) use Python's threading module. This library includes various ways of managing threads, including defining which function can execute in a separate thread, coordinating sharing of information between threads (locks, semaphores (resource access counter) and so on), and letting a thread run its task after waiting for a while (called timers). Even with multiple threads defined, NVDA is mostly single-threaded (serial execution).
To use threads, a programmer will define the thread type (regular thread, timer and so on), define some properties and tell the thread which routine to execute. Once the thread is defined, the start (thread.start) method is called to let the thread do its work.
Threads in Studio app module
For the most part, Studio app module uses only one thread (NVDA's main thread) to do its job. However, there are times when multiple threads are used - up to three can be active at a time: NVDA's main thread (announcing status changes, alarms, Cart Explorer and others), microphone alarm (a timer) and library scan (a background thread). Another situation threads are used is when background encoder monitoring is enabled (see the encoder routines article for details and use of threads there).
The main reason for using threads is to prevent background tasks from blocking user input (commands will not work when a long running task is run from the main NVDA thread). This is more noticeable when library scan is active as you'll find out soon. For now, let's take a look at microphone alarm.
Microphone alarm: A timer waiting to do its work
Simply put, microphone alarm is a timer (akin to a countdown timer). When the microphone becomes active, Studio app module will tell a timer thread to come alive. This timer's only job is to play the alarm sound and display a warning message, and it will wait a while (microphone alarm value in seconds; for example, five seconds).
The master switch which flips the microphone alarm timer is:
alarm = threading.Timer(micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage])
Where "micAlarm" denotes how long this timer will wait and the second argument is the task to be performed (messageSound function). If microphone alarm is off (value is 0), this switch will be left alone forever (turned off until you enable the alarm by specifying a value above 0).
However, microphone alarm is more than a timer: a unique feature of timers is responding to events (a cancel event, that is). When the microphone becomes active, microphone alarm timer will become active. If you happen to turn off your microphone before microphone alarm kicks in, NVDA instructs microphone alarm to quit (timer is canceled). In other words, the "real" master switch is status change, and one of the activities performed by name change event handler (event_nameChange function described earlier) is to manage microphone alarm timer via doExtraAction method (in fact, microphone alarm and Cart Explorer are managed from this function).
Library scan: a unique combination of Studio API and a background thread
When NVDA is told to keep an eye on background library scanning, it calls up another thread to perform this duty. This thread will ask Studio for number of items scanned so far and take appropriate action after scanning is complete (in fact, multiple helper functions are used).
The library scan routine is performed as follows:
1. NVDA will make sure you are not in Insert Tracks dialog (if you are, background library scan routine will not be invoked, as event_nameChange will perform this duty instead).
2. If you do close Insert Tracks while a scan is in progress, or invoke library scan from SPL Assistant (Shift+R), NVDA will instruct a thread to keep an eye on scan progress in the background(see below for signature) to allow you to use Studio commands and to let you hear scan progress from other programs.
3. Library scan thread will ask Studio to return number of items scanned (this is done every second) and will store the result for record keeping.
4. After the scan result is obtained, the thread will check where you are in studio, and if you are back in Insert Tracks dialog, the thread will terminate (see step 1).
5. Every five seconds, library scan thread will call a private function (which wants to see how many items were scanned and current library scan announcement setting) to announce library scan results as follows:
A. If you tell NVDA to announce scan progress, NVDA will say, "scanning" and/or play a beep (if told to do so).
B. If NVDA is told to announce scan count, number of items scanned so far will be announced (again with or without a beep).
C. This reporter function will not be invoked if you tell NVDA to ignore library scan completely or ask it to interupt you only when the overall scan is complete (you can press Alt+NVDA+R to cycle through different library scan announcement settings).
6. Once library scanning is complete (after checking scan result value every second and seeing that the previous scan result and the current one have same values), NVDA will announce scan results (in some cases, number of items scanned will be announced).
You can imagine what would have happened if the above operation was not a background task: cannot perform other NvDA commands until library scan is complete, cannot cancel this operation and what not. And this is the signature of the thread that performs the above operation:
libraryScanner = threading.Thread(target=self.libraryScanReporter, args=(_SPLWin, countA, countB, parem))
There are important arguments in use: the function (task) to be performed and arguments for this function. The most important argument is the last one: Studio 5.0x and 5.10 expects different arguments when told to report number of items scanned so far.
Despite limitations of Python's threading routines, if used properly, it can open new possibilities, and you saw some of them above: microphone alarm and background library scan. Use of threads in the Studio app module also allows NvDA to be responsive while using Studio and allows background tasks to be faithful to the tasks at hand. We'll come back to threads when we talk about encoder connection routines. There is a more "magical" feature we'll visit, and this is our next stop on the SPL Studio Add-on Internals: Cart Explorer.
1. Thread (Wikipedia): https://en.wikipedia.org/wiki/Thread_(computing)
2. Multi-core processor (wikipedia): https://en.wikipedia.org/wiki/Multi-core_processor
3. Multi-core introduction, Intel Developer Zone, March 5, 2012: https://software.intel.com/en-us/articles/multi-core-introduction
4. Intel Core I7-5960X specifications (Intel ARK): http://ark.intel.com/products/82930/Intel-Core-i7-5960X-Processor-Extreme-Edition-20M-Cache-up-to-3_50-GHz
5. Intel Xeon E7-8895V3 specifications (Intel ARK): http://ark.intel.com/products/84689/Intel-Xeon-Processor-E7-8895-v3-45M-Cache-2_60-GHz
6. Global Interpreter Lock (Python Wiki): https://wiki.python.org/moin/GlobalInterpreterLock
7. Threading (Python documentation, Python Software Foundation): https://docs.python.org/2/library/threading.html
8. Multiprocessing (Python documentation, Python Software Foundation): https://docs.python.org/2/library/multiprocessing.html#module-multiprocessing