Skip to content

Commit 61b47d5

Browse files
authored
Merge pull request #14 from cschreib/small-fixes
Clarify thread-safety in the readme, and improve CI for Wasm
2 parents 57c0bb0 + 3d7c3ac commit 61b47d5

File tree

2 files changed

+22
-13
lines changed

2 files changed

+22
-13
lines changed

.github/workflows/cmake.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
uses: actions/cache@v3.0.4
4343
with:
4444
path: ${{env.EM_CACHE_FOLDER}}
45-
key: ${{env.EM_VERSION}}-${{ runner.os }}
45+
key: ${{env.EM_VERSION}}-${{matrix.platform.name}}-${{matrix.build-type}}
4646

4747
- name: Setup Emscripten
4848
if: matrix.platform.compiler == 'em++'

README.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Built and tested on:
1616
- [enable_observer_from_this](#enable_observer_from_this)
1717
- [Policies](#policies)
1818
- [Limitations](#limitations)
19+
- [Thread safety](#thread-safety)
1920
- [Comparison spreadsheet](#comparison-spreadsheet)
2021
- [Speed benchmarks](#speed-benchmarks)
2122
- [Alternative implementation](#alternative-implementation)
@@ -31,7 +32,7 @@ The only difference between `observable_unique_ptr` and `observable_sealed_ptr`
3132

3233
These pointers are useful for cases where the shared-ownership of `std::shared_ptr` is not desirable, e.g., when lifetime must be carefully controlled and not be allowed to extend, yet non-owning/weak/observer references to the object may exist after the object has been deleted.
3334

34-
Note: Because of the unique ownership model, observer pointers cannot extend the lifetime of the pointed object, hence this library provides less safety compared to `std::shared_ptr`/`std::weak_ptr`. This is also true of `std::unique_ptr`, and is a fundamental limitation of unique ownership. If this is an issue, simply use `std::shared_ptr`/`std::weak_ptr`.
35+
Note: Because of the unique ownership model, observer pointers cannot extend the lifetime of the pointed object, hence this library provides less safety compared to `std::shared_ptr`/`std::weak_ptr`. See the [Thread safety](#thread-safety) section. This is also true of `std::unique_ptr`, and is a fundamental limitation of unique ownership. If this is an issue, simply use `std::shared_ptr`/`std::weak_ptr`.
3536

3637

3738
## Usage
@@ -104,10 +105,20 @@ If the trade-offs chosen to defined the "convenience" types are not appropriate
104105

105106
The following limitations are features that were not implemented simply because of lack of motivation.
106107

108+
- this library is not thread-safe, contrary to `std::shared_ptr`. See the [Thread safety](#thread-safety) section for more info.
107109
- this library does not support pointers to arrays, but `std::unique_ptr` and `std::shared_ptr` both do.
108110
- this library does not support custom allocators, but `std::shared_ptr` does.
109111

110112

113+
## Thread safety
114+
115+
This library uses reference counting to handle observable and observer pointers. The current implementation does not use any synchronization mechanism (mutex, lock, etc.) to wrap operations on the reference counter. Therefore, it is unsafe to have an observable pointer on one thread being observed by observer pointers on another thread.
116+
117+
The above could be fixed in the future by adding a configurable policy to enable or disable synchronization. However, the unique ownership model still imposes fundamental limitations on thread safety: an observer pointer cannot extend the lifetime of the observed object (like `std::weak_ptr::lock()` would do). The only guarantee that could be offered is the following: if `expired()` returns true, the observed pointer is guaranteed to remain `nullptr` forever, with no race condition. If `expired()` returns false, the pointer could still expire on the next instant, which can lead to race conditions. To completely avoid race conditions, you will need to add explicit synchronization around your object.
118+
119+
Finally, because this library uses no global state (beyond the standard allocator, which is thread-safe), it is perfectly fine to use it in a threaded application, provided that all observer pointers for a given object live on the same thread as the object itself.
120+
121+
111122
## Comparison spreadsheet
112123

113124
In this comparison spreadsheet, the raw pointer `T*` is assumed to never be owning, and used only to observe an existing object (which may or may not have been deleted). The stack and heap sizes were measured with gcc 9.3.0 and libstdc++.
@@ -126,13 +137,13 @@ Labels:
126137
| Owning | no | no | no | yes | yes | yes | yes |
127138
| Releasable | N/A | N/A | N/A | yes | no | yes | no |
128139
| Observable deletion | no | yes | yes | yes | yes | yes | yes |
129-
| Thread-safe deletion | no | yes | no(1) | yes(2) | yes | yes(2) | yes(2) |
130-
| Atomic | yes | no(3) | no | no | no(3) | no | no |
140+
| Thread-safe | no | yes | no | no | yes | no | no |
141+
| Atomic | yes | no(1) | no | no | no(1) | no | no |
131142
| Support arrays | yes | yes | no | yes | yes | no | no |
132143
| Support custom allocator | N/A | yes | no | yes | yes | no | no |
133-
| Support custom deleter | N/A | N/A | N/A | yes | yes(4) | yes | no |
134-
| Max number of observers | inf. | ?(5) | 2^31 - 1 | 1 | ?(5) | 1 | 1 |
135-
| Number of heap alloc. | 0 | 0 | 0 | 1 | 1/2(6) | 2 | 1 |
144+
| Support custom deleter | N/A | N/A | N/A | yes | yes(2) | yes | no |
145+
| Max number of observers | inf. | ?(3) | 2^31 - 1 | 1 | ?(3) | 1 | 1 |
146+
| Number of heap alloc. | 0 | 0 | 0 | 1 | 1/2(4) | 2 | 1 |
136147
| Size in bytes (64 bit) | | | | | | | |
137148
| - Stack (per instance) | 8 | 16 | 16 | 8 | 16 | 16 | 16 |
138149
| - Heap (shared) | 0 | 0 | 0 | 0 | 24 | 4 | 4 |
@@ -144,12 +155,10 @@ Labels:
144155

145156
Notes:
146157

147-
- (1) If `expired()` returns true, the pointer is guaranteed to remain `nullptr` forever, with no race condition. If `expired()` returns false, the pointer could still expire on the next instant, which can lead to race conditions.
148-
- (2) By construction, only one thread can own the pointer, therefore deletion is thread-safe.
149-
- (3) Yes if using `std::atomic<std::shared_ptr<T>>` and `std::atomic<std::weak_ptr<T>>`.
150-
- (4) Not if using `std::make_shared()`.
151-
- (5) Not defined by the C++ standard. In practice, libstdc++ stores its reference count on an `_Atomic_word`, which for a common 64bit linux platform is a 4 byte signed integer, hence the limit will be 2^31 - 1. Microsoft's STL uses `_Atomic_counter_t`, which for a 64bit Windows platform is 4 bytes unsigned integer, hence the limit will be 2^32 - 1.
152-
- (6) 2 by default, or 1 if using `std::make_shared()`.
158+
- (1) Yes if using `std::atomic<std::shared_ptr<T>>` and `std::atomic<std::weak_ptr<T>>`.
159+
- (2) Not if using `std::make_shared()`.
160+
- (3) Not defined by the C++ standard. In practice, libstdc++ stores its reference count on an `_Atomic_word`, which for a common 64bit linux platform is a 4 byte signed integer, hence the limit will be 2^31 - 1. Microsoft's STL uses `_Atomic_counter_t`, which for a 64bit Windows platform is 4 bytes unsigned integer, hence the limit will be 2^32 - 1.
161+
- (4) 2 by default, or 1 if using `std::make_shared()`.
153162

154163

155164
## Speed benchmarks

0 commit comments

Comments
 (0)