taskpool: Add taskpool API, switch Stasis to using it.

This change introduces a new API called taskpool. This is a pool
of taskprocessors. It provides the following functionality:

1. Task pushing to a pool of taskprocessors
2. Synchronous tasks
3. Serializers for execution ordering of tasks
4. Growing/shrinking of number of taskprocessors in pool

This functionality already exists through the combination of
threadpool+taskprocessors but through investigating I determined
that this carries substantial overhead for short to medium duration
tasks. The threadpool uses a single queue of work, and for management
of threads it involves additional tasks.

I wrote taskpool to eliminate the extra overhead and management
as much as possible. Instead of a single queue of work each
taskprocessor has its own queue and at push time a selector chooses
the taskprocessor to queue the task to. Each taskprocessor also
has its own thread like normal. This spreads out the tasks immediately
and reduces contention on shared resources.

Using the included efficiency tests the number of tasks that can be
executed per second in a taskpool is 6-12 times more than an equivalent
threadpool+taskprocessor setup.

Stasis has been moved over to using this new API as it is a heavy consumer
of threadpool+taskprocessors and produces a lot of tasks.

UpgradeNote: The threadpool_* options in stasis.conf have now been deprecated
though they continue to be read and used. They have been replaced with taskpool
options that give greater control over the underlying taskpool used for stasis.

DeveloperNote: The taskpool API has been added for common usage of a
pool of taskprocessors. It is suggested to use this API instead of the
threadpool+taskprocessor approach.
This commit is contained in:
Joshua C. Colp
2025-08-06 13:19:20 -03:00
committed by github-actions[bot]
parent 390d8f98c2
commit e9ee9d7d98
16 changed files with 2691 additions and 175 deletions

View File

@@ -48,6 +48,7 @@ int ast_named_locks_init(void); /*!< Provided by named_locks.c */
int ast_file_init(void); /*!< Provided by file.c */
void ast_autoservice_init(void); /*!< Provided by autoservice.c */
int ast_tps_init(void); /*!< Provided by taskprocessor.c */
int ast_taskpool_init(void); /*!< Provided by taskpool.c */
int ast_timing_init(void); /*!< Provided by timing.c */
void ast_stun_init(void); /*!< Provided by stun.c */
int ast_ssl_init(void); /*!< Provided by ssl.c */

View File

@@ -20,6 +20,7 @@
#define _AST_SERIALIZER_H
struct ast_threadpool;
struct ast_taskpool;
/*!
* Maintains a named pool of thread pooled taskprocessors. Also if configured
@@ -56,6 +57,26 @@ int ast_serializer_pool_destroy(struct ast_serializer_pool *pool);
struct ast_serializer_pool *ast_serializer_pool_create(const char *name,
unsigned int size, struct ast_threadpool *threadpool, int timeout);
/*!
* \brief Create a serializer pool on taskpool.
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* Create a serializer pool with an optional shutdown group. If a timeout greater
* than -1 is specified then a shutdown group is enabled on the pool.
*
* \param name The base name for the pool, and used when building taskprocessor(s)
* \param size The size of the pool
* \param taskpool The backing taskpool to use
* \param timeout The timeout used if using a shutdown group (-1 = disabled)
*
* \return A newly allocated serializer pool object
* \retval NULL on error
*/
struct ast_serializer_pool *ast_serializer_taskpool_create(const char *name,
unsigned int size, struct ast_taskpool *taskpool, int timeout);
/*!
* \brief Retrieve the base name of the serializer pool.
*

View File

@@ -0,0 +1,65 @@
/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2012-2013, Digium, Inc.
*
* Mark Michelson <mmmichelson@digium.com>
*
* See http://www.asterisk.org for more information about
* the Asterisk project. Please do not directly contact
* any of the maintainers of this project for assistance;
* the project provides a web site, mailing lists and IRC
* channels for your use.
*
* This program is free software, distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
#ifndef _ASTERISK_SERIALIZER_SHUTDOWN_GROUP_H
#define _ASTERISK_SERIALIZER_SHUTDOWN_GROUP_H
struct ast_serializer_shutdown_group;
/*!
* \brief Create a serializer group shutdown control object.
* \since 13.5.0
*
* \return ao2 object to control shutdown of a serializer group.
*/
struct ast_serializer_shutdown_group *ast_serializer_shutdown_group_alloc(void);
/*!
* \brief Wait for the serializers in the group to shutdown with timeout.
* \since 13.5.0
*
* \param shutdown_group Group shutdown controller. (Returns 0 immediately if NULL)
* \param timeout Number of seconds to wait for the serializers in the group to shutdown.
* Zero if the timeout is disabled.
*
* \return Number of serializers that did not get shutdown within the timeout.
*/
int ast_serializer_shutdown_group_join(struct ast_serializer_shutdown_group *shutdown_group, int timeout);
/*!
* \brief Increment the number of serializer members in the group.
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* \param shutdown_group Group shutdown controller.
*/
void ast_serializer_shutdown_group_inc(struct ast_serializer_shutdown_group *shutdown_group);
/*!
* \brief Decrement the number of serializer members in the group.
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* \param shutdown_group Group shutdown controller.
*/
void ast_serializer_shutdown_group_dec(struct ast_serializer_shutdown_group *shutdown_group);
#endif /* ASTERISK_SERIALIZER_SHUTDOWN_GROUP_H */

