How Thread Safety is Changing in .NET 9
Vložit
- čas přidán 26. 08. 2024
- Get our Singleton Course for free: dometrain.com/...
Check out our new OpenTelemetry course: dometrain.com/...
Become a Patreon and get special perks: / nickchapsas
Hello, everybody. I'm Nick, and in this video, I will introduce you to a new C# type coming in .NET 9, which is how you will be doing locking and thread safety from now on.
Workshops: bit.ly/nickwor...
Don't forget to comment, like and subscribe :)
Social Media:
Follow me on GitHub: github.com/Elf...
Follow me on Twitter: / nickchapsas
Connect on LinkedIn: / nick-chapsas
Keep coding merch: keepcoding.shop
#csharp #dotnet
The padlock at 2:50 should really be non-static because static would even lock across different instances which is not needed here. Nick very likely knows that but IMHO he skipped the details saying "we don't want to allocate every time" but the behavior is completely different it's not just about allocating for each instance and allocating once.
This makes me wonder... It makes sense, but if you are going to do that, why not lock `this` instead? I think I could see issues if something else locks your class but it seems like it would generally work fine
@@zpa89 Yep, you have pointed out the issue. You really do not want to lock on something someone else can lock on. Debugging this kind of problems could get really tricky.
EDIT: as @dammej pointed out, this will actually break thread safety, didn't think it through :p
It could be easily solved like this:
[ThreadStatic] private static object? _lock;
//Use this
private static object Lock => _lock ??= new();
This way you're still avoiding repeated allocations, but it still keeps it thread safe.
@@nocturne6320 repeated allocations of an empty object really aren't going to hurt you. You should avoid micro optimizations like that until you get to a point where you know you are having issues and you have run a profiler that shows you the actual allocation of that empty object is a noticeable bottleneck.
@@nocturne6320 That would not work!
Consider this: Parallel creates multiple threads. All threads use Monitor.Enter to wait for other threads who are currently in the critical section. With [ThreadStatic] instead of every thread observing the same lock object, every thread would instead wait for their own lock object. This would obviously always be free. So we would end up again with with the race condition, that we started with.
My comment might be a bit off topic, but for some operations, including the example from video sum of integers, there is a good choice to use lock-free instructions. See Interlocked class, which allows thread-safe adding of two integers: Interlocked.Add(ref _sum, number);
There is also interesting topic - compare-and-swap (CAS) algorythm, in C# it can be achieved with Interlocked.CompareExchange. There are even some lock-free thread-safe data structures (like hashtables), based on this algorythm, and maybe Nick finds it interesting to create a video on this topic.
+1 for lock-free techniques
7:45 Slightly incorrect about TryEnter with a timeout, the lock will not be exited if you hold it too long, the timeout specifies how long TryEnter should wait to try to grab the lock, and then fail and return false if it is unable to do so in that time. There is no mechanism that will automatically release the lock, except for whatever you write into your own code.
Could just be an implementation detail?
I'd love to hear more about how SemaphoreSlim and locks work with performance comparisons. When you'd use one over the other etc. I've always preferred SemaphoreSlim, as it allows me to control how many threads can be active in a block and also still make use of async await.
Had to double check when this video was posted 4:45 you say C# 9 instead of .NET 9 😂
I'd love to hear more about best practices and libraries for Async locking
There's some talk about `await lock` similar to `await foreach` to allow asynchronous locking
locks belong to threads so async lock doesn't really make sense IMO. Should be a semaphore.
Brings back memory of intro class to OS - mutex, p & v, monitor, critical session... Thanks for introducing the new lock mechanism.
critical section*
I had to check the date of the video when you mentioned C# 9 instead of .NET 9.
Still don't understand what problem the new approach solves? Its syntax has not changed much, and it also does not support asynchronous calls. What difference does it make how much faster it is if you're going to use SemaphoreSlim instead anyway?
not all methods are async, so locking with the Lock class may still be useful
FYI, SemaphoreSlim is efficient and also uses locking in the background. The point is avoiding the need to lock a thread completely until it can continue, and for synchronous code you just use this lock.
A key difference here is that you're allocading a ref struct instead of an object (i.e. the reference used in the lock() statement); also, the Lock type facilitates a number of different implementations that the Monitor type doesn't. I haven't moved to .NET 9 yet, but am keen on checking out the new API. In any case, for most developers, using the traditional approach is just fine for the majority of cases, yet retaining the ability to refactor for more modern types later.
Mutex and semaphore require an os. (Not supported by browser, as anoted in source code). Lock is available on every platform even wasm, so they are getting ahead for wasm2 and multithreading operation support
Especially since if you enable true in the project file couldn't it just improve the code if it's the same?
I was really hoping the new Lock class would just work with async by default, given how many code paths are async in modern C#. Guess I was wrong.
for me in the first example with lock, I usually lock on the instance of the object itself (in this case the running instance of Example), rather than doing readonly and static a new object. If you have a shared static object as the lock argument, then it locks it for all Example instances, whereas locking on yourself (this instance) just locks it for you.
You’re right that in this case the lock object shouldn’t have been static, but there are cases where it does need to be. In all cases, I prefer to have a dedicated lock object, but it’s not absolutely necessary.
@@code8986 I think that's a good preference. That said, I can't think of an instance where coordination/locking needs to be done with an external/dedicated object. If you have any suggestions at the ready, I would really appreciate your insight, because honestly, I can't think of any.
Just to touch on some of the comments, it is important to understand that the same thread can re-enter in a lock however a semaphore will prevent this unless there is an available counter (please correct me ife knowledge is out of date on this). I believe this is one of the reasons await and lock don't play that well together.
Very informative, thank you! Excited for this.
Thanks for the update. I have a question though.
Is the topic about .NET 9 or C#9.
at 4:46, recorded audio: 'starting from c# 9 ... '. I guess you mean .NET 9
I expected this to be syntactic sugar for SemaphoreSlim that integrates into lock() keyword if a specific Lock type is detected. Left sorely disappointed, the amount of code where I use a lock in non-async code vs async code is like 5%. And frankly in that case I use sync version of my SemaphoreSlim wrapper anyway just to be consistent. This smells like a feature that Microsoft made for themselves to speed up the internals.
Mutex and semaphore require an os. (Not supported by browser, as anoted in source code). Lock is available on every platform even wasm, so they are getting ahead for wasm2 and multithreading operation support
@@duffscsSemaphoreSlim works in Wasm
I've read somewhere, that acquiring a SemaphoreSlim is ~10x slower than acquiring a simple lock...
@@GlebAOsokin lock() makes sense in specific scenarios in projects that are not asynchronous in nature, in parallel processing of non-I/O-bound data and maybe in low-level networking. I write mostly web and desktop and I would always opt for fully async execution tree in such projects. To be fair I would even do a console app be fully async with some rare exceptions. I don't want synchronous lock() in my projects. Async semaphore being even 50x slower is nothing compared to multiple threads doing the spinlock dance and hogging CPU cores. For me seeing lock() is a code smell once there's even a remote possibility the execution path may need to touch any kind of I/O. Converting lock to semaphore later is not always trivial.
@@the-niker Well, async lock is already a very rare thing, since you normally don't want to block on a long-running operation, to say nothing of an IO-bound one. I use locks all the time and almost never SemaphoreSlim for the same purpose (although I do have a RAII-like disposable wrapper for it just in case). So I kinda understand, why .NET team implemented it the way they did.
Cool. This allows for the potential to have like an AsyncLock object or other behaviors. I do tend to write lock-free code but occasionally need a lock and then async rears its head. Then I'm back to a good old staple of my custom lock-free spin lock type of thing.
isn't that just a semaphore?
Thoughts on syntatic sugar should be covered by "true"?
I feel like I'm be hood winked. If Span is running .NET 6-8 optimizations under the hood and other optimizations to improve speed.
A new setting say "true" could ENFORCE the compiler to change the object to padLock conversion for older style code with a sledge hammer approach.
You could use an analyzer to create an error if lock is used with anything other than a Lock object.
I keep adding Nito.AsyncEx to my projects. It's a fine library, but I would love it if MS would make some more built-in functionality for async locks. I don't want to spend my threads waiting for locks, when they have better things to do.
1:09 Paralyzed loop? Didn't know programming concepts could have disabilities. What a diverse world we live in
When there's a deadlock in your parallelized loop.
Very useful in case of third party API calls, for example if you want to get the weather data of 10 locations at a time, or 10 days weather at a time but your third party API only supports one parameter for one call, then parallelize them and get all the results. it's fun lol
Just wanted to point out the fix of using interlocked to do the sum without locking. But the point was the locking.
SpinWait is your friend.
Until it isnt
The static padlock will restrict access and serialize *every* Example object, even though they are otherwise independent. You can make that a class member and it will then serialize for each object instance. You don't even need to allocate anything as each object has its own already... lock(this) { } will serialize access to that object (it's still a giant lock for everything that the object can do). You can make it even more fine grained by having a member variable specifically for Sum and locking it and having different padlocks for different members (if you had them).
.NET has always been weird when it comes to thread safety. I'd say thread safety with these things still remains very weird, but at least they address these things with more reasonable solutions.
I saw Rust's multi threading much easier to make sense of
@@shahzaibhassan2777 until async comes into the picture
Mutex / locks have been adequate for most situations in my experience (been on since 1.0 beta, circa 2002). The ReaderWriterLock/Slim was a nice addition. What has been weird in your experience, specifically?
How is .NET weird with thread safety? If anything its pretty standard.
I was happy for a while, but then it is the same hassle when using it with async-await LoL 😂
A bit confused onj the difference between Enter and EnterScope. When and why would I need to create a lock scope? Why not just lock using the object?
The lock scope will be created for you on the boundaries of the lock if you lock a lock type
I think EnterScope creates a disposable Scope-object, so that you can use the using-keyword to automatically call Exit at the end of the scope.
There may be more to it though, so best to read the official documentation.
Would have been nice with a performance comparison
Have not used lock before
You’ve been lucky :)
There should have been a dedicated Lock type starting back in .NET 1.0! As it is, every single heap-allocated object has extra overhead to be able to store locking information *just in case* someone might want to lock on that object, even though 99.9+% of objects will never be used as locks.
AFAIK, the only reason they allowed everything to be usable in a lock statement is that they wanted to copy the Java idiom of "lock (this)". But Microsoft later came out with guidance saying not to use "lock (this)" because you don't want to lock on anything public, because if it's public then someone else could use it in their own lock statement, which could lead to deadlocks or data corruption. If they had come up with that guidance before .NET 1.0 was released, we might have had a Lock type in the very beginning.
In this case with incrementing variable it's better to use Interlocked.CompareExchange method. This way you aren't blocking any threads and perform incrementing as an atomic operation
How would that demonstrate the new Lock class?
`await` in `lock` is disallowed due to possible change of thread that is executing the code. Doubtful that MS will change that.
Why using lock when you have semaphore slim?
I would appreciate it if Microsoft took a longer look at how Rust has solved the whole thing. It's a very different approach and possibly we will get that after they have introduced more next level code validation in general, but it would be a great development if they could take some ideas from it at least.
Now there are still lots of ways to accidentally introduce race conditions in your code, there is nothing which actively helps you guarantee that you do things the right way.
Why do I still have to manually create a variable in the code? A user-friendly implementation of "lock {...}" could create a variable of type Lock internally. What is the reason that I must declare the Lock variable?
You can lock multiple access points to the same collection with the same lock object.
Also, the object should already exist when the thread's code arrive at try enter, otherwise all threads would create their own objects, and nothing would be locked :)
Not sure if this make a lot of difference if i would use Monitor explicitly
6:53 Maybe I'm missing something, but shouldn't the IL code have the .EnterScope() functionality within the try-finally block as well so it makes sure the .Dispose() is called for the scope i.e. what if .EnterScope() blows up? (I haven't looked at what it does exactly, mind you)
The try-finally block is to guarantee that the Monitor is exited if the was entered. Entering a try block can't fail, so the Monitor.Enter call can/should be outside it.
The using statement works the same way in that the variable initialization is outside the try-finally block.
E.g.
using (var f = File.Open(fn))
{
var b = f.ReadByte();
}
becomes
var f = File.Open(fn);
try
{
var b = f.ReadByte();
}
finally
{
f.Dispose();
}
@@phizc thanks, for some reason I always thought it makes more sense for this to also be within the try block
@@mamicatatibe If the variable initialization is inside the try block, then the Dispose method could be called on an uninitialized variable.
@@louisfrancisco2171The variable *declaration* needs to be outside of the try block, but couldn’t (and shouldn’t) the assignment (i.e. initialization) be *inside* the try block?
@@code8986 If there were an exception between the start of the try block and the initialization, then the variable would be uninitialized.
In the example above, if the file doesn't exist, execution would go to the finally block and you'd get a NullReferenceException from the call of Dispose on an unitialized variable.
9:54 I wrote one!
How is this better than the ReaderWriterLock?
FYI you talk about new locks in C# 9 but it should be .NET 9
Is it supposed to replace only the simple lock on object, or types like the readerwriterlockslim as well?
The former.
I dont really get why the sum isnt getting computed correctly. I have used parallel foreach to process large documents and never have gotten into an issue where they dont finish what they should. Could someone explain?
Adding an int is not an atomic operation. It first needs to read current value, then add an int and then write it back. Two threads can read the same value, add their ints and write it back, and so one write will be lost. So you need either to create a critical section with mutex (lock keyword) or use lock-free atomic operations, such as compare-and-swap which is a CPU instruction. Read current value and add an int, then use CAS which does this: if current value is equal to a passed value (we pass the one we read previously, i.e. it checks if the value has changed), write the new value. If CAS fails, we do everything again. Value equality check and writing the value happens atomically in this case.
@@vasiliychernov2123 Thanks, put in that way makes a lot of sense.
In short it's a race condition.
C#9 or .Net 9 ?
You’re right, he should’ve said .NET 9.
Sounds like it'd be way simpler to just do FP and avoid mutating objects like the plague, and then learn and utilize whatever transactional and/or concurrency capabilities your persistence layer provides since that would then be the only place you'd have to coordinate state changes.
edit: unless your doing something low-level/non "web-dev" in which case I'm completely outside of my wheelhouse
I can't think of a rational reason to lock an await call.
Genuine question... What's the point of locking? It's just sequential code with extra steps...
It’s about thread safety. Some operations might be 90% thread safe by default but they might have this 10% aspect of them that needs to be thread safe. You still gain 90% of performance by parallelizing most of it while locking what isn’t safe
It's basically to protect shared resources from race conditions, i.e. when two or more threads are competing for a limited resource (such as a shared integer, in Nick's example, which trivially could have been implemented using Interlocked.Increment).
@@nickchapsas Yeah but... have you ever encountered a situation where such code should exist? The best "real world" example that I can come up with is awaiting some multiple callbacks that eventually modify shared resource... But even then, I'd consider putting that operation in some queue or message broker... I never, ever used locks...
Queues made for concurrency hide the lock away from you.
@@cocoscacao6102
If you are implementing the queue processing you would use it.
ReaderWriterLockSlim
Interlocked.*
Why didn't they decide to support async code? - That's a disappointment
The lock syntax has always felt off and out of place in c#.
Glad it's now a specific lock object, and I get that it's still using the same syntax for migrating, but I hope we get a more flowing syntax.
The semaphore felt even worse the first usage, just because the term felt so out of place. It doesn't feel like the proper type to use just because it doesn't really state the usage.
The result is always different in czcams.com/video/b4sUebHOTi4/video.html because you are not waiting for the Parallel.For to complete. This does not affect your point, but concurrency is not the cause of the value always being different. A more complex scenario would be required to demonstrate that.
You partially right, he in fact doesn't check that the parallel.for is completed, but even if he waited for the run to complete, it would still result in having a different number at the end AND THE RESULT WILL STILL BE INCORRECT. The result at the end should equal to 1225
parallel.For doesn't not add thread-safety (or concurrency safety), is only that the 50 execution of the same code, and in the example, SHARE THE SAME REFERENCE of the class Example. You, as the developer, should ensure that the code that Parallel.For will execute is thread-safe (concurrency safe), Thread-safe mean that the code doesn't use share memory reference OR that they are protected against concurrency issue by issuing appropriate code synchronization mechanism (lock, semaphore, interlock, ...).
he only use the parallel.for to quickly demonstrate a thread-safe issue that could be resolve by using a lock to demonstrate the new Lock type
EDIT: remove the wrong end result
@@davilinkicefire Factorial is multiplicative, hes doing additive.
1225 is 0 through 49 added.
Parallel.For is blocking
@@JW-lv1fm You right, my bad
@@rauberhotzenplotz7722 🤦♂You're right, it's blocking, I missed that.
Why not use Task.WhenAll?
~Nick Chapsas~ => *Nick Sharpsas*
Would be nice if they address Task.Waits and UI Thread locking issues
That's not their problem, that's the programmer's problem. Don't use synchronous waits on asynchronous operations if you don't know what you're doing.
am I the only one, who've never used locking in my career as a web developer and always manage concurrency in DB?
I think that is pretty normal for web developers, especially if you don't work on tooling or framework code. If what you do is just Create-Read-Update-Delete APIs and front-ends then you can pretty much just leave all that to the database as you said.
Some examples of where I've used locking is for example to write rate limiters, for example when my backend needs to integrate with an external API.
But it is also useful for example if you want to prevent a convoy of operations from e.g. hitting some external storage or cache service, to have a lock and for all requests to wait for the cache service to return a result, a lock can be used for that.
This is a very common thing, especially when you want to efficiently handle multiple instances of the same work.
@@chralexNET yeah, rate limiting is a good example
I rarely write simple CRUDs. I usually write pretty complicated domain logic and optimistic concurrency solves most problems for me. I think, the more stateless your app is, the better
Well, the language is general purpose, it is not made specifically for you. I think it is alright for you to not require it, I myself almost never use locks nowadays for example (but I already used in the past for many things).
Even as a back end developer I hadn’t needed to use locks very frequently until recently.
I had an API that ran background jobs to cache data into some singleton in-memory repositories from a DB for better performance since they were expensive queries. But I had a problem if an API query came in during the time that the cache was filling. I didn’t want to return an error, so I needed to await until the cache refill job had finished to read from the cache. So, I ended up using a SemaphoreSlim to do the job as Nick showed at the end here.
What's this like compared to Java? Is Java more elegant with their approach to using multithreading?
Java has had explicit locks for many, many years. ReadOnly, ReadWrite, Reentrant, etc. It also has the intrinsic object monitors like C#.
If you don't have async support, you'd better have decent locks.
Modern java tends to use function paradigms like share nothing/immutable state
Don't use static, ever, all instances will have the same lock
In this case it was definitely wrong, but "ever" isn't true either. There are quite a few situations where static lock objects are the only good solution.
@@user-zz6fk8bc8u
The challenge from my experience is static variables creates a tight coupling, impacting testing, refactoring, evolution of the system, thread safety, etc...
@@user-zz6fk8bc8u can you elaborate please?
And if it's a shared system resource, instance level locks are worse than useless.
The only Hard rule for anything with programming is quite simple = Use the right tool for the job
You do not want to be a cargo cultist going with the first paradigm you learned for every problem.
Here to say first before five others do.
Seems dumb. Why not just make a MonitorSlim class and update the compiler to use that instead for locking or something. Why do we need a Lock object, it seems like an unnecessary change.
The Lock cannot be used in the async because it is a ref struct.
You can't use async code inside a lock block with the old approach either.
c# copying Java again after realising their way is wrong 🤣🤣🤣
The simple trick is to avoid locking whenever possible.
Theres a lot of bad advice and incorrect statements in this video. Love your stuff usually Nick but this isnt up your usual standard
Mind explaining?
Sure. I think a lot of others have mentioned this too but having a static lock object is a bad idea. Yes here in this video it's fine kind of but there's no need and teaches people the incorrect approach. In general the lock object should be the same life time as the resources it is guarding access to. Here (and most often) it's protecting instance data and so the lock object should be an instance field. If it's static you end up locking across all instances which is not needed when you are protecting data specific to one instance. (Hope that makes sense. CZcams comments aren't the easiest place to voice this 😂).
The other issue I spotted is the explanation of 'TryEnter' the timeout is not a limit to how long you can hold the lock. It's a limit for how long you'll wait to get it before it returns. Returning a Boolean indication of whether it successfully got the lock or not.
Love you videos Nick and apologies if it came across rude. It's just I see a lot of these misconceptions around and you're usually a good source of correcting people
I have seen that pattern for quite a long time. Using a static object means you are locking access to all objects, besides the one you are operating on. What you usually want (and no allocation is required) is to just call lock(this) { }.Only the target object is locked.
lock(this) is bad standards, because this is public and anyone outside can unlock it.
Another problem is that you don't indicate what logic you are locking. Sometimes when there are a lot of functions, its just nicer to say lock(SumVariableManipulationLock), in case you want to have more than 1 lock type for logic type, but now you partially indicate what its meant to keep threadsafe.
@@JW-lv1fm Thanks for the comment. Regarding the "it is public" part, no, you cannot "unlock" it since (in this particular case) you could only invoke "Monitor.Exit()" from within the same thread that has acquired the lock. Regarding your second statemente, I absolutely agree with you: you may use "sub-locks" for more granular locking behaviour, but I would never make them static. In any case, my preference is going "lock-free" (when possible) via "interlocked" operations.
@@dpduser The problem is not that someone else can unlock it. It's that anybody can lock it.
This was a known bad practice in java before C# stole it.
Locks themselves aren't performemt. Better to avoid them as much as possible imo, single threaded programing and queues really helps here. But still nicer syntax in c# 9 for when you need the lock
That really depends on what you are trying to achieve. I wouldn't say locks themselves are slow as they do their work in 10's of nanoseconds. They are used in alot of places even if you are not aware of them.
That's not great advice. They exist for a reason. Anyone who's looking into locking already knows that something special is happening and needs to learn about them.
This is actually a useful mechanism for high performance code (so calling them slow just means they've been used improperly - how do you single thread a queue lookup when you are waiting on an external resource, like a cold file? You would be blocking the other hot files with that queue).
There are many types of locks as well, which could be made more mainstream/have better documentation I suppose:
Spinlock, Mutex, Monitor, Semaphore, ReaderWriterLockSlim
They should add GPU-Based compilation
Thank you for support Ukraine!