Skip to content

Commit 4634dbf

Browse files
committed
feat: implement Shared Pointer
1 parent fe68195 commit 4634dbf

10 files changed

Lines changed: 867 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ jobs:
5656
- uses: Swatinem/rust-cache@v2
5757
- run: make miri-spinlock
5858

59+
miri-sharedptr:
60+
name: miri / SharedPtr + SharedPtrV2
61+
runs-on: ubuntu-latest
62+
timeout-minutes: 5
63+
steps:
64+
- uses: actions/checkout@v6
65+
- uses: dtolnay/rust-toolchain@nightly
66+
with:
67+
components: miri
68+
- uses: Swatinem/rust-cache@v2
69+
- run: make miri-sharedptr
70+
5971
miri-spscringbuffer:
6072
name: miri / SPSCRingBuffer + SPSCRingBufferV2
6173
runs-on: ubuntu-latest

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ loom:
4949
miri-spinlock:
5050
$(RUN_MIRI) spinlock
5151

52+
miri-sharedptr: # v1 and v2
53+
$(RUN_MIRI) sharedptr
54+
5255
miri-spscringbuffer: # v1 and v2
5356
$(RUN_MIRI) spscringbuffer
5457

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<div align="center">
22

3-
![Unsafe Usage](https://img.shields.io/badge/unsafe%20usages-33-brown?style=for-the-badge&logo=rust&logoColor=white)
4-
![Unsafe Blocks](https://img.shields.io/badge/unsafe%20blocks-25-red?style=for-the-badge&logo=rust&logoColor=white)
5-
![Unsafe Impls](https://img.shields.io/badge/unsafe%20impl-8-orange?style=for-the-badge&logo=rust&logoColor=white)
6-
![Files with Unsafe](https://img.shields.io/badge/files%20with%20unsafe-4%20%2F%206-blueviolet?style=for-the-badge&logo=rust&logoColor=white)
3+
![Unsafe Usage](https://img.shields.io/badge/unsafe%20usages-45-brown?style=for-the-badge&logo=rust&logoColor=white)
4+
![Unsafe Blocks](https://img.shields.io/badge/unsafe%20blocks-33-red?style=for-the-badge&logo=rust&logoColor=white)
5+
![Unsafe Impls](https://img.shields.io/badge/unsafe%20impl-12-orange?style=for-the-badge&logo=rust&logoColor=white)
6+
![Files with Unsafe](https://img.shields.io/badge/files%20with%20unsafe-6%20%2F%208-blueviolet?style=for-the-badge&logo=rust&logoColor=white)
77

88
> *Да, мы знаем что делаем. Наверное. (c)*
99
@@ -25,6 +25,8 @@
2525
- [Single-Producer-Single-Consumer Ring Buffer](src/spscringbuffer/README.md)
2626
- [Single-Producer-Single-Consumer Ring Buffer V2](src/spscringbufferv2/README.md) (cache line bouncing free)
2727
- [Lazy Initializer](src/lazy/README.md)
28+
- [Shared Pointer](src/sharedptr/README.md)
29+
- [Shared Pointer V2](src/sharedptrv2/README.md) (implicit `fence` optimizations)
2830
- [Кратко о главном](#кратко-о-главном)
2931
- [1. Mutual Exclusion](#1-mutual-exclusion)
3032
- [2. Точка с запятой](#2-точка-с-запятой)
@@ -852,6 +854,23 @@ _Раз мы читаем буфер, то увидели, что флаг вз
852854
853855
Скажем, что `a` **synchronizes-with** `b`, если `b` прочитало значение, записанное `а`.
854856
857+
> [!NOTE]
858+
> Это упрощённое определение. Полное определение из стандарта C++
859+
> ([atomics.order#2](https://eel.is/c++draft/atomics.order#2.sentence-1)):
860+
>
861+
> Release store `A` **synchronizes-with** acquire load `B` на том же атомике `M`, если `B` читает
862+
> значение из **release sequence headed by `A`**.
863+
>
864+
> **Release sequence** ([intro.races](https://eel.is/c++draft/intro.races#def:release_sequence)) -
865+
> максимальная непрерывная подпоследовательность side effects в **modification order** `M`, где
866+
> первая операция - release store `A`, а каждая последующая это атомарная **RMW-операция** (любого потока).
867+
>
868+
> Почему это важно: без release sequence **synchronizes-with** не работал бы через промежуточные потоки.
869+
> Например, в [SharedPtr](src/sharedptr/README.md) поток T1 делает release store (первый `fetch_sub`), а поток T3 - acquire load
870+
> (последний `fetch_sub`). Между ними поток T2 делает свой `fetch_sub` (RMW). Благодаря release sequence,
871+
> цепочка T1 -> T2 -> T3 сохраняет **synchronizes-with**, и T3 видит все записи T1 и T2 перед вызовом
872+
> деструктора.
873+
855874
### Видимость в happens-before
856875
857876
Каждое чтение не-атомика читает **последнюю** предшествующую в порядке **happens-before** запись в эту переменную.

assets/shared-ptr.png

491 KB
Loading

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
mod lazy;
2+
mod sharedptr;
3+
mod sharedptrv2;
24
mod spinlock;
35
mod spscringbuffer;
46
mod spscringbufferv2;

src/sharedptr/README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# SharedPtr (V1 — наивный AcqRel)
2+
3+
**SharedPtr** — указатель с подсчётом ссылок (аналог `std::shared_ptr` / `Arc`), демонстрирующий использование
4+
`memory_order_acq_rel` на RMW-операциях (Read-Modify-Write).
5+
6+
## Структура
7+
8+
```
9+
SharedPtr SharedInner (heap)
10+
┌─────────┐ ┌──────────────────────────┐
11+
| inner |──────>| ref_count: AtomicUsize |
12+
└─────────┘ | data: T |
13+
└──────────────────────────┘
14+
```
15+
16+
- `inner` — сырой указатель на аллоцированный в куче `SharedInner`
17+
- `ref_count` — атомарный счётчик ссылок
18+
- `data` — пользовательские данные
19+
20+
## Почему AcqRel
21+
22+
В предыдущих примерах (SpinLock, Lazy, SPSC) мы использовали отдельные `Acquire`-load и `Release`-store.
23+
Это работает, потому что load и store — разные операции на разных шагах.
24+
25+
В SharedPtr ситуация другая: `fetch_add` и `fetch_sub`**RMW-операции**, которые одновременно
26+
читают и пишут. Для RMW-операций `AcqRel` означает:
27+
28+
- **Acquire-часть** (чтение): видит все записи, опубликованные предшествующими Release-операциями
29+
- **Release-часть** (запись): публикует все записи этого потока для последующих Acquire-наблюдателей
30+
31+
## Memory Orders
32+
33+
### `clone`
34+
35+
```rust
36+
fn clone(&self) -> Self {
37+
unsafe { &*self.inner }
38+
.ref_count
39+
.fetch_add(1, Ordering::AcqRel); // (1)
40+
Self { inner: self.inner }
41+
}
42+
```
43+
44+
| # | Операция | Memory order | Обоснование |
45+
| --- | --------------------- | ------------ | ------------------------------------------------------------------------------------------------- |
46+
| 1 | `ref_count.fetch_add` | **AcqRel** | RMW: Release публикует инкремент, Acquire видит текущее состояние ref_count в modification order. |
47+
48+
### `drop`
49+
50+
```rust
51+
fn drop(&mut self) {
52+
let prev = unsafe { &*self.inner }
53+
.ref_count
54+
.fetch_sub(1, Ordering::AcqRel); // (2)
55+
56+
if prev == 1 {
57+
unsafe { drop(Box::from_raw(self.inner)); } // (3)
58+
}
59+
}
60+
```
61+
62+
| # | Операция | Memory order | Обоснование |
63+
| --- | ---------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
64+
| 2 | `ref_count.fetch_sub` | **AcqRel** | RMW: Release публикует все записи потока (модификации данных через `&T`). При `prev == 1` Acquire гарантирует видимость всех записей всех потоков, делавших предыдущие `fetch_sub`. |
65+
| 3 | `Box::from_raw` (деструктор) || Безопасно: AcqRel на (2) при `prev == 1` установил happens-before со всеми предыдущими drop-операциями через release sequence. |
66+
67+
## Release Sequence
68+
69+
![shared-ptr.png](../../assets/shared-ptr.png)
70+
71+
На диаграмме сценарий с тремя потоками:
72+
73+
- **T1** создаёт объект `Bar()`, передаёт указатель `p` в другой поток через **mo** (modification order)
74+
- `p → Foo()`: второй поток обращается к данным через `deref`
75+
- **`fa`** (`fetch_add`, 1 -> 2): clone с `Relaxed`, инкрементирует refcount
76+
- **`fs`** (`fetch_sub`, 2 -> 1 и 1 -> 0): drop с `Release`, декрементирует refcount
77+
- **`fa rlx`** (1 -> 2): промежуточная RMW-операция, часть release sequence
78+
- Последний `fs` (1 -> 0) с `AcqRel` / `fence(Acquire)`: синхронизируется со всей **release sequence headed by `fs`**, вызывает деструктор
79+
80+
Ключевой момент: `synchronizes-with` работает через промежуточные потоки.
81+
82+
```
83+
T1 (clone) T2 (drop) T3 (drop, последний)
84+
────────── ───────── ────────────────────
85+
fetch_add(AcqRel)
86+
1 -> 2
87+
fetch_sub(AcqRel)
88+
2 -> 1
89+
fetch_sub(AcqRel)
90+
1 → 0
91+
<- видит ВСЕ записи T1 и T2
92+
drop(Box::from_raw)
93+
```
94+
95+
Все `fetch_add`/`fetch_sub` — RMW-операции в **modification order** атомика `ref_count`.
96+
Они образуют **release sequence**, headed by первым Release store.
97+
98+
Согласно стандарту ([atomics.order#2](https://eel.is/c++draft/atomics.order#2.sentence-1)):
99+
100+
> Release store `A` **synchronizes-with** acquire load `B`, если `B` читает значение
101+
> из **release sequence headed by `A`**.
102+
103+
Поэтому T3, выполняя `fetch_sub(AcqRel)` и получая `prev == 1`, синхронизируется со всей
104+
цепочкой предыдущих RMW-операций и видит побочные эффекты всех потоков перед вызовом деструктора.
105+
106+
## Оптимизация
107+
108+
Эта версия использует AcqRel **везде** — это корректно, но избыточно. См. `SharedPtrV2` для
109+
оптимизированной версии с `Relaxed` / `Release` + `fence(Acquire)`, как в `std::sync::Arc`.

0 commit comments

Comments
 (0)