-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AssertThreadSafe
(name TBD) – a more general API for lifting conservative !Send
or !Sync
implementations
#322
Comments
One possible alternative / addition would be to introduce separate |
Just some thoughts on the problem you're addressing, not on the specific proposal to solve it: I very much agree that the !Sync/!Send-ness of pointers and UnsafeCell should be 'weaker' than that of other types (like some MutexGuards, etc.). It's annoying how this does not compile: fn main() {
let a: *const i32 = &10;
std::thread::scope(|s| {
s.spawn(|| {
println!("{a:?}");
});
})
} But this does: fn main() {
let a: *const i32 = &10;
+ let a = a as usize;
std::thread::scope(|s| {
s.spawn(|| {
+ let a = a as *const i32;
println!("{a:?}");
});
})
} One one hand it's good that this forces you to properly encapsulate the pointer (and the related unsafety) in a type, although in this trivial example (just printing an address), there is no unsafety at all to be encapsulated. (I suppose that in any real world situation, you'd only use a pointer when doing something unsafe with it, so it's good to force encapsulation. But I've noticed that this is can cause confusion and annoyance when learning or explaining Rust with small snippets.) This "not Send/Sync but could safely be Send/Sync" property only really applies to four types ( I've often thought that this problem should be solved at the language level, but this ACP shows that we can also provide a reasonable solution/workaround in the library. Whether this is the best solution we can provide is a good question. Making pointers Send and Sync today would make existing code unsound, as many structs today rely on a pointer field making them !Send and !Sync. We could make pointers Send and Sync in a new edition, however, if we allow for another way to make your type !Send/!Sync (negative impls or std::marker::NotSync or something). But that would cause any code that is not carefully fixed/updated (including sample snippets from books etc.) to become wildly unsound when copied into a newer edition. |
The But I'm not sure if this solution makes it much better:
Perhaps it's just a naming problem, but this reads to me as if we're a) making an unsafe assumption, b) making an assumption about the Send/Sync-ness of I'm trying to imagine how I'd explain More importantly, even with this |
That Which makes me wonder how often one really wants AssertThreadSafe vs something weaker ("safer"?). I suppose that's the exact same question as whether
You mention that More importantly, I can think of a few use cases where one might use an But.. in those cases, are we really asserting something about the UnsafeCell, or about the T? That is, should we be looking at For the latter, we already have that exact type (unstably): Exclusive.
This idea basically splits up the functionality of Would that be better, or just be harder to explain and get right? It feels "safer" to me. |
Thanks for the feedback on the naming. I think it’s safe to say that including the word “assert” has its problems, and it now seems like using it for thread-safety traits ( More than asserting anything about the contained type, instead “
Let’s not forget about pub struct IterMut<'a, T: 'a> {
ptr: NonNull<T>,
end_or_len: *mut T,
_marker: PhantomData<&'a mut T>,
}
Fair point. My example was using I figured it was important to point out (and demonstrate with a code example) that I don't even know what the exact reasoning was behind the design decision that
I think the opposite case is just as important. Many use cases will require more than
How useful One would teach that raw pointers (and UnsafeCell) are Footnotes
|
I think the requirements between “thread-safe versions of pointers”1 and “thread-safe versions of It could even be the case that For example… if in the end of such a discussion, “thread-safe versions of pointers” doesn’t get any Footnotes
|
Under provenance rules (which ralf is going to RFC) this might become a dubious justification. Do we have any other way to smuggle pointers between scopes that would remain valid? |
Probably conversion to and from |
Here’s the relevant code example fn main() {
+ use core::sync::atomic::AtomicPtr;
let a: *const i32 = &10;
+ let a = AtomicPtr::new(a.cast_mut());
std::thread::scope(|s| {
s.spawn(|| {
+ let a = a.into_inner() as *const i32;
println!("{a:?}");
});
})
} |
A thought to consider before stabilization of an API like this: Given some better trait coherence handling, we could instead write this such that it can't be instantiated at all on certain types (e.g. MutexGuard), rather than such that it can be instantiated but isn't actually Send: This is not a blocker for the ACP. It's something I'd like added to unresolved questions in the corresponding tracking issue for the unstable feature if this is added. That way, if coherence has improved by the time we go to stabilize this, we can use it, and if it hasn't, we'll probably go ahead with this as-is. |
@joshtriplett Can you give some more context to this? This sounds a bit like a thought I’d had, too, that supporting types Here’s my reasoning why that could be undesired: There’s the point that “there’s little use in supporting it”, because the indended use-case of Besides the point that there’s no reason against such a restriction, I could see the downside that something like User’s structs can include a And now how it could be achieved: I don’t seem to be able to spot any blockers or language features that could help make this any more easily achieved, but I’m curious to learn more. I though, it could be a simple trait bound on the struct.
The third trait is sometimes more generally implemented and thus necessary. E.g. impl<T> CanAssertThreadSafe for UnsafeCell<T> {} fully general, but impl<T: Send> SendIsNotUnsound for UnsafeCell<T> {} with the Also I’m right now realizing that I’m missing a lot of Footnotes
|
How about naming the types As for the API:
|
Proposal
Problem statement
This proposes a generalization of the existing unstable API
SyncUnsafeCell
. TheSyncUnsafeCell
type – as far as I understand from the PR that adds it – serves (at least) two purposes:static mut
Send
orSync
implementations could be skipped.The motivation for skipping explicit
Send
orSync
implementations isn’t fully explained. However my take on this is that it allows to reduce verbosity, and maybe even to increase accuracy, lowering the chance to mess up manualSend
orSync
implementations: For instance, if you are implementing a synchronization primitive that closely mirrors another existing one in terms of its API – say, a customMutex
re-implementation – then you could simply useSyncUnsafeCell<T>
, together with aPhantomData<Mutex<T>>
and then be confident that you got the rightSend
andSync
implementation to inherit fromstd
’sMutex
. (This example also doesn’t fully work withSyncUnsafeCell<T>
because unlike aMutex
,SyncUnsafeCell<T>: Sync
requiresT: Sync
, something that makes it not a fullstatic mut
replacement, anyways, but that’s a discussion separate from this proposal which can support either of keeping or dropping thisT: Sync
restriction.)My observation now is: There is another type in the standard library that comes with a conservative, but non necessary for soundness, implementation of
!Send
and!Sync
: Raw pointers. And there, too, it’s very common one has to re-writeSend
andSync
implementations manually, even when the type very closely resembles another existing one.Just to give one example: The type
std::slice::Iter
which even contains a
PhantomData
marker already, where it could simply inheritSend
andSync
implementations from&'a T
, needs to verbosely (and with a chance of typo / mistake), feature some manualunsafe impl Send
andunsafe impl Sync
implementations mirroring the ones from&'a T
.There likely countless other use cases of pointers used in ways where the pointer is “almost like a reference” in most ways including thread safety, and coming with an accompanying
PhantomData
(taking care of the lifetime). For example if one uses a pointer only because the data is not aligned properly, or to opt out of strict aliasing requirements as inaliasable::boxed::AliasableBox
.Instead of offering a full copy of
UnsafeCell
’s API calledSyncUnsafeCell
without the conservative!Sync
restriction, and then addressing the above analogous problems for pointers, too, by mirroring their full API in hypothetical things likeSyncConstPtr<T>
,SyncMutPtr<T>
,SyncNonNull<T>
, we can introduce a single unified way (API) for lifting instance of types with a “lint-like!Sync
and!Send
implementation”, quite similar to the “lint-like” nature of!UnwindSafe
being lifted byAssertUnwindSafe
, I’m using the nameAssertThreadSafe
here.Motivating examples or use cases
To take the above example of
slice::Iter
, that could then look likeand needs no additional
Send
orSync
implementation anymore.Existing
SyncUnsafeCell<T>
is replaced byAssertThreadSafe<UnsafeCell<T>>
.Solution sketch
A minimal / starting point of such an API looks as follows
This particular approach also gives some
cross-crate traits with a default impl, like `Send`, should not be specialized
warnings, so I’m not sure whether or not this needs some additional (!Send
+!Sync
) marker field to reduce theSend
andSync
implementations to just the listed ones.Alternatively, there’s the option to make this (eventually) extensible, usable even for other crates that may feature some conservatively-
!Sync
types, too. The trait names are placeholders, but the idea would look like this:One could also support additional types from
std
. For exampleOption<NonNull<T>>
comes to mind, becauseAssertThreadSafe<Option<NonNull<T>>>
would be easier to work with perhaps thanOption<AssertThreadSafe<NonNull<T>>>
.To that end, this kind if
impl
could even be further generalized, at least in principle.though it’s hard to say where to stop, and also this all can be happening subsequently, anyways.
Beyond the above
#[derive(Debug, Copy, Clone)]
,AssertThreadSafe
could implement other traits, too. In particularDefault
. Furthermore,Deref
/DerefMut
(pointing to the wrapped type, similar to howManuallyDrop
does it) would also be an option. The field ofAssertThreadSafe
could also be private instead of public, and/or named.Alternatives
Removing or keeping
SyncUnsafeCell
as-is is the alternatives regardingUnsafeCell
. For pointer types: As already mentioned in the “Problem Statement” section, one could also offer non-!Sync
/!Send
pointer types via a dedicated API, such as individual typesSyncConstPtr<T>
,SyncMutPtr<T>
,SyncNonNull<T>
, or a wrapper only for the pointer types. But if those would be introduced in the future, we definitely need to compare the option of multiple separate types, or a smaller-scope wrapper type, to the unified API from this ACP.Or we can decide non-
!Sync
/!Send
pointer types will never be wanted enough, suggest relying on the crate ecosystem for anyone that may find use in it, in which case, forUnsafeCell
alone, a generalized wrapper like this may be overkill.Links and related work
Existing discussions on
SyncUnsafeCell
should be relevant, I don’t have a full overview of the most important ones at hand; I can update this section if anything relevant/related gets suggested.I’ve read rust-lang/rust#53639 (comment)
and the answer rust-lang/rust#53639 (comment)
where a hypothetical wrapper type came up being called
UnsafeSync<UnsafeCell<T>>
, and my proposal is partially inspired by that, even though I’m not sure what API (and most likely something different from this) was envisioned for “UnsafeSync
” in that comment. Also the observation in the answer thatdoesn’t apply here, because
AssertThreadSafe<UnsafeCell<T>>
can support requiringT: Sync
forSelf: Sync
, just as it does (and has to do to stay sound) forSend
. As mentioned above, I’m questioning whether thatT: Sync
is even wanted, but it works either way. (Just not both ways, in case we wanted both versions for some reason.1) (Here the idea to distinguishSyncUnsafeCell
from a differentAlwaysSyncCell
is mentioned, for instance.)What happens now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
Second, if there's a concrete solution:
Footnotes
Or maybe it can even work both ways, dropping the
T: Sync
requirement only onceAssertThreadSafe<AssertThreadSafe<UnsafeCell<T>>>
is used to really emphasize all thread-safety that can safely be dropped will be dropped. ↩The text was updated successfully, but these errors were encountered: