| ## Threadpool | |
| ### Overview | |
|  | |
| An api that lets you create a pool of worker threads, and a queue of tasks that | |
| are bound to a wsi. Tasks in their own thread synchronize communication to the | |
| lws service thread of the wsi via `LWS_CALLBACK_SERVER_WRITEABLE` and friends. | |
| Tasks can produce some output, then return that they want to "sync" with the | |
| service thread. That causes a `LWS_CALLBACK_SERVER_WRITEABLE` in the service | |
| thread context, where the output can be consumed, and the task told to continue, | |
| or completed tasks be reaped. | |
| ALL of the details related to thread synchronization and an associated wsi in | |
| the lws service thread context are handled by the threadpool api, without needing | |
| any pthreads in user code. | |
| ### Example | |
| https://libwebsockets.org/git/libwebsockets/tree/minimal-examples/ws-server/minimal-ws-server-threadpool | |
| ### Lifecycle considerations | |
| #### Tasks vs wsi | |
| Although all tasks start out as being associated to a wsi, in fact the lifetime | |
| of a task and that of the wsi are not necessarily linked. | |
| You may start a long task, eg, that runs atomically in its thread for 30s, and | |
| at any time the client may close the connection, eg, close a browser window. | |
| There are arrangements that a task can "check in" periodically with lws to see | |
| if it has been asked to stop, allowing the task lifetime to be related to the | |
| wsi lifetime somewhat, but some tasks are going to be atomic and longlived. | |
| For that reason, at wsi close an ongoing task can detach from the wsi and | |
| continue until it ends or understands it has been asked to stop. To make | |
| that work, the task is created with a `cleanup` callback that performs any | |
| freeing independent of still having a wsi around to do it... the task takes over | |
| responsibility to free the user pointer on destruction when the task is created. | |
|  | |
| #### Reaping completed tasks | |
| Once created, although tasks may run asynchronously, the task itself does not | |
| get destroyed on completion but added to a "done queue". Only when the lws | |
| service thread context queries the task state with `lws_threadpool_task_status()` | |
| may the task be reaped and memory freed. | |
| This is analogous to unix processes and `wait()`. | |
| If a task became detached from its wsi, then joining the done queue is enough | |
| to get the task reaped, since there's nobody left any more to synchronize the | |
| reaping with. | |
| ### User interface | |
| The api is declared at https://libwebsockets.org/git/libwebsockets/tree/include/libwebsockets/lws-threadpool.h | |
| #### Threadpool creation / destruction | |
| The threadpool should be created at program or vhost init using | |
| `lws_threadpool_create()` and destroyed on exit or vhost destruction using | |
| first `lws_threadpool_finish()` and then `lws_threadpool_destroy()`. | |
| Threadpools should be named, varargs are provided on the create function | |
| to facilite eg, naming the threadpool by the vhost it's associated with. | |
| Threadpool creation takes an args struct with the following members: | |
| Member|function | |
| ---|--- | |
| threads|The maxiumum number of independent threads in the pool | |
| max_queue_depth|The maximum number of tasks allowed to wait for a place in the pool | |
| #### Task creation / destruction | |
| Tasks are created and queued using `lws_threadpool_enqueue()`, this takes an | |
| args struct with the following members | |
| Member|function | |
| ---|--- | |
| wsi|The wsi the task is initially associated with | |
| user|An opaque user-private pointer used for communication with the lws service thread and private state / data | |
| task|A pointer to the function that will run in the pool thread | |
| cleanup|A pointer to a function that will clean up finished or stopped tasks (perhaps freeing user) | |
| Tasks also should have a name, the creation function again provides varargs | |
| to simplify naming the task with string elements related to who started it | |
| and why. | |
| #### The task function itself | |
| The task function receives the task user pointer and the task state. The | |
| possible task states are | |
| State|Meaning | |
| ---|--- | |
| LWS_TP_STATUS_QUEUED|Task is still waiting for a pool thread | |
| LWS_TP_STATUS_RUNNING|Task is supposed to do its work | |
| LWS_TP_STATUS_SYNCING|Task is blocked waiting for sync from lws service thread | |
| LWS_TP_STATUS_STOPPING|Task has been asked to stop but didn't stop yet | |
| LWS_TP_STATUS_FINISHED|Task has reported it has completed | |
| LWS_TP_STATUS_STOPPED|Task has aborted | |
| The task function will only be told `LWS_TP_STATUS_RUNNING` or | |
| `LWS_TP_STATUS_STOPPING` in its status argument... RUNNING means continue with the | |
| user task and STOPPING means clean up and return `LWS_TP_RETURN_STOPPED`. | |
| If possible every 100ms or so the task should return `LWS_TP_RETURN_CHECKING_IN` | |
| to allow lws to inform it reasonably quickly that it has been asked to stop | |
| (eg, because the related wsi has closed), or if it can continue. If not | |
| possible, it's okay but eg exiting the application may experience delays | |
| until the running task finishes, and since the wsi may have gone, the work | |
| is wasted. | |
| The task function may return one of | |
| Return|Meaning | |
| ---|--- | |
| LWS_TP_RETURN_CHECKING_IN|Still wants to run, but confirming nobody asked him to stop. Will be called again immediately with `LWS_TP_STATUS_RUNNING` or `LWS_TP_STATUS_STOPPING` | |
| LWS_TP_RETURN_SYNC|Task wants to trigger a WRITABLE callback and block until lws service thread restarts it with `lws_threadpool_task_sync()` | |
| LWS_TP_RETURN_FINISHED|Task has finished, successfully as far as it goes | |
| LWS_TP_RETURN_STOPPED|Task has finished, aborting in response to a request to stop | |
| The SYNC or CHECKING_IN return may also have a flag `LWS_TP_RETURN_FLAG_OUTLIVE` | |
| applied to it, which indicates to threadpool that this task wishes to remain | |
| unstopped after the wsi closes. This is useful in the case where the task | |
| understands it will take a long time to complete, and wants to return a | |
| complete status and maybe close the connection, perhaps with a token identifying | |
| the task. The task can then be monitored separately by using the token. | |
| #### Synchronizing | |
| The task can choose to "SYNC" with the lws service thread, in other words | |
| cause a WRITABLE callback on the associated wsi in the lws service thread | |
| context and block itself until it hears back from there via | |
| `lws_threadpool_task_sync()` to resume the task. | |
| This is typically used when, eg, the task has filled its buffer, or ringbuffer, | |
| and needs to pause operations until what's done has been sent and some buffer | |
| space is open again. | |
| In the WRITABLE callback, in lws service thread context, the buffer can be | |
| sent with `lws_write()` and then `lws_threadpool_task_sync()` to allow the task | |
| to fill another buffer and continue that way. | |
| If the WRITABLE callback determines that the task should stop, it can just call | |
| `lws_threadpool_task_sync()` with the second argument as 1, to force the task | |
| to stop immediately after it resumes. | |
| #### The cleanup function | |
| When a finished task is reaped, or a task that become detached from its initial | |
| wsi completes or is stopped, it calls the `.cleanup` function defined in the | |
| task creation args struct to free anything related to the user pointer. | |
| With threadpool, responsibility for freeing allocations used by the task belongs | |
| strictly with the task, via the `.cleanup` function, once the task has been | |
| enqueued. That's different from a typical non-threadpool protocol where the | |
| wsi lifecycle controls deallocation. This reflects the fact that the task | |
| may outlive the wsi. | |
| #### Protecting against WRITABLE and / or SYNC duplication | |
| Care should be taken than data prepared by the task thread in the user priv | |
| memory should only be sent once. For example, after sending data from a user | |
| priv buffer of a given length stored in the priv, zero down the length. | |
| Task execution and the SYNC writable callbacks are mutually exclusive, so there | |
| is no danger of collision between the task thread and the lws service thread if | |
| the reason for the callback is a SYNC operation from the task thread. | |
| ### Thread overcommit | |
| If the tasks running on the threads are ultimately network-bound for all or some | |
| of their processing (via the SYNC with the WRITEABLE callback), it's possible | |
| to overcommit the number of threads in the pool compared to the number of | |
| threads the processor has in hardware to get better occupancy in the CPU. |