Update documentation of new shared pointer interface.

This commit is contained in:
Patrick Pelissier committed 2024-11-30 10:25:15 +01:00
1 parent 50d4e15297
commit dd509f2b7e
1 file changed
+507 -4
+507 -4
View File
@@ -25,7 +25,7 @@ M\*LIB: Generic type-safe Container Library for C language
11. [Priority queue](#m-prioqueue)
12. [Fixed buffer queue](#m-buffer)
13. [Atomic Shared Register](#m-snapshot)
14. [Shared pointers](#m-shared)
14. [Shared pointers](#m-shared-ptr)
15. [Intrusive Shared Pointers](#m-i-shared)
16. [Intrusive list](#m-i-list)
17. [Concurrent adapter](#m-concurrent)
@@ -161,14 +161,12 @@ The available containers of M\*LIB for thread synchronization are in the followi
* [m-buffer.h](#m-buffer): header for creating fixed-size queue (or stack) of generic type (multiple producer / multiple consumer),
* [m-snapshot](#m-snapshot): header for creating 'atomic buffer' (through triple buffer) for sharing synchronously big data (thread safe),
* [m-shared.h](#m-shared): header for creating shared pointer of generic type,
* [m-concurrent.h](#m-concurrent): header for transforming a container into a concurrent container (thread safe),
* [m-shared-ptr.h](#m-shared-ptr): header for creating shared pointer of generic type,
* m-c-mempool.h: WIP header for creating fast concurrent memory allocation.
The following containers are intrusive (You need to modify your structure to add fields needed by the container) and are defined in:
* [m-i-list.h](#m-i-list): header for creating doubly-linked intrusive list of generic type,
* [m-i-shared.h](#m-i-shared): header for creating intrusive shared pointer of generic type (Thread Safe).
Other headers offering other functionality are:
@@ -191,6 +189,11 @@ Finally, headers for compatibility with non C11 compilers:
* [m-atomic.h](#m-atomic): header for ensuring compatibility between C's `stdatomic.h` and C++'s atomic header (provide also its own implementation if nothing is available),
* [m-thread.h](#m-thread): header for providing a very thin layer across multiple implementation of mutex/threads (C11/PTHREAD/WIN32).
The following headers are obsolete:
* [m-shared.h](#m-shared): header for creating shared pointer of generic type,
* [m-concurrent.h](#m-concurrent): header for transforming a container into a concurrent container (thread safe),
* [m-i-shared.h](#m-i-shared): header for creating intrusive shared pointer of generic type (Thread Safe).
Each containers define their iterators (if it is meaningful).
All containers try to expose the same common interface:
@@ -4432,10 +4435,504 @@ any new call to `name_read_start`.
End the reservation of the data `old`.
This function is thread-safe and performs atomic operation on the snapshot.
_________________
### M-SHARED-PTR
This header is for creating shared pointer.
A [shared pointer](https://en.wikipedia.org/wiki/Smart_pointer)
is a smart pointer that retains shared ownership of an object.
Several shared pointers may own the same object, sharing ownership of this object.
It is a mechanism to keep tracks of all registered users of an object
and performs an automatic destruction of the object only when all users release
their need on this object. As shared pointer owns no object if it is a null constant pointer.
In addition, M\*LIB shared pointer provides methods that protects concurrent access of the object by different threads,
transforming a standard container (`LIST`, `ARRAY`, `DICT`, `DEQUE`, ...)
into an equivalent container but compatible with concurrent access by different threads.
In practice, it puts a mutex lock to access the container.
As such it is quite generic. However, it is less efficient than containers
specially tuned for multiple threads.
Finally, M\*LIB shared pointer provides abstraction between declaration and definition,
enabling to decouple then.
There are two kinds of shared pointer:
* weak shared pointer (don't support thread concurrency)
* shared pointer (support for multiple threads both for the counter and lock for the data)
with 3 flavors:
* `_DECL` : for header files, declare only the functions and opaque type.
* `_DEF_EXTERN` : for source files, define the functions (to be used with DECL)
* `_DEF` : for header/source files, define the function as static inline.
You can provide or not the name of the shared pointer (`_AS`). Therefore we got 12 macros to generate the shared pointers.
There are also two oplists for this object, depending on how you want to handle it:
* one to handle the shared pointer itself (ie. the copy method creates a new owner of the same shared data),
* one to handle the data behind the shared pointer (ie. the copy method create a new shared data)
There are designed to work with buffers created with policy `BUFFER_PUSH_INIT_POP_MOVE`
so that we can send a shared pointer across multiple threads, and destroying the object
only when all threads have terminated their job.
Two level of API are created:
* the public one, created by `_DECL` generation macro to be used by everyone,
* the private one, created by `_DEF` and `_DEF_EXTERN` generation macro to be used by the implementation only.
The only mandatory operator is `CLEAR`.
#### `SHARED_PTR_DECL(name, oplist)`
#### `SHARED_PTR_DECL_AS(name, name_type, oplist)`
#### `SHARED_WEAK_PTR_DECL(name, oplist)`
#### `SHARED_WEAK_PTR_DECL_AS(name, name_type, oplist)`
Declare the shared pointer `name_t *` as a C pointer to an opaque structure and
declare also the associated public methods.
`name` shall be a C identifier that will be used to identify the shared pointer.
It will be used to create the type and functions to handle the container.
This declaration shall be done once per name and per compilation unit.
A corresponding definition `SHARED_PTR_DEF_EXTERN`
(resp. `SHARED_PTR_DEF_EXTERN_AS`, `SHARED_WEAK_PTR_DEF_EXTERN` and `SHARED_WEAK_PTR_DEF_EXTERN_AS`)
shall be done once in **one** compilation unit.
`SHARED_PTR_DECL` and `SHARED_PTR_DECL_AS` create a shared pointer qualified as _strong_ since the tracking of ownership is atomic and the destruction of the object is thread safe. In addition, there is also a lock protecting the integrity of the data.
They shall be used to track ownership of an object in multi thread program.
`SHARED_WEAK_PTR_DECL` and `SHARED_WEAK_PTR_DECL_AS` create a shared pointer qualified as _weak_ since the tracking of ownership is non atomic and the destruction of the object is not thread safe. There is also no lock protecting the integrity of the data.
They shall be used to track ownership of an object in a single thread program.
The given `oplist` is **mandatory** but it might be a simplified oplist (even if a real oplist works).
This parameter is only used for two reasons:
* to identify if a method of a shared pointer exists: for example, the method `name_new()` is created only if the `INIT` operator is provided.
* to provides the sub-types of the type: the key type, the value type, the subtype, the emplace types, the oplist of the subtype, the emplace types of the subtype, ...
As such, a simplified oplist like `(INIT(1), INIT_SET(1))` is valid for the _declaration_ only.
Theses macros should be put typically in header files.
`SHARED_PTR_DECL_AS` (resp. `SHARED_WEAK_PTR_DECL_AS`) is the same as `SHARED_PTR_DECL` (resp. `SHARED_WEAK_PTR_DECL`)
except the type of the shared pointer `name_type` is given as parameter instead of defined as `name_t`.
#### `SHARED_PTR_DEF_EXTERN(name, type[, oplist])`
#### `SHARED_PTR_DEF_AS_EXTERN(name, name_type, type[, oplist])`
#### `SHARED_WEAK_PTR_DEF_EXTERN(name, type[, oplist])`
#### `SHARED_WEAK_PTR_DEF_AS_EXTERN(name, name_type, type[, oplist])`
Define the shared pointer `name_t *` as a C pointer to an opaque structure that encapsulate the access to the C type
`type` which oplist is `oplist` and the associated public and private methods as external linkage.
`oplist` parameter is optional. If not present, it will look for a globaly registered oplist.
It shall match with the `oplist`argument given to the corresponding `_DECL`macro with the same `name`.
It shall be done once in **one** compilation unit.
Theses macros should be put typically in one source file.
See `SHARED_PTR_DECL` for additional information.
#### `SHARED_PTR_DEF(name, type[, oplist])`
#### `SHARED_PTR_DEF_AS(name, name_type, type[, oplist])`
#### `SHARED_WEAK_PTR_DEF(name, type[, oplist])`
#### `SHARED_WEAK_PTR_DEF_AS(name, name_type, type[, oplist])`
Define the shared pointer `name_t *` as a C pointer to an opaque structure that encapsulate the access to the C type
`type` which oplist is `oplist` and the associated public and private methods as static inline.
`oplist` parameter is optional. If not present, it will look for a globaly registered oplist.
This definition shall be done once per name and per compilation unit.
See `SHARED_PTR_DECL` for additional information.
#### `SHARED_PTR_OPLIST(name, oplist)`
Define the oplist of the shared pointer `name` view as a pointer (i.e. share ownership).
The parameter `oplist` can be a simplified oplist of the encapsulated type (See `_DECL` macros).
#### `SHARED_DATA_OPLIST(name, oplist)`
Define the oplist of the shared pointer `name` view as an object (i.e. copy object).
The parameter `oplist` can be a simplified oplist of the encapsulated type (See `_DECL` macros).
Example:
```C
#include "m-shared-ptr.h"
#include "m-buffer.h"
// To put in one header
// Define a shared data that supports pushing/popping integer
#define OPL (INIT(1),INIT_SET(1),SET(1),CLEAR(1),SUBTYPE(int),PUSH(1),POP(1))
SHARED_PTR_DECL(shared_data, OPL)
// Define a communication buffer of this shared data that supports sending/receiving the shared pointer
#define OPL_COMM (INIT(1),INIT_SET(1),SET(1),CLEAR(1),SUBTYPE(shared_data_t*),PUSH(1),POP(1))
SHARED_PTR_DECL(comm_buffer, OPL_COMM)
extern comm_buffer_t *comm1, *comm2;
// A C file using the header
void f(void) {
// Create a new shared data
shared_data_t *p = shared_data_new();
// Push some data in it
shared_data_push(p, 23);
shared_data_push(p, 32);
// And send it through 2 channels of communication
comm_buffer_push(comm1, shared_data_acquire(p));
comm_buffer_push(comm2, shared_data_acquire(p));
// Release our ownership of the data
shared_data_clear(p);
}
// To put in one C file
#include "m-array.h"
// Define the shared_data as an encapsulation of an array of int
ARRAY_DEF(array, int)
SHARED_PTR_DEF_EXTERN(shared_data, array_t, ARRAY_OPLIST(array, M_BASIC_OPLIST))
// Define the communication buffer as an encapsulation of a buffer
BUFFER_DEF(buffer, shared_data_t *, 10, BUFFER_QUEUE|BUFFER_PUSH_INIT_POP_MOVE, SHARED_PTR_OPLIST(shared_data, OPL))
SHARED_PTR_DEF_EXTERN(comm_buffer, buffer_t, BUFFER_OPLIST(buffer, SHARED_PTR_OPLIST(shared_data, OPL)))
comm_buffer_t *comm1, *comm2;
```
#### Created types
The following types are automatically defined by the previous definition macro if not provided by the user:
##### `name_t`
Opaque structure of the shared object, for which `name_t *`is a shared pointer.
#### Public interface
##### `name_t *name_new(void)`
Create a new shared object initialized with its `INIT` operator and return a shared pointer to it.
This method is created only if the `INIT` operator is defined.
##### `name_t *name_clone(const name_t *src)`
Create a new shared object initialized with the content of `*src`, creating effectively a clone.
This method is created only if the `INIT_SET` operator is defined.
##### `name_t *name_make[<emplace_suffix>](<emplace_args>)`
Create a new shared object initialized with the content of `<emplace_args>`.
This method is created only if the `EMPLACE_TYPE` operator is defined.
##### `void name_copy(name_t *out, const name_t *src)`
Copy into the shared object `*out` the content of `*src`, creating effectively a copy if the shared pointers are not the same.
This method is created only if the `SET` operator is defined.
##### `name_t *name_acquire(name_t *out)`
Acquire a new ownership of the pointer, returning the same pointer but with one more registered owner.
##### `void name_release(name_t *out)`
Release the ownership of the pointer.
If there is no longer any owner of the shared data, it is destroyed using its `CLEAR` method and the allocated memory freed.
##### `void name_clear(name_t *out)`
Alias of `name_release`
##### `void name_set(name_t **dst, name_t *out)`
Release the current ownership of `*dst` and set `*dst` as a new owner of `*out`
(Copy of pointer)
##### `void name_swap(name_t *a, name_t *b)`
Swap the content of the shared objects `*a` and `*b`
This method is created only if the `SWAP` operator is defined.
##### `void name_reset(name_t *a)`
Reset the content of the shared object `*a`.
This method is created only if the `RESET` operator is defined.
##### `bool name_empty_p(const name_t *a)`
Test if the shared object `*a` is empty (return true) or not (return false).
This method is created only if the `EMPTY_P` operator is defined.
##### `bool name_full_p(const name_t *a)`
Test if the shared object `*a` is full (return true) or not (return false).
This method is created only if the `FULL_P` operator is defined.
##### `size_t name_size(const name_t *a)`
Return the number of elements of the shared object `*a`
This method is created only if the `GET_SIZE` operator is defined.
##### `bool name_equal_p(const name_t *a, const name_t *a)`
Test if the shared objects `*a` and `*b` are equal (return true) or not (return false).
This method is created only if the `EQUAL` operator is defined.
##### `int name_cmp(const name_t *a, const name_t *b)`
Compare the order of the shared objects `*a` and `*b`, returning their relative order.
This method is created only if the `CMP` operator is defined.
##### `size_t name_hash(const name_t *)`
Return the hash of the shared object `*a`.
This method is created only if the `HASH` operator is defined.
##### `void name_add(name_t *a, const name_t *b, const name_t *c)`
Compute in `*a` the sum of `*b`and `*c`
This method is created only if the `ADD` operator is defined.
##### `void name_sub(name_t *a, const name_t *b, const name_t *c)`
Compute in `*a` the difference of `*b`and `*c`
This method is created only if the `SUB` operator is defined.
##### `void name_mul(name_t *a, const name_t *b, const name_t *c)`
Compute in `*a` the product of `*b`and `*c`
This method is created only if the `MUL` operator is defined.
##### `void name_div(name_t *a, const name_t *b, const name_t *c)`
Compute in `*a` the dividend of `*b`and `*c`
This method is created only if the `DIV` operator is defined.
##### `void name_splice(name_t *a, name_t *b)`
Append in `*a` all elements of `*b` and reset `*b`.
This method is created only if the `SPLICE` operator is defined.
##### `bool name_get(value_type *val, const name_t *a, key_type const key)`
Set `*val` to the value associated to the key `key` in the shared object `*a`
Return true in this case. If there is no association for `key`, return false
if the called `GET_KEY` operator supports returning NULL for the encapsulated container.
This method is created only if the `GET_KEY` operator is defined.
##### `void name_safe_get(value_type *val, name_t *a, key_type const key)`
Set `*val` to the value associated to the key `key` in the shared object `*a`,
creating a new entry for `key` if needed.
This method is created only if the `GET_SAFE_KEY` operator is defined.
##### `void name_set_at(name_t *a, key_type const key, value_type const val)`
Set the association of `key` to `val` in `*a`
This method is created only if the `SET_KEY` operator is defined.
##### `bool name_erase(name_t *a, key_type const key)`
Erase the association of `key` in `*a` if it exists (return true in this case).
Otherwise return false.
This method is created only if the `ERASE_KEY` operator is defined.
##### `void name_push(name_t *a, sub_type const el)`
Push in `*a` the element `el`, waiting forever for the container to be not full if needed.
This method is created only if the `PUSH` operator is defined.
##### `void name_push_move(name_t *a, sub_type *el)`
Push in `*a` the element `*el`, stealing as much resource from it as possible, waiting forever for the container to be not full if needed.
Afterwards `*el` is cleared.
This method is created only if the `PUSH_MOVE` operator is defined.
##### `void name_emplace<emplace_suffix>(name_t* a[, <emplace_args> args])`
Try to push in `*a` the element constructed from the arguments `args` if possible, waiting forever for the container to be not full if needed.
This method is created only if the `PUSH_MOVE` operator and the `EMPLACE_TYPE` of the sub_type (within the sub-oplist operator `OPLIST`) are defined.
##### `bool name_try_push(name_t *a, sub_type const el)`
Try to push in `*a` the element `el`,if it is not full (return true in this case).
Return false otherwise (cannot push element)
This method is created only if the `PUSH` operator is defined.
##### `bool name_try_push_move(name_t *a, sub_type *el)`
Try to push in `*a` the element `*el`,if the container is not full and clear `*el` (return true in this case).
Return false otherwise (cannot push element) and `*el` is still initializated.
This method is created only if the `PUSH_MOVE` operator is defined.
##### `bool name_try_emplace<emplace_suffix>(name_t *a[, <emplace_args> args])`
Try to push in `*a` the element constructed from the arguments `args`, if the container is not full (return true in this case).
Return false otherwise (cannot push element).
This method is created only if the `PUSH_MOVE` operator and the `EMPLACE_TYPE` of the sub_type (within the sub `OPLIST`) are defined.
##### `void name_pop(sub_type *const el, name_t *a)`
Pop the element from `*a` and set `*el` with it, waiting forever for an element to be available.
This method is created only if the `POP` operator is defined.
##### `void name_pop_move(sub_type *el, name_t *a)`
Pop the element from `*a` and initialize `*el` with it, stealing as much resource as possible, waiting forever for an element to be available.
This method is created only if the `POP_MOVE` operator is defined.
##### `bool name_try_pop(sub_type*, name_t *)`
Pop the element from `*a` and set `*el` with it if an element to be available (return true in this case).
Otherwise return false.
This method is created only if the `POP` operator is defined.
##### `bool name_try_pop_move(sub_type *, name_t *)`
Pop the element from `*a` and initialize `*el` with it, stealing as much resource as possible, if an element is available (return true in this case).
Otherwise return false.
This method is created only if the `POP_MOVE` operator is defined.
##### `int name_apply(name_t *a, int (*callback(void *data, sub_type*), void *data)`
Apply the callback `callback` to all elements of the container `*a` from front to back.
The callback may modify the given element if possible.
`data` is a user parameter given to the callback at user convenience.
If the callback returns a non null argument, the function stops and returns immediately with this error code.
This method is created only if the `IT_FIRST`, `IT_NEXT` and `IT_REF` operators are defined.
##### `int name_for_each(const name_t *a, int (*callback)(void *data, const sub_type*), void *data)`
Apply the callback `callback` to all elements of the container `*a` from front to back.
The callback shall not modify the given element.
`data` is a user parameter given to the callback at user convenience.
If the callback returns a non null argument, the function stops and returns immediately with this error code.
This method is created only if the `IT_FIRST`, `IT_NEXT` and `IT_CREF` operators are defined.
##### `int name_r_apply(name_t *, int (*callback(void *data, sub_type*), void*data)`
Apply the callback `callback` to all elements of the container `*a` from back to front.
The callback may modify the given element if possible.
`data` is a user parameter given to the callback at user convenience.
If the callback returns a non null argument, the function stops and returns immediately with this error code.
This method is created only if the `IT_LAST`, `IT_PREVIOUS` and `IT_REF` operators are defined.
##### `int name_r_for_each(const name_t *, int (*callback)(void *data, const sub_type*), void*data)`
Apply the callback `callback` to all elements of the container `*a` from back to front.
The callback shall not modify the given element.
`data` is a user parameter given to the callback at user convenience.
If the callback returns a non null argument, the function stops and returns immediately with this error code.
This method is created only if the `IT_LAST`, `IT_PREVIOUS` and `IT_CREF` operators are defined.
##### `void name_out_str(FILE *f, const name_t *a)`
Output `*a` into the FILE `*f`
This method is created only if the `OUT_STR` operator is defined.
##### `bool name_in_str(name_t *a, FILE *f)`
Read `*a` from the FILE `f`
This method is created only if the `IN_STR` operator is defined.
##### `void name_get_str(string_t str, const name_t *a, bool append)`
Output `*a` into the string `str`, appending it if `append` is true.
This method is created only if the `GET_STR` operator is defined.
##### `bool name_parse_str(name_t *a, const char *str, const char **endptr)`
Set `*a` to the value read from the string `str`.
`*endptr` is set to the end of the parsing in the string if `endptr` is not null.
This method is created only if the `PARSE_STR` operator is defined.
##### `m_serial_return_code_t name_out_serial(m_serial_write_t serial, const name_t *a)`
Output `*a` into the serial object `serial`.
This method is created only if the `OUT_SERIAL` operator is defined.
##### `m_serial_return_code_t name_in_serial(name_t *a, m_serial_read_t serial)`
Set `*a` to the value read from the serial object `serial`.
This method is created only if the `IN_SERIAL` operator is defined.
#### Private interface
The private interface is only availale for the implementation.
##### `void name_init_lock(name_t *out)`
This function shall be called from a custom constructor to initialize the part of the shared object not linked to the encapsulated type.
It initialize the internal locks and counters of the opaque structure of the object.
##### `void name_read_lock(const name_t *out)`
Enter the lock of the object for reading its state (The object shall hold no lock). This lock is non recursive.
An object shall not be at the same time in read lock and in write lock (it is exclusive).
##### `void name_read_wait(const name_t *out)`
Wait for the object to receive new data.
It shall be called within the read lock.
##### `void name_read_unlock(const name_t *out)`
Leave the lock of the object for reading its state.
It shall be called after `name_read_lock`
##### `void name_write_lock(name_t *out)`
Enter the lock of the object for writing its state (The object shall hold no lock). This lock is non recursive.
An object shall not be at the same time in read lock and in write lock (it is exclusive).
##### `void name_write_wait(name_t *out) `
Wait for the object to create some more space to store more data.
It shall be called within the write lock.
##### `void name_write_signal(name_t *out)`
Signal to the threads that some new data have been appended in the object.
It shall be called within the write lock.
##### `void name_free_signal(name_t *out)`
Signal to the threads that some space to store more data have been created.
It shall be called within the write lock.
##### `void name_write_unlock(name_t *out)`
Leave the lock of the object for writing its state.
It shall be called after `name_write_lock`
##### `type *name_ref(name_t *out)`
Return a pointer to the encapsulated object.
##### `type const *name_cref(const name_t *out)`
Return a constant pointer to the encapsulated object.
##### `name_t *name_new_from(type const src)`
Create a new shared object initialized with its `INIT_SET` operator on the given `src` and return a shared pointer to it.
This method is created only if the `INIT_SET` operator is defined.
_________________
### M-SHARED
> [!NOTE]
> This header is obsolete: M-SHARED-PTR should be used instead.
This header is for creating shared pointer.
A [shared pointer](https://en.wikipedia.org/wiki/Smart_pointer)
is a smart pointer that retains shared ownership of an object.
@@ -4603,6 +5100,9 @@ _________________
### M-I-SHARED
> [!NOTE]
> This header is obsolete: M-SHARED-PTR should be used instead.
This header is for creating intrusive shared pointer.
#### `ISHARED_PTR_INTERFACE(name, type)`
@@ -4967,6 +5467,9 @@ _________________
### M-CONCURRENT
> [!NOTE]
> This header is obsolete: M-SHARED-PTR should be used instead.
This header is for transforming a standard container (`LIST`, `ARRAY`, `DICT`, `DEQUE`, ...)
into an equivalent container but compatible with concurrent access by different threads.
In practice, it puts a mutex lock to access the container.