321
include/asterisk/taskpool.h Normal file
View File

@@ -0,0 +1,321 @@
/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2025, Sangoma Technologies Corporation
*
* Joshua C. Colp <jcolp@sangoma.com>
*
* See http://www.asterisk.org for more information about
* the Asterisk project. Please do not directly contact
* any of the maintainers of this project for assistance;
* the project provides a web site, mailing lists and IRC
* channels for your use.
*
* This program is free software, distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
/*! \file
* \ref Taskpool
*
* \page Taskpool API providing queued task execution across threads.
The taskpool API is a specialized API for the queueing of tasks
in a synchronous or asynchronous manner, to be executed across
a pool of threads. For cases where serialization is needed a
serializer API is also provided ensuring that tasks queued to
the serializer are executed in a serialized fashion within the
taskpool.
On creation of a taskpool various options can be set and used to
control the operation of the pool. This includes how many taskprocessors
are present, whether the pool can grow, whether the pool can shrink,
and how long idle taskprocessors should exist before being terminated.
This provides flexibility based on the specific needs of the user of
the taskpool and the environment.
The queueing of tasks to the taskpool is done using a selector. The
selector examines the available taskprocessors and decides which one
to queue the task to. This operation can also examine the state of
the pool to see if it needs to grow and if enabled and possible does so.
The taskpool API is preferred for many cases over the use of the
threadpool due to the far lower overhead involved. Taskpools require
no additional thread or task queue for management of the pool itself and
the act of queueing tasks, the most common operation, is written to be as
simple and minimal as possible. Threadpools are best used for long
running tasks and operations.
*/
#ifndef _ASTERISK_TASKPOOL_H
#define _ASTERISK_TASKPOOL_H
struct ast_taskpool;
struct ast_taskprocessor;
struct ast_serializer_shutdown_group;
/*!
* \brief Selectors for choosing which taskprocessor in a pool to use
*/
enum ast_taskpool_selector {
AST_TASKPOOL_SELECTOR_DEFAULT = 0, /* The selector that is generally the best for most use cases */
AST_TASKPOOL_SELECTOR_LEAST_FULL, /* Select the least full taskprocessor */
AST_TASKPOOL_SELECTOR_SEQUENTIAL, /* Select taskprocessors in a sequential manner */
};
struct ast_taskpool_options {
#define AST_TASKPOOL_OPTIONS_VERSION 1
/*! Version of taskpool options in use */
int version;
/*!
* \brief The selector to use for choosing a taskprocessor
*/
enum ast_taskpool_selector selector;
/*!
* \brief Time limit in seconds for idle dynamic taskprocessors
*
* A time of 0 or less will mean no timeout.
*/
int idle_timeout;
/*!
* \brief Number of taskprocessors to increment the pool by
*/
int auto_increment;
/*!
* \brief Number of taskprocessors that will always exist
*
* Zero is a valid value if the taskpool will never have taskprocessors
* that always exist, allowing the pool to drop to zero if not used.
*/
int minimum_size;
/*!
* \brief Number of taskprocessors the pool will start with
*
* Zero is a valid value if the taskpool should start
* without any taskprocessors allocated.
*
* \note This must be equal to or greater than the minimum_size,
* otherwise the taskpool will adjust this to the minimum_size.
*/
int initial_size;
/*!
* \brief Maximum number of taskprocessors a pool may have
*
* When the taskpool's size increases, it can never increase
* beyond this number of taskprocessors.
*
* Zero is a valid value if the taskpool does not have a
* maximum size for taskprocessors.
*
* \note This must be equal to or greater than the initial_size,
* otherwise the taskpool will adjust this to the initial_size.
*/
int max_size;
/*!
* \brief The threshold for when to grow the pool
*
* This is the number of tasks that must be in queue before the pool will grow.
*
* \note If not specified a default of the 50% of the high water threshold defined
* in taskprocessor.h will be used.
*/
int growth_threshold;
/*!
* \brief Function to call when a taskprocessor starts
*
* This is useful if there is something common that all
* taskprocessors in a taskpool need to do when they start.
*/
void (*thread_start)(void);
/*!
* \brief Function to call when a taskprocessor ends
*
* This is useful if there is common cleanup to execute when
* a taskprocessor completes
*/
void (*thread_end)(void);
};
/*!
* \brief Create a new taskpool
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* This function creates a taskpool. Tasks may be pushed onto this task pool
* and will be automatically acted upon by taskprocessors within the pool.
*
* Only a single taskpool with a given name may exist. This function will fail
* if a taskpool with the given name already exists.
*
* \param name The unique name for the taskpool
* \param options The behavioral options for this taskpool
* \retval NULL Failed to create the taskpool
* \retval non-NULL The newly-created taskpool
*
* \note The \ref ast_taskpool_shutdown function must be called to shut down the
* taskpool and clean up underlying resources fully.
*/
struct ast_taskpool *ast_taskpool_create(const char *name,
const struct ast_taskpool_options *options);
/*!
* \brief Get the current number of taskprocessors in the taskpool
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* \param pool The taskpool to query
* \retval The number of taskprocessors in the taskpool
*/
size_t ast_taskpool_taskprocessors_count(struct ast_taskpool *pool);
/*!
* \brief Get the current number of queued tasks in the taskpool
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* \param pool The taskpool to query
* \retval The number of queued tasks in the taskpool
*/
long ast_taskpool_queue_size(struct ast_taskpool *pool);
/*!
* \brief Push a task to the taskpool
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* Tasks pushed into the taskpool will be automatically taken by
* one of the taskprocessors within
* \param pool The taskpool to add the task to
* \param task The task to add
* \param data The parameter for the task
* \retval 0 success
* \retval -1 failure
*/
int ast_taskpool_push(struct ast_taskpool *pool, int (*task)(void *data), void *data)
attribute_warn_unused_result;
/*!
* \brief Push a task to the taskpool, and wait for completion
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* Tasks pushed into the taskpool will be automatically taken by
* one of the taskprocessors within
* \param pool The taskpool to add the task to
* \param task The task to add
* \param data The parameter for the task
* \retval 0 success
* \retval -1 failure
*/
int ast_taskpool_push_wait(struct ast_taskpool *pool, int (*task)(void *data), void *data)
attribute_warn_unused_result;
/*!
* \brief Shut down a taskpool and remove the underlying taskprocessors
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* \param pool The pool to shut down
*
* \note This will decrement the reference to the pool
*/
void ast_taskpool_shutdown(struct ast_taskpool *pool);
/*!
* \brief Get the taskpool serializer currently associated with this thread.
*
* \note The returned pointer is valid while the serializer
* thread is running.
*
* \note Use ao2_ref() on serializer if you are going to keep it
* for another thread. To unref it you must then use
* ast_taskprocessor_unreference().
*
* \retval serializer on success.
* \retval NULL on error or no serializer associated with the thread.
*/
struct ast_taskprocessor *ast_taskpool_serializer_get_current(void);
/*!
* \brief Serialized execution of tasks within a \ref ast_taskpool.
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* A \ref ast_taskprocessor with the same contract as a default taskprocessor
* (tasks execute serially) except instead of executing out of a dedicated
* thread, execution occurs in a taskprocessor from a \ref ast_taskpool.
*
* While it guarantees that each task will complete before executing the next,
* there is no guarantee as to which thread from the \c pool individual tasks
* will execute. This normally only matters if your code relies on thread
* specific information, such as thread locals.
*
* Use ast_taskprocessor_unreference() to dispose of the returned \ref
* ast_taskprocessor.
*
* Only a single taskprocessor with a given name may exist. This function will fail
* if a taskprocessor with the given name already exists.
*
* \param name Name of the serializer. (must be unique)
* \param pool \ref ast_taskpool for execution.
*
* \return \ref ast_taskprocessor for enqueuing work.
* \retval NULL on error.
*/
struct ast_taskprocessor *ast_taskpool_serializer(const char *name, struct ast_taskpool *pool);
/*!
* \brief Serialized execution of tasks within a \ref ast_taskpool.
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* A \ref ast_taskprocessor with the same contract as a default taskprocessor
* (tasks execute serially) except instead of executing out of a dedicated
* thread, execution occurs in a taskprocessor from a \ref ast_taskpool.
*
* While it guarantees that each task will complete before executing the next,
* there is no guarantee as to which thread from the \c pool individual tasks
* will execute. This normally only matters if your code relies on thread
* specific information, such as thread locals.
*
* Use ast_taskprocessor_unreference() to dispose of the returned \ref
* ast_taskprocessor.
*
* Only a single taskprocessor with a given name may exist. This function will fail
* if a taskprocessor with the given name already exists.
*
* \param name Name of the serializer. (must be unique)
* \param pool \ref ast_taskpool for execution.
* \param shutdown_group Group shutdown controller. (NULL if no group association)
*
* \return \ref ast_taskprocessor for enqueuing work.
* \retval NULL on error.
*/
struct ast_taskprocessor *ast_taskpool_serializer_group(const char *name,
struct ast_taskpool *pool, struct ast_serializer_shutdown_group *shutdown_group);
/*!
* \brief Push a task to a serializer, and wait for completion
* \since 23.1.0
* \since 22.7.0
* \since 20.17.0
*
* \param serializer The serializer to add the task to
* \param task The task to add
* \param data The parameter for the task
* \retval 0 success
* \retval -1 failure
*/
int ast_taskpool_serializer_push_wait(struct ast_taskprocessor *serializer, int (*task)(void *data), void *data);
#endif /* ASTERISK_TASKPOOL_H */

