 Hi folks, welcome back to another Crust of Rust. It's been a while since last time, although if you're watching this recorded, I guess maybe it hasn't been, in which case, welcome immediately back. I've been trying to find, you know, what are good next topics for Crust of Rust, and over the past couple months I've sort of been noodling with a couple of different ideas, and Send and Sync came up because, not because Send and Sync are all that complicated in and of themselves, but more so because they tend to cause a bunch of confusion, probably because people think they're more complicated than they really are, and so I wanted to have a video going through, you know, at a high level, what are they, what are they for, but also dig into which types are send, which types are sync, and crucially, which types are not, because I think that's a good indicator of what they're really used for. I expect that this stream will probably be fairly short. There's not too much to talk about here, and so what I'll do is also some Q&A at the end. If you're watching this video on demand, what I'll do is I'll cut the Q&A sort of into a separate video, so I'll link that up here somewhere, and without more ado, I think we'll just dive straight in. So Rust has these two traits, Send and Sync, that are used to describe thread safety in the language, and specifically, they're used to represent thread safety at the type level. You know, there are all sorts of other languages where there are checks for whether you are doing something concurrently that you're not allowed to. The first one that comes to mind here, of course, is in Java collections. If you try to iterate over a collection while you're also modifying it, you get this concurrent modification exception, and it's a good example of the language at runtime catching if you're doing something that's probably wrong. However, in many cases, it's really just documentation that tells you whether or not it's okay for a given value or a given type to be used either across thread boundaries or used by multiple threads at the same time. In Rust, those concepts are all baked into the type system, so that at least in most cases, you can check for the thread safety and the correctness in terms of thread safety of your program just by type checking it. The two traits are very related, but they do serve different purposes. You couldn't really get away with just one or the other. Before I talk about how they differ, I want to talk about how they are the same. First of all, they are both marker traits. You'll see that Send is in the Stood Marker module and same thing for Sync. Markers, if we go over to the Marker module, this talks about primitive traits and types representing basic properties of types. What do you see if you look through these is that all of these marker traits are specifically traits with no methods, hence the name marker. They're only used to mark that type meets a given property or has a particular property or guarantee about it, but it does not confer any additional behavior. For example, if something implements a Send trait, you don't get a send method on it. That's not a thing. It just tells you that that type is Send. Marker traits are a little bit special as far as the compiler is concerned, too. If we dig into the source here, you'll see that it is marked as an auto trait. This is not directly combined with the fact that it's a marker trait. Not all marker traits are auto traits, but the fact that it's an auto trait means that the compiler will automatically implement this trait for you if all of the members of a type are themselves that same trait. In the case of Send, for example, if you define a new type, whether it's an enum or a struct or a type alias, that type alias is a little different. Let's take structs and enums or tuples and arrays and whatnot. They implement Send if all of the types that are inside of it also implement Send. Same thing for sync. These are auto traits. You can imagine that it would be weird to have an auto trait that is not a marker trait, because an auto trait implicitly is implemented for anything where all the members implement the trait, but it's not clear what you would put in if there was a method you had to implement there, how you would delegate that to the inner types. In general, all auto traits are marker traits, but not all marker traits are auto traits. The empty comment here is very helpful. Thanks. Those are the ways in which they're the same. They're both marker traits. They're both auto traits. Apart from that, they work the same way as all other traits. They're primarily used in trait bounds. That would be you can have a function or a method or something or even just a type that requires that an argument that is passed or a field that it has or something is generic over must be safe to cross thread boundaries. That gets us to, how are they different? We'll talk about Send first, because it's easier to explain sync in terms of Send than the other way around. The property that Send tells you about a type is that it's okay to pass that value to another thread. Specifically, that is, give away ownership of this thing to some other thread. That thread from that point forward gets to do whatever it wants with that value. It sort of confers that it's not just that that thread gets to read or gets to write. That thread can do whatever it wants to the value is the way to think about Send. Most types are just like in general. The idea being that if you have a primitive type, for example, if you have a Boolean or a number or something, giving it away to another thread has no downsides. There's no problem handing that value to another thread because it's just a number, it's just a Boolean, it's just a string. The kind of things that are not Send are more around types where if you pass them to another thread, then that thread might violate some implicit assumption or invariant of the underlying type. There are two primary examples of this. RC, the non-atomic reference counted type. We'll get into why that's a problem. The other one that comes up a fair amount is mutex guards. That applies both to the standard mutex but also to RW lock. This is not a universal property of any mutex but specifically the ones that are in the standard library that are backed by the OS implementation of these are usually, sorry, they are not Send because there's a requirement, at least on certain operating systems, that the thread that gets the lock has to be the same thread that releases the lock. If you take a lock on thread A, you're not allowed to then have thread B try to release the lock that A got. It has to be A that releases that lock. Notice that this doesn't mean that the mutex type itself isn't Send. It doesn't mean that RW lock is not Send. It's specifically the guard that's not Send. This is an example of a more general property which is if you have the drop implementation of a type refer to a thread local to thread local state that was created as part of the creation of the object, then that generally means the type can't be Send. Because imagine you create this object on thread A and it uses thread local state on A and then you send it to thread B and then thread B tries to drop it. It goes out of scope or whatever. Then when thread B drops it, if the drop tries to access that same thread local state, it's going to try to access B's thread local state, not A's, even though A is what it used in order to create the object. Therefore, you end up violating some internal invariant and that usually doesn't go very well. RC is a little bit of a different beast. The reason why RC isn't Send is because I can write code that, in fact, let me cargo new lib. What are we going to call this one? Sharing is caring. So if I go in here, I'll get rid of some of this. No, I mean actual thread locals, like the thread local macro from the sender library. So the problem with RC, in fact, let's just make our own RC. This is roughly what RC looks like. There is a box of an inner of T and the inner has a count, which is U size and it has a value, which is T. Actually, this is a, oops, no. This is one of these. We did a separate video on reference count and smart pointer types and the like, so I'm not going to go through all the details of why it does exactly this. But I wanted to show specifically for RC what happens. So it returns itself and that's just going to be RC, inner is going to be box into raw, box new, inner count is one, value is, I guess, values V. So it's going to be new and then impl T clone for RC T. Notice here, we don't require the T as clone. So I write this out because it's going to be a little bit easier to show where the problem arises. So this is going to be, you know, we create a new RC where inner is self.inner. But first we have to increment the reference count. So that's going to be so dereferencing self.inner mutably.count plus equals one and we're going to implement drop for RC T and that's going to be do this. So when we drop it, if the current count is one, then we're going to drop the inner thing from raw self.inner and that's also unsafe. And finally, you know, we implement ops deref for RC T. I'm going to talk to this a little bit more in a second. So that's going to be just unsafe self.inner dot value. All right. So this is the construction of RC very roughly and I've chosen not to use any of the other like unsafe cell or anything. This is just to give you a very basic overview of what happens. So you have an RC, all of the all the clones you have in an RC point to the same heap allocation. And in that heap allocation, you keep both the actual value that you're protecting and a count of the number of RCs that there are. Every time you clone an RC, you increment that count by one. And then the time you drop that RC, you decrement the count by one. If the count ever drops to zero, that means there are no more RCs that you just drop the last RC and therefore you drop the inner heap allocation and the value with it. And the reasoning here for why this unsafe is okay is specifically because and again, this, you know, technically this can't be a star mute. This has to be an unsafe cell. There are a bunch of things that I'm sort of skimming over here. But the reason why this is generally safe is because RC is not send and not sync. And what that means is we know that even though we have an immutable reference, there are no other threads. There's no other concurrent execution that is accessing count that is accessing inner at all. They might like there might be other RCs that have a reference to inner, but none of them are currently accessing it because RC is not allowed to leave the thread. And therefore we know that nothing else can be concurrently touching self dot inner because we are currently executing on this thread, right? So it's a sort of mutual exclusion by the fact that one thread can only be doing one thing at one time. So that's the reason why this axis is safe. And it's also the reason why this can just be a standard U size operation. It doesn't need to be any kind of atomic because we know that there are no other threads involved. Same thing for drop here. Same thing for why this is okay, right? We know that there's no race on this count because we know there's no concurrent execution. We are the only thread that's executing right now. Oh, the reason this needs to be boxed from raw is because self dot inner is just a raw pointer. So dropping it does nothing. We have to turn it into an owned box again. So the box drop gets to run to deallocate heap allocation and also run the the destructor for the inner type T. So that's sort of the basics of the argument. But crucially, as you see, this relies on the fact that RC is not sent. If RC was sent, then what I could do is, you know, X is RC nu of, you know, some value one. And then I can do thread spawn. I do let them Y equals RC nu two. And then I can do move and let Y standard thread unmatched. I think that's a lie. I don't think there's an unmatched here. Oh, it's because I need to move source lib to source main. And if I run cargo check, oh, X dot clone is what I meant to do here. Why is it allowing me to do this? Wait. Yeah, so as you see, the method signature on spawn requires that the function you pass in that it's argument descent, right? So it requires that you have to be able to pass the anything that the closure that you pass to spawn, anything that that closure moves is is send is allowed to be sent to another thread. And this is sort of where send primarily comes in useful, right? Anytime you want to spawn a thread, you will almost certainly be going through this API. And this API requires send. That's the reason why send has an effect. Why this is allowing this particular operation, though, is unclear because star mute should not implement send. But let me try this just to why is it allowing this? No, so I shouldn't need to explicitly not implement send because this type should already not be send. Give me one second here because this is real weird. Wait a second. Why is this allowing me to this? This is a good way to just test whether a type constraint you accept you expect actually works the way you want. So I'm going to take an X which is going to be an RC of whatever and it's going to call foo with X. Yeah. Okay, so our type isn't send, right? So what I'm doing here is I create a foo, a function foo that requires its argument to be send. I create a function bar that takes an RC. This is the RRC type and calls foo with that RC. And the compiler says, you know, bar calling foo this way is not okay because RC of unit does not implement send, right? And so the error gives us more explanation here. It says star mute inner cannot be sent between threads safely. And specifically this is because it appears within RC so the trade send is not implemented. And it's required by this bound in foo. But this should also not be okay. It might be because, okay, I think it's just the compiler being particularly smart here and realizing that this thread doesn't actually use Y. I think that was like this meant that it didn't even have to move Y into this thread at all and therefore it didn't require a send but that seems real weird. But okay, let's go back to this so I can remove our little foo bar business here. Okay, so this is let's pretend that didn't happen although it is useful. This seems like a arguably the compiler being too smart for its own good. But yeah, so what we're doing here is we're moving we're attempting at least to move Y into this new thread and then we're just going to drop it. But we do need to move it into the thread. But we also have a clone of that same RC in X. And now of course these two threads both have a pointer to the same RC to the same inner. And when we imagine that these two threads run concurrently and they both try to drop Y. So they both try to drop what is effectively the same RC. So both threads enter drop for RC at the same time. They both read the count. Then you might end up with neither of them seeing the other ones decrement yet because they're running concurrently, right? So they both here when they do when they dereference the count they might both see two not one. So neither of them go into this. They both go in here. They both decrement the count by one each so it's now zero but neither of them took this to actually drop. And that's that's you know less of a problem. It just means you have a memory leak. But it can happen the other way to right where you could have multiple threads seeing one because they race on the axis of this count field and then they both end up dropping. They both end up calling box from raw. They both end up invoking the drop type of the inner T. And that's just straight up unsound. That's undefined behavior. And so this is why send is valuable, right? It tells the compiler that this type you cannot share across thread boundaries because bad things will happen. And then the compiler will and in fact it's not even you know the compiler doesn't know anything special about threads. It just knows that this thread spawn function requires that the type it's passed implement send. And so it checks does everything that goes into this closure implement send. RC does not therefore this is not okay. Right? The type of why here is also RC. All right. So in other words this is why RC can't be sent. Very different reason for why mutex guard can't be sent but this is the reason that can't be sent. So let's then move on to sync. So sync is fairly related to send. In fact they're so related that sync is almost defined in terms of send. So specifically a type T is sync if and only if a reference to T is send. So let's try to digest that a little bit. If you have a type where a reference to that type is allowed to be shared across threads then that type is sync. Even if the type itself cannot be passed to some other thread. Like I can't give you a mutex guard but if I have a mutex guard I can give you a reference to that mutex guard. That's fine. Right? Because and this sort of come and the reason this distinction is important this comes back to the reason why mutex guard can't be sent. Right? Is that we can't allow some other thread to drop it. But if we just give away a shared reference to one that we already have they're not going to drop it for us. Right? They don't get any kind of owned access to that mutex guard. They can't drop it because it's behind a shared reference and dropping required a mutable reference. They can't you do like tricks with mem replace or anything because all of those require a mutable reference to the inner type. All they can do is read from it. Which is all fine. None of that means that they drop which is the thing that we were concerned about. However RC can't be sync. Right? So it's the same kind of problem. If RC was sync then what I could do is I could take a reference to an RC give that reference to another thread and then that other thread just calls clone on it. And we know that the clone implementation also requires that all access happens on one thread. So that is why RC is not send and not sync. Whereas a mutex guard is not send but it is sync. When I say shared reference I mean an actual reference. Like I mean the you know this type an actual shared reference. When I say mutable reference I mean of this. So mutex guard is not send but it is sync. RC is not send and not sync. Which might make you wonder okay are there types that are sync that are send but not sync. And there are. The the primary examples of this are the interior mutability types. So things like cell and ref cell. And cell is a really interesting example. So the basic idea of cell is that it doesn't give out references to the inner type. So you can have a cell of T and the things that you can do with a cell of T are you can create a new one by giving an old value. You can replace its value. You can extract its value but again buy value only by consuming self. And there's nothing in here that allows you to get a reference to the value that's inside of the cell. And the reason why this is okay is because on any single thread right imagine you're on a single thread here. If the values encapsulate inside of there and you never give it a reference to it then at any given point in time you know that you have exclusive access to the inner value. No one else has a reference to it because this doesn't give out references. And no other thread is currently executing on that value. So it's okay to again this ties back to the fact that this type is not sync. So no other thread has a shared reference to this type. And you haven't given out any shared references to the value that's inside of this. So therefore you are the only one with access to the thing inside the cell whenever you are executing. And therefore you're allowed to modify the value that's inside there because you you effectively have exclusive access. No other threads accessing and the current thread is you. And so that's why you can safely mutate the inside of a cell even though you only have a shared reference. But again it relies on the fact that cell is not sync. But it is send though right because again you haven't given out any references. So if you give away a cell to another thread there's nothing left on this thread that might interfere with that thread modifying the value right. If I have a cell and I give it away to you I cannot possibly have any references to the cell because then I wouldn't be allowed to move it or to the stuff inside the cell because there are no accessors that give out a shared reference in the first place. And so sending it totally fine. The moment I send it that other thread is free to do things like modify the inner value because it can rely on the same invariant right that no one else has a shared reference to the cell as in no other thread. And on this thread any shared reference to the cell is not currently executing because there's only one thread involved. Before I continue let's let's do some questions because I think there are a bunch of questions that are sort of related on this. Shouldn't cell not demand not sink on T then? Yeah so this is another good question which is what are the actual requirements on the inner type T here? So for cell if we go down a little bit you'll see that it implements send for cell T only when T is send. So this requirement on the inner type makes sense right because imagine that I have a cell T. If I give it away to another thread then I'm effectively giving away the T as well right because you can call into inner on a cell and get the inner T. So by sending the cell I'm also sending the T and therefore it's only safe to send the cell if the T is also send. And you see the implementation of not sink is just a blanket implementation. Like there are no requirements on T it's just saying cell is just never sink no matter what the inner type is. And so there's a question okay should cell require that the inner type is sink even though you're not sending it to a different thread and you don't need that requirement because if I give away a cell to another thread there are no references involved. I'm giving away the actual inner value so all I require is that that inner value is send. Is it good to use cell directly though? I mostly see code wrapping it inside some box or RC. Cell on its own is not super useful because why would you have the cell if you could just you know have an own T and just take references to it. The reason why cell comes in useful is imagine you're doing something like graph traversal where you might have cycles and stuff. You might be walking this tree in a single thread but that means that because you're walking the tree you can only have shared references to everything because you know if you're exploring the tree who knows what like let's say you're walking a chain right and that chain might loop you back on yourself. You can't take exclusive access or claim exclusive access to any given node because you might walk the same node more than once. So you're only going to have shared access to all the nodes in the tree at that point in time. But because you're single threaded you know that even though you only have shared access it's okay for you to mutate that thing in place and so that's where you might want to be able to modify the value through a shared reference. The reason you sometimes see it through RC is sort of the same reason because you might have some data structure where you might want to store a given value in multiple places and so but you do want to be able to mutate it if you ever walk to it and so you have an RC cell sort of the same thing. Yeah and then the other thing to mention about cell while we're on cell is that if the inner type is copy then you can get the inner t even with just a reference to self the idea being of course you can just copy the value out. Okay so let's look a little bit more at some of the details here. So if we scroll down the sort of implementation of send list you'll see there are a couple of these negative implementations. So negative implementations are an unstable feature. I can't actually see what the unstable feature is called but there's a there's an unstable feature I think it's called negative impulse which allows you to write as you saw just here. Imple not send for some type. Normally you can't write that in Rust. Negative implementations are an unstable feature. You can however always write impulse send for a type to tell the compiler that okay this type even though you think it's not send or you think it's not sink because of the because of the auto traits I'm telling you that this type actually is. Doing so is unsafe right because if the compiler knows that there's some inner type in what you constructed that's not send or not sync it assumes that that means that your type is also not send and not sync. But you can sort of tell it that no I've really checked but in order to do that that is unsafe because you are claiming something that the compiler can't guarantee is true. So doing a positive implementation of send is unsafe. Doing a negative implementation is unstable but it's not safe. It's not unsafe. So you might wonder okay why would I ever need this? Like why aren't you know why would I ever want to not implement send for a type? It's fairly rare that you actually need to do this. Usually it's the case that the auto impulse are going to be overly negative rather than overly positive. So in general your types are not going to implement send and sync when you want them to rather than they're going to implement send and sync and you don't want them to. That is very rare. It comes up usually if you're writing code where the internals of it hold only types that are send or sync like they hold boxes or integers or something but you're doing something with them like accessing a thread local where the act of accessing that thread local makes it not thread safe but there's nothing about the inner types that makes it not sync. So mutex guard might be example of this where you could at least imagine that what it contains is something that's you know totally send but because of this thread local access that makes it not send and it's not something the compiler could infer for you. Another thing that that's worth bringing up here if we go through this list is you'll see that there's an implementation of this is the one we the sort of the not primitive but the the basic implementation of sync right which is we implement send for a reference to T where T is sync. That's almost the definition of sync. You'll also see there's an implementation of send for a mutable reference to T where T is send and this might strike you as a little odd like you know if we're giving away a shared reference to T why does that require that that T is also send because I'm not giving away the actual T I'm just giving away a reference to it even though it is a mutable reference and the reason for this of course is methods like mem replace right so the mem replace function takes a mutable reference to a T and a T and gives you back the T that was stored behind this destination and it sort of swaps it out in place with this T instead but the problem here of course is that means that you can take ownership of something you were only passed a mutable reference to so if I send you a mutable reference to a T if that T is not send you could use this to take out the T and then you drop it so again I can imagine a mutex guard if I give you a mutable reference to a mutex guard you could take your own lock swap the two mutex guards and then drop my mutex guard which would not be okay and so that's why there's this send bound on the T for mutable references if we keep scrolling through a lot of these implementations that you see you might be surprised like why does there need to be a an explicit send implementation like auto trade implementations are not listed in this list so why does there need to be an explicit implementation of send for you know intermute for vecdq for example and if we go in there we might be able to see why so you see there are these manual implementations of the types and you see the safety comment says we do nothing thread local there's no interior immutability so the usual structural send sync applies so then you might go well why didn't the auto traits do this and the reason for that we can find by scrolling down a little bit longer where you see that there's an implementation of not send for const T and not send for mute T you'll see the same for sync so if we scroll down far enough you'll see that there's a negative implementation of sync for all the raw pointer types now the reason that exists is not because it has to be there right if you have a raw pointer you know there's nothing that stops you from sending the pointer or having sync on the pointer because ultimately it's unsafe to dereference anyway so why have these implementations they're not fundamentally not send or not sync the reason why these negative implementations are there is entirely to make it so that if a someone who writes a type isn't entirely clear on what the rules for send and sync are we sort of fail in the safe way which is their types are going to be not send and not sync if there have raw pointers in there because chances are they haven't thought about threat safety and then we would rather the types not be send and sync than for them to accidentally be send and sync when that wasn't okay so this is sort of a you can think of it as a sort of counter measure to the auto traits which is to say if your type contains raw pointers chances are you have to think carefully about threat safety and so therefore we're going to require that you put in a manual implementation of send and sync for those types and so we were sort of going to break the the auto trait inference intentionally if you ever stick a raw pointer in there does that make sense for why that's useful and indeed that is what we see for iterator right so iter mute here in in vectiq you see contains a just a raw pointer and therefore this type does not implement send or sync automatically by auto traits and so it requires this manual implementation and the manual implementation is okay because you know the as the comment here says they're not doing anything that makes it not send and sync but they need the explicit implementation to get over the fact that the auto traits did not apply here and if we keep scrolling through here you know the the send list is fairly short you see there's nothing that's super surprising here there are explicit implementations for anything that contain inner pointers there are negative implementations for raw pointers including things like non null which is also effectively a raw pointer as a negative implementation for rc and for rc weak which is effectively also just rc you see cell and ref cell both do implement send like we talked about they do not implement sync because that would not be okay what is interesting is you know atomic pointer which is also really just a raw pointer type does implement send and the idea here being you know if you if you know to be using atomic pointer chances are you have thought about send and sync i don't know whether this one was a good idea i think you know even if you're using an atomic pointer i think you should also still be required to do the manual implementation but you know such as life um what else do we have anything that's interesting yeah so here we see the the not send for the guards that we talked about um more not send for rc's oh interesting so the procedural macro types seem like they don't implement send so that probably means that they are that probably means that there's some thread local state when invoking um procedural macros i wish i could go to the source for those but i think those are um compiler implemented and so there's no source we can go to for sync uh the list is pretty much the same you know there are a bunch of manual implementation there are some uh there's some negative implementation interesting so receiver and sender are not sync they are send but they're not sync let's see if we can figure out why uh yeah so this is another good example actually if you look at um channel senders you'll see that so imagine that you have one of these multi-threaded channels right so you have a send end and a receive end um you'll notice that the send method does not require the t ascend you know that there's nothing here that requires the t ascend and the reason is if you have if you create a sender receiver pair of a channel um but you never move them to other threads then there's no requirement that the types that you send over there are themselves send because they never cross the thread boundary so instead and you'll see this is a fairly common pattern in the sender library um you'll see that there's only requirement that t ascend if the sender is sent so you can only move the sender and similarly the receiver across thread boundaries if the t ascend and so you can create a channel of non-send things and use it within a thread just fine and it's only the moment you try to move either end of the channel across the thread boundary that requires that the inner t ascend which is interesting the the reason i say you might say let's see this elsewhere um is if we look at something like arc um let's go to arc here so for arc you'll see that arc also does not require the t is send or the t is sync um sync is the more relevant one for arc right because in general you can't get a mutable reference from um from an arc t but it is possible right so it has things like get mute uh where is get mute right so you have this um if you have a mutable reference to an arc you might be able to get a mutable reference to the inner t if the if the number of clones of the arc is one like you have the last remaining reference then you can get an own t um but you'll see that none of these require the t is send or sync and it's only down here where if we find the send implementation so arc is only send if t is sync and send so again you can create an arc of some non sendable type just fine you can create clones of it that's fine it's only the moment you try to move any arc across the thread boundary or a reference to an arc across the thread boundary only then do we actually require that the inner type is thread safe and notice that for arc to be send and sync the inner type has to be both sync and send it has to be synced because all these other threads that have arcs effectively have references to that type so that type better be safe to reference for multiple threads but it also needs to be send because whichever thread drops the last rc is going to drop the inner type and you don't control which thread that is so therefore the t must be send because whichever thread drops it is going to take ownership of it and that might be a different thread that the one that created the t so it has to be send if it is a reference then it needs to be static or you have to move it by move I don't know what that question is moving or sending a ref requires sync which means it can be accessed by different threads yeah exactly so this is what we talked about earlier where we only implement send for references if the inner type is sync can we explicitly implement send for guards in certain os's you know it's a good question I think realistically you don't really want to do that because suddenly now which whether a type is thread safe in certain ways depends on what operating system you are on which is going to you know be really weird to debug like I understand the attraction but I think it's easier to just sort of have the same thing apply for all of them the other thing that's nice about requiring mutics guards to be not be sent is that it probably allows you to do slightly more optimizations on it right so even though there are some operating systems where maybe it's okay to drop it on a different thread you might be able to do optimizations based on the fact that you know it's not going to be sent yeah I wonder why there's a not sync implementation for sender I don't have a good answer for that I think it's because sender so mpsc is a sync uh you know that's a good question I think it's because the way a sender works internally in the standard library is each sender has a sort of slot in the channel and so when you clone a sender you basically sort of add a slot and if you had multiple threads that were allowed to send through the same sender then they would be contending for that slot and that slot is not intended to be thread safe that's my guess for why it's not sync because notice here sending on a channel just takes a reference to self not a mutable reference and so this is the way to say you know you can have a thread can have lots of shared references to its one sender and send through any of them without requiring any kind of coordination but the but you do have to have a separate sender for each thread I feel like this is probably the implementation bleeding into the api a little bit like you know this is not a fundamental requirement of like any multi-producer single consumer queue this seems more like you know this particular implementation required and therefore the implementation is there is the send trait that by default implemented on user's own structs and so this gets back to the fact that it's an auto trait so whether or not it is implemented depends entirely on what types it contains so if all the inner types of your type are themselves send then your type will be send if your type contains you know an rc or a mutex guard or a reference to a t where the t isn't sync or any kind of raw pointer then your type will not be send okay the last place i want to go to let me see if there are any other sort of sync things we should talk about here yes you can you can like scroll through all the auto implementers too if you if you're like curious to see these but in general the auto implementations are much less interesting because they are only implemented you know they're implemented without the programmer of that type having sat down and really thought about the the send and syncness whereas the manual implementations are because the auto stuff did not do the right thing and so they're usually more instructive to look at and my guess is you know if you go look at the source for these they're generally going to explain why this type is an exception right so as we talked about mutex guard for example totally fine to have multiple shared references you send to other threads to that mutex guard as long as you still retain ownership of the mutex guard and you're responsible for dropping it and i wonder whether the implementation says that i wish there were more comments for things like these impulse they really should have if you want an idea for going to send a pr to the standard library go do that the other thing you might have noticed is that for both the both send and sync in the standard library they both refer to the nomicon for more details the nomicon entry on send and sync is is pretty good it talks about many of the things that we've talked about today some of them in more detail some of them in less feel free to go read it it is a little bit of an interesting read it gives a slightly different perspective on this and you'll see that it talks about the the major exception same thing as we have you know on raw pointers and rc unsafe cell is the sort of deeper reason why our why cell and ref cell are not sync right is because unsafe cell is the only type in rust that allows you the only basic type in rust that allows you to mutably access something through a shared reference and that's what cell and ref cell both use internally in order to access their interstate because fundamentally they are mutating something given only a shared reference so they must go through unsafe cell and unsafe cell itself is not sync because it if you are using this type again they want to force you to think carefully about whether your type should be sync given that you're using this unsafe construct internally and that's why cell and ref cell will through their auto traits not implement sync and then it goes through you know the yeah okay so the feature is called negative impulse and as it points out you know it should be incredibly rare that you actually have to add such a negative implementation to yourself they go through a sort of example of you know a type that you might want to implement send for implement sync for because it happens to be okay and the requirements you know where you use these inner bounds we've talked a bunch about that too and yeah it talks about mutex guards so i think in general it touches on everything we have but it does have a more thorough example where they sort of walk through the code that it might be useful and that's sort of all there is to send and sync right that there's no compiler magic around send and sync beyond the fact that they're auto traits like the compiler doesn't know about thread safety all the compiler knows is that some types some like primitive types implement send some primitive types implement sync if all of the inner types of a type implement send then that type implement send same for sync and so that that's sort of the auto trade business and then it knows that there are some functions that have a a bound that requires that a type is send or that a type is sync and it just type checks those the same way it would any other trait so send and sync are not baked into the compiler at all the only thing they rely on is auto traits and one thing you should know about send and sync is that they are because they're market traits and they're a little bit special you're allowed to include them in in dynamic trait dispatch as additional traits so normally you know if i say uh if i do appear i suppose fnfu and i take a box din iterator item equal whatever um so normally if you use the sort of din syntax you're only allowed to name one trait you're not allowed to say you know din iterator plus clone for example this is something that we might get one day but as you say it says only auto traits can be used as additional traits on a trait object the way you would normally work around this you would say like iterator plus and clone and you say that it extends iterator with item equals this and clone and then you would say this takes a iterator and clone right clone is a bad example because it can't be a trait object anyway let's go with uh debug so this is the way that you normally get trait dispatch to multiple traits but as that error message was indicating the exception to this is auto traits like send and sync so you are allowed to write this so i can say you know this type is iterator and it is also both send and sync or i could do you know but only one or the other and this is a special property of these audio traits you can't do it with anything else you'll see this a lot in futures code like anything that uses async await and stuff where you might see you know din future output equals something plus send plus sync indicating that this particular type this particular future is safe to send to another thread to continue execution there normally for futures you only really care about the type being send because you know future to pull a future you need a mutable reference to it so it doesn't really matter that it's sync or it shouldn't really matter that it's sync but it does matter that it's send because very often you have multi-threaded executors and so imagine that a future is like running on this thread and then it gets paused because you know the network socket isn't ready or something so that thread of the executor picks up some other future and runs that instead and then this original future becomes ready again and then some other executor thread as idle it might pick up that future but that means that future now moved to a different thread and so we need to make sure that the future is send so that that operation is is valid and so very often you'll see that multi-threaded executors like the like the tokyo executor for example will require that the futures you pass it or send unless you use a sort of specific construct that allows you to run non-send futures and I think in tokyo it's called local set is a way that you can sort of spawn futures that aren't allowed to leave the current executor thread are there other basic methods other than thread spawn that require send and sync so the examples that come up here are you know arc and channels but ultimately both of those only it only matters whether those are send and sync because thread spawn requires it right and same thing for tokyo really if you think about it the reason it requires send and sync is because deep down there somewhere they spawn new threads and those threads their arguments need to be sent and those arguments are you know in some sense receivers for channels where they have to receive tasks on and those receivers therefore have to be send which means that the type inside those receivers have to be send and that's why the futures have to be send so like it's not quite turtles all the way down right like sort of at the bottom is whatever you try just run on a different thread that thing has to be send which means that any inputs it takes and any handles it has to anything else has to be sent and that's sort of where it all funnels down to so the question is you know are there other routes to this graph than thread spawn and not really I mean you could imagine that you you know if you directly implemented thread spawning yourself you know through like libc calls or you know the extreme version of this is something like fork but but if you were to implement this sort of primitive yourself you would need to write the send and sync bound but realistically for most rust code this comes down to the fact that somewhere in the call graph deep down there's a thread spawn and it doesn't have to be that you know you call thread spawn transitively it might be you know you have a sender but the receiving end of that was sent to a thread spawn right and therefore in order for you to get that sender the when the pair was created you know that receiver went to that thread but the sender went to you which means that the sender had to be sent thread spawn only requires that the type is static and send it does not require sync because it shouldn't right it doesn't take references to anything the reason it needs to be static is because that thread there's nothing that guarantees that this thread won't outlive the current thread so you know if i allocate some value on my stack and then i spawn a thread and it has a reference to my stack then that thread might keep running after i return and then that reference would no longer be valid there is some work in the standard library now to add back thread scoped which allows scoped threads so this allows you to have threads that don't take static arguments and the way that these work are basically by ensuring that the current thread or the current stack frame can't return until the thread has been joined so you know that the thread won't outlive the current stack frame and therefore it doesn't require a static it's a pretty neat addition it used to be in the standard library way back in the day and then was found to be on sound so it was removed but now it's back and it's also existed in the crossbeam crate for a while could a type be sync but not send yeah so mutex guard is an example of that it is sync but it is not send uh all right um i think that's where i want to end it unless there are questions you know specifically about send and sync before i before we move over to like a q and a section i told you this would be short what i really want to convey here is like send and sync are not magic they are the emphasis is antithesis to magic um the the only magical thing about them is the auto trait part and the auto trait part arguably doesn't matter right if if we didn't have if send wasn't an auto trait then everyone would have to implement it all over the place it'll be really annoying but they would still work for you know the their practical purpose of making sure that your program is thread safe but there's no you know the compiler doesn't have knowledge of threads it all just is this is a trait that is generally not implemented for types that aren't thread safe and that property sort of propagates through the system through the auto trait business and negative implementations for sort of key things in the standard library and then they're used in thread bounce and that's the only way that they you know work how do i implement not sync plus send i mean you just write those impulse i will say that you know the the negative impulse feature is because it's not because you can't use it on the let's see let's say that i had a you know mutex guard t and i can't use that a t this is a stupid mutex guard but let's say that i had this so this will automatically implement both send and sync you know assuming that t is respectively send and sync uh but imagine that i didn't want it to implement send you know i i can't i can't do this currently the standard library the on the stable compiler i won't be able to do this mutex card negative trait bounds are not yet fully implemented so you might wonder well how do i do this if i want to work on stable and usually the trick you pull is something like what you saw me do earlier which is you can stick you know a phantom data in here of an rc of nothing marker and phantom data means that you're not actually storing in rc the phantom data is just like a pretend this type was here but don't actually put one here don't take up any space and phantom data propagates through basically everything so particularly it it propagates through auto traits so rc is not send and not sync and therefore this type will be not send and not sync so that way you only write the positive impulse rather than writing the negative impulse and those you can write in the standard library so here i could write you know unsafe impulse sync for mutex guard t and that way mutex guard would be sync but not send by virtue of this sort of one way to to work around the the lack of negative impulse on stable auto traits are compiler magic um auto traits are you know the compiler when it constructs a type looks at the all the contained types and if all of them meet the the trait that's marketed as auto then the outer type also implements that trait so those those are magic uh implementing sync and safe implementing sync and send are both unsafe properties right because the compiler you would only write these if the the compiler's auto trait inference says that these types are not send and sync and then the compiler is going you know these shouldn't be implemented for this type and you're claiming they should be anyway that's unsafe you're you're claiming these types that i don't think are thread safe are thread safe prove it to me and that's generally the meaning of unsafe or the meaning of unsafe is the compiler saying you prove it if you're so smart right like that's really what the compiler does so manually implementing these is unsafe uh auto inference is not unsafe right so if i remove these um now mutex guard will be send and sync and i didn't have to do anything unsafe all right um here's what i'm going to do let's stop the uh send sync stuff here um and then i'll take just a quick like couple minutes break and then we'll do just general q and a um not related to send and sync at all uh so give me like five ish minutes um and then we'll be back and do more all right see you all in a very short while