This API implements a task parallelization mechanism based on the thread pool pattern.
| Interface | Implementation |
|---|---|
wqm.h |
wqm.c |
Create a static and dynamic library using :
$ make libs
cc -O -fPIC -std=c11 -I. -I../minimaps -c -o wqm.o wqm.c
ar rcs libwqm.a wqm.o
cc -shared -o libwqm.so wqm.o
| Static library | Dynamic library |
|---|---|
libwqm.a |
libwqm.so |
It requires libmap and libtimer implemented in minimaps.
The thread pool pattern allows to run parallel tasks easily without the burden of thread management.
The user:
- declares a thread pool and chooses the maximum number of parallel workers in charge of the execution of tasks (
threadpool_create_and_start ()) ; - submits tasks to the thread pool (
threadpool_add_task ()) that will be passed to one of available workers and will be executed asynchronously ; - waits for all the tasks to be completed (
threadpool_wait_and_destroy ()).
This C standard implementation of a thread pool brings unique features, not found anywhere else at the time of writing.
- Standard C: It uses the standard (minimalist) C11 thread library <threads.h> (§7.28 of ISO/IEC C programming language), rather the POSIX threads. It can therefore be ported more easily to systems other than unix-like systems.
- Data management: The data passed to tasks (via
threadpool_add_task ()) can be accessed, retrieved and released multi-thread-safely after completion of the task (via the user-defined functionjob_delete ()), allowing collecting data at task termination. - Global data management: Global data can be defined and accessed (via
threadpool_global_data ()) by all tasks. - Worker data management: Local data can be defined (via
threadpool_set_worker_local_data_manager ()) and accessed (viathreadpool_worker_local_data ()) for each worker of the thread pool. - Resource management: Global resources can be allocated and deallocated (via
threadpool_set_global_resource_manager ()) and accessed (viathreadpool_global_resource ()) for the thread pool. - Worker life-time management: Workers will stay alive for a short idle time, ready to process new submitted tasks, even though
threadpool_wait_and_destroy ()has already been called and no tasks are available, as long as some other tasks are still being processed and could therefore create new tasks dynamically. - Monitoring facility: The activity of the thread pool can be monitored and displayed by a front-end user defined function (via
threadpool_set_monitor ()). - Task cancellation: Pending tasks can be cancelled after submission (via
threadpool_cancel_task ()). - Virtual tasks: The thread pool can wait for asynchronous calls without blocking workers (via
threadpool_task_continuation ()andthreadpool_task_continue ()).
Those features are detailed below.
| Function | Description |
|---|---|
threadpool_create_and_start |
Creates and starts a new pool of workers |
threadpool_add_task |
Adds a task to the pool of workers |
threadpool_wait_and_destroy |
Waits for all the tasks to be done and destroy the pool of workers |
Those features are detailed below.
| Function | Description |
|---|---|
threadpool_global_data |
Gives access to the user defined shared global data of the pool of workers |
threadpool_set_worker_local_data_manager |
Defines the workers' local data manager functions |
threadpool_worker_local_data |
Gives access to the user defined local data of a worker |
threadpool_set_global_resource_manager |
Defines the resource manager functions |
threadpool_global_resource |
Gives access to the global resource of the thread pool |
Those features are detailed below.
| Function | Description |
|---|---|
threadpool_task_continuation |
Defines a virtual task to be processed after the response of an asynchronous call |
threadpool_task_continue |
Callback function to be called by the callback function of an asynchronous call to proceed a virtual task |
Those features are detailed below.
| Function | Description |
|---|---|
threadpool_nb_workers |
Gets the number of requested workers in a threadpool |
threadpool_current |
Gives access to the current threadpool |
threadpool_current_worker_no |
Gets the current worker sequence number |
threadpool_cancel_task |
Cancels either all pending tasks, or the last, or the next submitted task, or a specific task |
threadpool_set_monitor |
Sets a user-defined function to retrieve and display monitoring information of the thread pool activity |
threadpool_set_idle_timeout |
Modifies the idle time out (default is 0.1 s) before an idle worker terminates |
Those features are detailed below.
The API is defined in file wqm.h.
struct threadpool *threadpool_create_and_start (size_t nb_workers,
void *global_data,
tp_property_t property)A thread pool is declared and started with a call to threadpool_create_and_start ().
The first argument nb_workers is the number of requested workers, that is the maximum number of tasks that should be executed in parallel by the system.
TP_WORKER_NB_CPUcan be used as first argument to fit to the number of processors currently available in the system (as returned byget_nprocs ()with GNU C standard library).TP_WORKER_SEQUENTIALcan be used as first argument to create a sequential thread pool: tasks will be executed asynchronously, one at a time, and in the order there were submitted.
The maximum number of workers can be defined to a higher value than the number of CPUs as workers will be started only when solicited and will be released when unused after an idle time.
Nevertheless, the actual number of workers will be limited by the operating system to a lower value than nb_workers.
The third argument is either :
TP_RUN_ALL_TASKS: Run all submitted tasks (usual expected standard behaviour).TP_RUN_ALL_SUCCESSFUL_TASKS: Run submitted tasks until at least one fails (returnsTP_JOB_FAILURE). Cancel automatically other (already or to be) submitted tasks.TP_RUN_ONE_SUCCESSFUL_TASK: Run submitted tasks until at least one succeeds (returnsTP_JOB_SUCCESS). Cancel automatically other (already or to be) submitted tasks.
- The second argument
global_data, if not null, is a pointer to global data. This pointer can next be retrieved by tasks with the functionthreadpool_global_data ().
The current thread pool can further be retrieved in a working context,
inside user-defined functions work and job_delete (as passed to threadpool_add_task) and work (as passed to threadpool_task_continuation),
with:
struct threadpool *threadpool_current (void)size_t threadpool_nb_workers (struct threadpool *threadpool)Each worker created by the thread pool is given a unique and constant number, that can be retrieved and used in a working context (mostly for tracing purposes).
inside user-defined functions work and job_delete (as passed to threadpool_add_task) and work (as passed to threadpool_task_continuation),
with:
size_t threadpool_current_worker_no (void)This number is increment by one for every new created worker.
tp_task_t threadpool_add_task (struct threadpool *threadpool,
tp_result_t (*work) (void *job),
void *job,
void (*job_delete) (void *job, tp_result_t result))A task is submitted to the thread pool with a call to threadpool_add_task ().
- The first argument
threadpoolis a thread pool returned by a previous call tothreadpool_create_and_start (). - The second argument
workis a user defined function to be executed by a worker of the thread pool onjob. This functionworkshould returnTP_JOB_SUCCESSon success,TP_JOB_FAILUREotherwise.workshould be made thread-safe as severalworkwill be running in parallel (that's the whole point of the thread pool). This function receives the jobjobas argument, as it was passed tothreadpool_add_task. If needed,workcan itself (multi-thread-safely) callthreadpool_add_task(on the current thread pool retrieved withthreadpool_current). - The third argument
jobis a pointer to the data to be used by the task and that will be processed bywork. It should be fully initialised before it is passed tothreadpool_add_task. It can be allocated automatically, statically or dynamically, as long it survives until the job is done bywork.
The function threadpool_add_task returns a unique id of the submitted task, or 0 on error (with errno set to ENOMEM).
- The fourth argument
job_delete, if not null, is a user defined function called at termination of the task (after processing or cancellation.) This function receives thejobof the task and the result ofworkas arguments.
job_delete should be used if the job was allocated dynamically in order to release and destroy the data after work is done and avoid memory leaks.
job_delete is called in a multi-thread-safe manner and can therefore safely aggregate results to those of previous tasks for instance
(in a map and reduce pattern for instance). See below.
- A handler is provided for convenience.
void threadpool_job_free_handler (void *job, tp_result_t result);It calls free (job), whatever the value of result.
Therefore, if job was allocated with a single malloc () (or affiliated functions), threadpool_job_free_handler is a possible choice for job_delete.
job_deletecould as well be called manually (rather than passed as an argument tothreadpool_add_task) at the very end ofwork (), but it then would not be executed multi-thread-safely, forbidding any aggregation.
If a part of a task needs to be synchronised,
threadpool_guard_begin ()andthreadpool_guard_end ()could be used to guard some sections of the task. Nevertheless, these functions SHOULD generally NOT BE USED. Calls tojob_deleteare synchronised and should respond to ususal cases.
Besides simply releasing data job, the user-defined job_delete can also be used as a callback function, called after the task is done or cancelled, to retrieve the used and possibly modified job and take actions (displaying, computing, aggregating, ...).
For instance, a user-defined type job_t could be declared as a structure containing:
typedef struct {
struct { ... } input;
struct { ... } result;
} job_t;A job of type job_t would then be passed to threadpool_add_task.
The task post-processing pattern stands in 4 steps :
- A
job, of typejob_t, is allocated and initialised:inputdata is set ;
- The
jobis passed is passed tothreadpool_add_task. - The
jobis processed in the user-defined functionwork:- the previously set
inputdata is used to compute theresultdata of the task ;
- the previously set
- In the user-defined function
job_delete:- if the result of
workisTP_JOB_SUCCESS, theresultcan multi-thread-safely update an aggregated result of all the tasks ;global_data, passed tothreadpool_create_and_startand retrievable bythreadpool_global_data, is a possible choice to hold the aggregated result ; - any required deallocation of
jobis done.
- if the result of
See below fuzzy words and map, filter, reduce for examples of such a pattern.
Global and local data of threads can be retrieved and updated safely in the context of working threads.
The global data of a thread pool (global_data as passed to threadpool_create_and_start):
- should be fully initialised before
threadpool_create_and_startis called ; - should survive after
threadpool_wait_and_destroyis called ; - can be accessed inside user-defined functions
make_local,delete_local(as passed tothreadpool_create_and_start),workandjob_delete(as passed tothreadpool_add_task),work(as passed tothreadpool_task_continuation) anddeallocator(as passed tothreadpool_set_resource_manager) with :
void *threadpool_global_data (void)
threadpool_global_datacan not be called in the resourceallocatorset bythreadpool_set_resource_manager.
The local data of a worker (created and returned by make_local and destroyed by delete_local, as passed to threadpool_set_worker_local_data_manager) can be accessed inside user-defined functions work and job_delete (as passed to threadpool_add_task) and work (as passed to threadpool_task_continuation) with :
void threadpool_set_worker_local_data_manager (struct threadpool *threadpool, void *(*make_local) (void), void (*delete_local) (void *local_data))void *threadpool_worker_local_data (void)These two functions must be called in the context of a running thread, otherwise they return 0.
size_t threadpool_cancel_task (struct threadpool *threadpool, tp_task_t task_id)Previously submitted and still pending tasks can be cancelled.
task_id is :
- either a unique id returned by a previous call to
threadpool_add_task; - or
TP_CANCEL_ALL_PENDING_TASKSto cancel all still pending tasks ; - or
TP_CANCEL_NEXT_PENDING_TASKto cancel the next still pending submitted task (it can be used several times in a row) ; - or
TP_CANCEL_LAST_PENDING_TASKto cancel the last still pending submitted task (it can be used several times in a row).
Canceled tasks won't be processed, but job_delete, as optionally passed to threadpool_add_task, will be called though
(TP_JOB_CANCELED will be passed to job_delete).
The function returns the number of cancelled tasks, if any, or 0 if there are not any left pending task to be cancelled.
void threadpool_wait_and_destroy (struct threadpool *threadpool)The single argument threadpool is a thread pool returned by a previous call to threadpool_create_and_start ().
This function declares that all the tasks have been submitted. It then waits for all the tasks to be completed by workers.
threadpool should not be used after a call to threadpool_wait_and_destroy ().
A monitoring of the thread pool activity can optionally be activated by calling
void threadpool_set_monitor (struct threadpool *threadpool, threadpool_monitor_handler handler, void *arg, threadpool_monitor_filter filter)A user-defined handler function is passed as second argument, with signature
void (*threadpool_monitor_handler) (struct threadpool_monitor, void *arg)0 or a user-defined filter function can be passed as fourth argument, with signature
int (*threadpool_monitor_filter) (struct threadpool_monitor d)The handler will be called to retrieve and display information about the activity of the thread pool.
It :
- will be called whenever the state of the thread pool changes and, if
filteris not null, wheneverfilterreturns non-zero.filtershould be set to 0 to monitor every change of the thread pool state. - will be passed the argument
arg(which can be0) previously passed tothreadpool_set_monitoras third argument, - will be called asynchronously, without interfering with the execution of workers (actually, a sequential dedicated asynchronous thread pool is used for monitoring),
- will be executed multi-thread-safely,
- should not be called after
threadpool_wait_and_destroyhas been called.
The monitoring data are passed to the handler function in a structure threadpool_monitor which contains:
struct threadpool *threadpool: the thread pool for which the monitoring handler is called ;double time: the elapsed seconds since the creation of the thread pool (threadpool_create_and_start) ;int closed: 1 if the thread pool has been closed (by a call tothreadpool_wait_and_destroy), 0 otherwise ;size_t workers.nb_requested: the requested number of workers, as defined at the creation of the thread pool ;size_t workers.nb_max: the maximum number of workers granted by the operating system (<=workers.nb_requested) ;size_t workers.nb_alive: the number of alive workers, either running (tasks.nb_processing) or waiting (workers.nb_idle) ;size_t workers.nb_idle: the number of idle worker, i.e. waiting (some time) for a task to process ;size_t tasks.nb_pending: the number of tasks submitted to the thread pool and not yet processed or being processed ;size_t tasks.nb_processing: the number of running workers, i.e. processing a task ;size_t tasks.nb_asynchronous: the number of asynchronous (virtual) tasks ;size_t tasks.nb_succeeded: the number of already processed and succeeded tasks by the thread pool (a task is considered successful whenwork, the function passed tothreadpool_add_task, returns 0) ;size_t tasks.nb_failed: the number of already processed and failed tasks by the thread pool (a task is considered failed whenwork, the function passed tothreadpool_add_task, does not return 0) ;size_t tasks.nb_canceled: the number of cancelled tasks ;size_t tasks.nb_submitted: the number of submitted tasks (either pending, processing, succeeded, failed or cancelled).
A handler threadpool_monitor_to_terminal is available for convenience:
- It can be used as the second argument of
threadpool_set_monitor. - It displays monitoring data as text sent to a stream of type
FILE *, passed as the third argument ofthreadpool_set_monitor.stderrwill be used by default if this third argument isNULL.
A filter threadpool_monitor_every_100ms is available for convenience:
- It can be used as the fourth argument of
threadpool_set_monitor. - It permits to monitor not more often than every 100 ms.
Even though the handler is automatically called when relevant, it can be called manually with threadpool_monitor.
Data used in the context of a thread pool can be managed globally or locally with four different ways, depending on the scope and life-cycle of the data.
| Scope | Access | Management |
|---|---|---|
| Thread pool | threadpool_global_data |
threadpool_create_and_start, see Manage global data |
| Active thread pool (running workers) | threadpool_global_resource |
threadpool_set_global_resource_manager, see Manage global resources |
| Worker | threadpool_worker_local_data |
threadpool_set_worker_local_data_manager, see Manage worker local data |
| Task | job in work |
threadpool_add_task and threadpool_task_continuation, see Manage task data |
A global context data of a thread pool can be passed as second argument when it is created with threadpool_create_and_start.
This data should be allocated (statically, automatically or dynamically) before calling threadpool_create_and_start and respectively deallocated after calling threadpool_wait_and_destroy.
This global data of a thread pool can be accessed inside:
- user-defined functions
allocatoranddeallocatorpassed tothreadpool_set_global_resource_manager; - user-defined functions
make_localanddelete_local(as passed tothreadpool_set_worker_local_data_manager) ; - user-defined functions
workandjob_delete(as passed tothreadpool_add_task) ; - user-defined functions
work(as passed tothreadpool_task_continuation) ;
with :
void *threadpool_global_data (void)In case external resources should be allocated for tasks processing (for instance a shared connection to a database), user-defined functions allocator and deallocator can be set with:
void threadpool_set_global_resource_manager (struct threadpool *threadpool, void *(*allocator) (void *global_data), void (*deallocator) (void *resource))This function should be called after
threadpool_create_and_startand before adding tasks to the thread pool withthreadpool_add_task. Otherwise, it would have no effect (as workers are already running) anderrnowould be set toECANCELED.
- The user-defined function
allocator:- should fully initialise and return the global resource of the thread pool ;
- is passed the
global_dataof the thread pool as an argument ; - will generally be called once per thread pool, before processing the very first task.
- The user-defined function
deallocator:- should fully release the global resource of the thread pool ;
- is passed the resource to deallocate, as previously returned by
allocator, as an argument ; - will generally be called once per thread pool, after all tasks have been processed or cancelled.
Moreover, if the thread pool remains idle (waiting for tasks to process) for too long (see below), resources will be deallocated automatically, and will be reallocated automatically when the thread pool gets alive again.
The global resource of a thread pool (as returned by the allocator passed to threadpool_set_global_resource_manager) can be accessed inside user-defined functions make_local, delete_local (as passed to threadpool_set_worker_local_data_manager), work and job_delete (as passed to threadpool_add_task),
work (as passed to threadpool_task_continuation) with :
void *threadpool_global_resource (void)The timeout delay after which thread pool internal and external resources are deallocated when a thread pool has been kept idle for too long can be modified with:
void threadpool_set_idle_timeout (struct threadpool *threadpool, double delay)delay, in seconds, should be a non negative value, otherwise it is ignored and errno is set to EINVAL.
It can not exceed 10,000,000 seconds.
This delay would be set to a value larger than the time required to allocate global resources for tasks. It should nevertheless be kept as low as possible to release unused scarce resources when workers are kept idle for a long period.
The default delay is 0.1 seconds when a thread pool is created by threadpool_create_and_start.
The delay is set to 10,000,000 seconds by default after threadpool_set_global_resource_manager is called : resources will not be deallocated by default if the thread pool is idle.
threadpool_set_idle_timeout should be called after threadpool_set_global_resource_manager to lower the delay in order to deallocate scarce resources after a specified idle delay.
In case resources should be allocated for each worker (for instance a connection to a database), user-defined functions make_local and delete_local can be set with:
void threadpool_set_worker_local_data_manager (struct threadpool *threadpool, void *(*make_local) (void), void (*delete_local) (void *local_data));This function should be called after
threadpool_create_and_startand before adding tasks to the thread poll withthreadpool_add_task. Otherwise, it would have no effect (as workers are already running) anderrnowould be set toECANCELED.
make_local will be called when a worker is created and delete_local when it is terminated:
- The argument
make_local, if not null, is a user defined function that returns a pointer to data for a local usage by a worker. This pointer can next be retrieved by tasks with the functionthreadpool_worker_local_data ().make_localis called in a multi-thread-safe manner at the initialisation of a worker.make_localcan safely access (and update) the content ofglobal_data(withthreadpool_global_data) if needed. - The argument
delete_local, if not null, is a user defined function that is executed to release and destroys the local data used by each worker (passed as an argument) when a worker stops.delete_localis called in a multi-thread-safe manner at the termination of a worker.delete_localcan safely access (and update) the content ofglobal_data(withthreadpool_global_data) if needed (to gather results or statistics for instance).
The local data of a worker (as returned by the make_local passed to threadpool_set_worker_local_data_manager) can be accessed inside user-defined functions work and job_delete (as passed to threadpool_add_task) and work (as passed to threadpool_task_continuation) with :
void *threadpool_worker_local_data (void)The data of a job is passed to a task when it is added to a thread pool with threadpool_add_task.
It can be accessed inside user-defined functions work and job_delete (as passed to threadpool_add_task) and work (as passed to threadpool_task_continuation).
See Submit a task and below.
In case a task would need to use an asynchronous call, the continuation of the task work_continuator can be specified by calling
uint64_t threadpool_task_continuation (tp_result_t (*work_continuator) (void *data), double seconds)just before the asynchronous call.
The function work_continuator indicates the function to be called when the asynchronous call notifies its completion.
work_continuator should return TP_JOB_SUCCESS on success, TP_JOB_FAILURE otherwise.
threadpool_task_continuationreturns 0 and setserrnotoEINVALifwork_continuatoris null.threadpool_task_continuationreturns 0 and setserrnotoEPERMif it is called outside of:- a function
workof a task scheduled withthreadpool_add_task; - a function
work_continuatorof a task scheduled withthreadpool_task_continuation;
- a function
- otherwise
threadpool_task_continuationreturns a non-zero unique ID that should be passed to the asynchronous call.
seconds is a timeout delay, in seconds, for the asynchronous response. After this delay, the asynchronous response will be ignored (see below).
In the callback function of the asynchronous call (when the asynchronous call notifies its completion), the function
tp_result_t threadpool_task_continue (uint64_t uid)should be called with the ID previously retrieved to continue to process the initial task.
The function work_continuator previously declared by threadpool_task_continuation will then be automatically called:
- If
threadpool_task_continueis called before the timeout delaysecondshas elapsed,work_continuatorwill be scheduled by the thread pool andTP_JOB_SUCCESSwill be returned. - If
threadpool_task_continueis called after the timeout delaysecondshas elapsed,threadpool_task_continuewill have no effect, will returnTP_JOB_FAILUREanderrnowill be set toETIMEOUT.
The task does not block any worker between the calls to threadpool_task_continuation and threadpool_task_continue.
Workers are available to process any other tasks (either asynchronous or not): the tasks using threadpool_task_continuation and threadpool_task_continue behave like virtual tasks.
If needed, work_continuator can itself (multi-thread-safely) call threadpool_add_task (on the current thread pool retrieved with threadpool_current).
This features was inspired from Java virtual threads (see https://openjdk.org/jeps/444).
Type make to compile and run the examples (in sub-folder examples).
An example of the usage of thread pool is given in files qsip_wc.c and qsip_wc_test.c here.
It sorts 2 bunches of 50 lists of 1.000.000 numbers.
Two encapsulated thread pools are used : one to distribute 100 tasks over 7 monitored threads, each task sorting 1000000 numbers distributed over the CPU threads.
-
qsip_wc.cis an attempt to implement a parallelised version of the quick sort algorithm (using a thread pool);- It uses features such as global data, worker local data, dynamic creation and deletion of jobs.
- It reveals that a parallelised quick sort is inefficient due to thread management overhead.
-
qsip_wc_test.cis an example of a thread pool that sorts several arrays using the above parallelised version of the quick sort algorithm.- It uses features such as global data, worker local data, task cancellation, (fake) resource management and monitoring.
Run this example with
$ make qsip_wc_test
This example matches a list of french fuzzy words against the french dictionary.
Run it with:
$ make fuzzyword
Two encapsulated thread pools are used : one to distribute the list of words on one monitored single thread (words are processed sequentially), each word being compared to the entries (distributed over the CPU threads) of the dictionary.
It uses job_delete as a callback function for task post-processing and threadpool_set_global_resource_manager for global resource management.
This example requests more workers than what the system permits.
Run it with:
$ make intensive
This example uses threadpool_task_continuation and threadpool_task_continue to create asynchronous virtual tasks.
Asynchronous tasks (here a pause of one second) can be processed with a thread pool of one worker only: those tasks behave like virtual tasks which do not block the worker (awesome !)
Run it with:
$ make timers
This example shows how to implement a map, filter, reduce pattern with parallelisation.
Results of each job are aggregated in the worker local data and then in the thread pool global data.
Run it with:
$ make mfr
The API is implemented in C11 (file wqm.c) using the standard C thread library <threads.h>.
It is highly inspired from the great book "Programming with POSIX Threads" written by David R. Butenhof, 21st ed., 2008, Addison Wesley.
It has been heavily tested, but bugs are still possible. Please don't hesitate to report them to me.
It makes use of libmap and libtimer implemented in minimaps :
map.handmap.cdefine an unprecedented MT-safe implementation of a map library that can manage maps, sets, ordered and unordered lists that can do it all with a minimalist interface.timer.handtimer.cdefine a OS-independent (as compared to POSIXtimer_settime) timer.
Workers are started automatically when needed, that is when a new task is submitted whereas all workers are already booked. Workers are running in parallel, asynchronously. Each worker can process one task at a time. A task is processed as soon as a worker is available.
A worker stops when all submitted tasks have been processed or after an idle time. Idle workers are kept ready for new tasks for a short time and are then stopped automatically to release system resources.
For instance, say a task requires 2 seconds to be processed and the maximum idle delay for a worker is half a second:
- if the task is repeatedly submitted every 3 seconds to the thread pool, one worker will be created and activated on submission, and stopped after completion of the task and an idle time (of half a second).
- if the task is submitted every 2.4 seconds to the thread pool, one worker will be created and activated on submission, kept idle and reactivated for the next task.
- if the task is submitted every 2 seconds, one worker will be active to process the tasks.
- if the task is submitted every second, two workers will be active to process the tasks.
- if the rate of submitted tasks is very high, the maximum number of workers (as passed to
threadpool_create_and_start ()) will be active.
Therefore, the number for workers automatically adapts to the rate and duration for tasks.
Zed is dead, but C is not.
