Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ system, such as SQLite.
* [Observing changes to shared state](#Observing-changes-to-shared-state)
* [Initialization rules](#Initialization-rules)
* [Deriving shared state](#Deriving-shared-state)
* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm moving this up higher because it's important to know this before the testing material.

* [Testing shared state](#Testing-shared-state)
* [Testing when using persistence](#Testing-when-using-persistence)
* [Testing when using custom persistence strategies](#Testing-when-using-custom-persistence-strategies)
Expand All @@ -41,7 +42,6 @@ system, such as SQLite.
* [Testing tips](#Testing-tips)
* [Read-only shared state](#Read-only-shared-state)
* [Type-safe keys](#Type-safe-keys)
* [Concurrent mutations to shared state](#Concurrent-mutations-to-shared-state)
* [Shared state in pre-observation apps](#Shared-state-in-pre-observation-apps)
* [Gotchas of @Shared](#Gotchas-of-Shared)

Expand Down Expand Up @@ -446,6 +446,39 @@ else { return }
todo // Shared<Todo>
```

## Concurrent mutations to shared state

[mutating-shared-state-article]: https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/mutatingsharedstate

While the [`@Shared`](<doc:Shared>) property wrapper makes it possible to treat shared state
_mostly_ like regular state, you do have to perform some extra steps to mutate shared state.
This is because shared state is technically a reference deep down, even
though we take extra steps to make it appear value-like. And this means it's possible to mutate the
same piece of shared state from multiple threads, and hence race conditions are possible. See
[Mutating Shared State][mutating-shared-state-article] for a more in-depth explanation.
Comment on lines +457 to +458
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I'm linking out to the dedicated "Mutating Shared State" article over in the Sharing repo.


To mutate a piece of shared state in an isolated fashion, use the `withLock` method
defined on the `@Shared` projected value:

```swift
state.$count.withLock { $0 += 1 }
```

That locks the entire unit of work of reading the current count, incrementing it, and storing it
back in the reference.

Technically it is still possible to write code that has race conditions, such as this silly example:

```swift
let currentCount = state.count
state.$count.withLock { $0 = currentCount + 1 }
```

But there is no way to 100% prevent race conditions in code. Even actors are susceptible to
problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many
mutations of the shared state as possible in a single `withLock`. That will make
sure that the full unit of work is guarded by a lock.

## Testing shared state

Shared state behaves quite a bit different from the regular state held in Composable Architecture
Expand Down Expand Up @@ -476,7 +509,7 @@ struct Feature {
Reduce { state, action in
switch action {
case .incrementButtonTapped:
state.count += 1
state.$count.withLock { $0 += 1 }
return .none
}
}
Expand Down Expand Up @@ -964,36 +997,6 @@ struct FeatureView: View {
}
```

## Concurrent mutations to shared state

While the [`@Shared`](<doc:Shared>) property wrapper makes it possible to treat shared state
_mostly_ like regular state, you do have to perform some extra steps to mutate shared state.
This is because shared state is technically a reference deep down, even
though we take extra steps to make it appear value-like. And this means it's possible to mutate the
same piece of shared state from multiple threads, and hence race conditions are possible.

To mutate a piece of shared state in an isolated fashion, use the `withLock` method
defined on the `@Shared` projected value:

```swift
state.$count.withLock { $0 += 1 }
```

That locks the entire unit of work of reading the current count, incrementing it, and storing it
back in the reference.

Technically it is still possible to write code that has race conditions, such as this silly example:

```swift
let currentCount = state.count
state.$count.withLock { $0 = currentCount + 1 }
```

But there is no way to 100% prevent race conditions in code. Even actors are susceptible to
problems due to re-entrancy. To avoid problems like the above we recommend wrapping as many
mutations of the shared state as possible in a single `withLock`. That will make
sure that the full unit of work is guarded by a lock.

## Gotchas of @Shared

There are a few gotchas to be aware of when using shared state in the Composable Architecture.
Expand Down
Loading