It is possible to receive a request, create a process or thread, service the request, and return to the caller the results of the operation. Many years ago, creating a process was the default approach. The issue was that creating and destroying a process when done are quite expensive operations.
At some point in time, threads and later fibers were developed. The advantage of a thread over a process is the speed to create and destroy a thread and the time and less amount of resources required when comparing with a process. That said; it still takes some time to create and destroy a thread. For this reason, the idea came up to create a thread pool with two or more threads. This approach circumvents the time to create and destroy threads.
What is a thread pool? If you are interested in the subject, you may wish to take a look here. I have some experience developing thread pools. The reason is that for many years I worked on a storage server. For performance the software was developed using the C programming language. Some things were bought while others were developed in house. One of the home grown frameworks that I developed was pool of threads.
The work pool thread library was implemented with a data structure and a set of functions. The idea is quite straightforward. You need a queue, a mutex and the actual data structure to manage the thread pool. At the time and for portability, all was written in ANSI C using Microsoft Visual Studio.
The description of the data structure and functions / methods follows:
typedef struct SENCOR_MONITOR_WORKER { HANDLE *semaphore; unsigned long totalThreads; HANDLE *workMutex; SENCOR_QUEUE *workQ; HANDLE *workSema; void (__cdecl *workThread)(struct SENCOR_MONITOR_WORKER *); unsigned char filler[SENCOR_MONITOR_WORKER_LEN - 24]; } SENCOR_MONITOR_WORKER; SENCOR_EXPORT void __cdecl MonitorWorkerThreads ( SENCOR_MONITOR_WORKER *threadArgs ); SENCOR_EXPORT INT_CALL ThreadCancelWork ( HANDLE *workMutex, SENCOR_QUEUE *workQ, HANDLE *workSema, void *privateData ); SENCOR_EXPORT INT_CALL ThreadCheckPool ( HANDLE *workMutex, SENCOR_QUEUE *workQ, unsigned long *pendingTasks ); SENCOR_EXPORT INT_CALL ThreadGetThreadCount ( HANDLE *workMutex, SENCOR_QUEUE *workQ, HANDLE *workSema, unsigned long *totalThreads ); SENCOR_EXPORT INT_CALL ThreadGetWork ( HANDLE *workMutex, SENCOR_QUEUE *workQ, HANDLE *workSema, void **privateData ); SENCOR_EXPORT INT_CALL ThreadKillThreads ( HANDLE *workMutex, SENCOR_QUEUE *workQ, HANDLE *workSema ); SENCOR_EXPORT INT_CALL ThreadPutWork ( HANDLE *workMutex, SENCOR_QUEUE *workQ, HANDLE *workSema, void *privateData ); SENCOR_EXPORT INT_CALL ThreadWorkerThreads ( unsigned long totalThreads, SENCOR_QUEUE **workQ, HANDLE **workMutex, HANDLE **workSema, void (__cdecl *workThread)(SENCOR_MONITOR_WORKER *) );
Now a days, it seems that every programming language has thousands of libraries and have implemented thread pools in different ways; some simpler than others but the number of features vary. In this post we will look at the Executor class in Java. I will be using the Eclipse IDE for developing the code.
The Java code is implemented in two source files. The first deals with the main program which shows how easy it is to create a pool of threads, provide them with some work, and wait for the work to be completed. Please note that in practice, servers create multiple pools of threads and just need to wait on single threads to complete a task in order to return results to the client.
The code for the main program follows:
package com.canessa.threadpool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * */ public class SimpleThreadPool { /** * */ public static void main(String[] args) { // **** **** final int workCount = 7; final int numOfWorkers = 3; // **** initial message **** System.out.println(""); // **** create thread pool **** ExecutorService executor = Executors.newFixedThreadPool(numOfWorkers); // **** provide work for the threads **** System.out.println("main <<< providing work ..."); for (int i = 0; i < workCount; i++) { Runnable worker = new WorkerThread("" + i); executor.execute(worker); } System.out.println("main <<< work provided !!!"); // **** wait for all the work to be completed **** executor.shutdown(); while (!executor.isTerminated()) { System.out.println("main <<< waiting for work to be completed ..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } // **** final message **** System.out.println("main <<< work completed !!!"); } }
Each pool of threads needs to be instantiated and need to be able to wait for work, pass the work to available threads and perform the work.
The code for this part follows:
package com.canessa.threadpool; /** * */ public class WorkerThread implements Runnable { // **** **** final int threadDelay = 5000; // **** **** private String command; /** * Constructor */ public WorkerThread(String s){ this.command = s; } /** * Process work requests. */ @Override public void run() { System.out.println("run <<< name ==>" + Thread.currentThread().getName() + "<== start command ==>" + command + "<=="); processCommand(); System.out.println("run <<< name ==>" + Thread.currentThread().getName() + "<== end !!!"); } /** * Process the work. */ private void processCommand() { try { Thread.sleep(threadDelay); } catch (InterruptedException e) { e.printStackTrace(); } } }
Now that we had an opportunity to look at the code, let’s give it a try. What follows is a screen capture of the Eclipse IDE console when the program is executed:
main <<< providing work ... main <<< work provided !!! run <<< name ==>pool-1-thread-2<== start command ==>1<== run <<< name ==>pool-1-thread-3<== start command ==>2<== run <<< name ==>pool-1-thread-1<== start command ==>0<== main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... run <<< name ==>pool-1-thread-3<== end !!! run <<< name ==>pool-1-thread-2<== end !!! run <<< name ==>pool-1-thread-1<== end !!! run <<< name ==>pool-1-thread-2<== start command ==>3<== run <<< name ==>pool-1-thread-1<== start command ==>4<== run <<< name ==>pool-1-thread-3<== start command ==>5<== main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... run <<< name ==>pool-1-thread-2<== end !!! run <<< name ==>pool-1-thread-2<== start command ==>6<== run <<< name ==>pool-1-thread-1<== end !!! run <<< name ==>pool-1-thread-3<== end !!! main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... main <<< waiting for work to be completed ... run <<< name ==>pool-1-thread-2<== end !!! main <<< work completed !!!
Of course, with time, software needs to be enhanced. This is due to new or improved requirements. Due to this fact, software needs to evolve. What used to fit the requirements a few months ago may fall short today. This is why software should always be developed as simple as possible keeping in mind that it will have to be enhanced in the future. Obfuscation by having stacks with dozens of function / method calls makes it hard to understand and maintain. Always follow the KISS (Keep It Simple & Short) rule. Does this recommendation seem not compliant with how some software is developed, then open a python console and enter the following:
C:\Users\John>python Python 3.5.2 (v3.5.2:4def2a2901a5, Jun 25 2016, 22:18:55) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those! >>>
If you have comments or questions regarding this or any other post, please leave me a comment or send me a private message via email.
Enjoy;
John
@john_canessa