View File

@@ -340,6 +340,11 @@ const char *ast_taskprocessor_name(struct ast_taskprocessor *tps);
*/
long ast_taskprocessor_size(struct ast_taskprocessor *tps);
/*!
* \brief Return the listener associated with the taskprocessor
*/
struct ast_taskprocessor_listener *ast_taskprocessor_listener(struct ast_taskprocessor *tps);
/*!
* \brief Get the current taskprocessor high water alert count.
* \since 13.10.0

View File

@@ -24,6 +24,8 @@ struct ast_threadpool;
struct ast_taskprocessor;
struct ast_threadpool_listener;
#include "asterisk/serializer_shutdown_group.h"
struct ast_threadpool_listener_callbacks {
/*!
* \brief Indicates that the state of threads in the pool has changed
@@ -196,28 +198,6 @@ int ast_threadpool_push(struct ast_threadpool *pool, int (*task)(void *data), vo
*/
void ast_threadpool_shutdown(struct ast_threadpool *pool);
struct ast_serializer_shutdown_group;
/*!
* \brief Create a serializer group shutdown control object.
* \since 13.5.0
*
* \return ao2 object to control shutdown of a serializer group.
*/
struct ast_serializer_shutdown_group *ast_serializer_shutdown_group_alloc(void);
/*!
* \brief Wait for the serializers in the group to shutdown with timeout.
* \since 13.5.0
*
* \param shutdown_group Group shutdown controller. (Returns 0 immediately if NULL)
* \param timeout Number of seconds to wait for the serializers in the group to shutdown.
* Zero if the timeout is disabled.
*
* \return Number of serializers that did not get shutdown within the timeout.
*/
int ast_serializer_shutdown_group_join(struct ast_serializer_shutdown_group *shutdown_group, int timeout);
/*!
* \brief Get the threadpool serializer currently associated with this thread.
* \since 14.0.0