dotnet / csharplang

The official repo for the design of the C# programming language
11.49k stars 1.02k forks source link

[Proposal]: Lock statement pattern (VS 17.10, .NET 9) #7104

Open stephentoub opened 1 year ago

stephentoub commented 1 year ago

Lock statement pattern

(This proposal comes from @kouvel. I've populated this issue primarily with text he wrote in a separate document and augmented it with a few more details.)

Summary

Enable types to define custom behaviors for entering and exiting a lock when an instance of the type is used with the C# “lock” keyword.

Motivation

.NET 9 is likely to introduce a new dedicated System.Threading.Lock type. Along with other custom locks, the presence of the lock keyword in C# might lead developers to think they can use it in conjunction with this new type, but doing so won't actually lock according to the semantics of the lock type and would instead treat it as any arbitrary object for use with Monitor.

Detailed design

Example

A type would expose the following to match the proposed pattern:

class Lock : ILockPattern
{
    public Scope EnterLockScope();

    public ref struct Scope
    {
        public void Dispose();
    }
}

public interface ILockPattern { }

EnterLockScope() would enter the lock and Dispose() would exit the lock. The behaviors of entering and exiting the lock are defined by the type.

The ILockPattern interface is a marker interface that indicates that usage of values of this type with the lock keyword would override the normal code generation for arbitrary objects. Instead, the compiler would lower the lock to use the lock pattern, e.g.:

class MyDataStructure
{
    private readonly Lock _lock = new();

    void Foo()
    {
        lock (_lock)
        {
            // do something
        }
    }
}

would be lowered to the equivalent of:

class MyDataStructure
{
    private readonly Lock _lock = new();

    void Foo()
    {
        using (_lock.EnterLockScope())
        {
            // do something
        }
    }
}

Lock pattern and behavior details

Consider a type L (Lock in this example) that may be used with the lock keyword. If L matches the lock pattern, it would meet all of the following criteria:

A marker interface ILockPattern is used to opt into the behaviors below, including through inheritance, and so that S may be defined by the user (for instance, as a ref struct). For a type L that implements interface ILockPattern:

SpinLock example

System.Threading.SpinLock (a struct) could expose such a holder:

struct SpinLock : ILockPattern
{
    [UnscopedRef]
    public Scope EnterLockScope();

    public ref struct Scope
    {
        public void Dispose();
    }
}

and then similarly be usable with lock whereas today as a struct it's not. When a variable of a struct type that matches the lock pattern is used with the lock keyword, it would be used by reference. Note the reference to SpinLock in the previous section.

Drawbacks

Alternatives

Unresolved questions

Design meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-01.md#lock-statement-improvements https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-16.md#lock-statement-pattern https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-12-04.md#lock-statement-pattern

tfenise commented 1 year ago
  1. If a System.Threading.Lock lo becomes typed as Object at compile-time for whatever reason, lock(lo) would break silently. For example:

    Dictionary<string, object> dic = new();
    Lock lo = new();
    lock(dic["my lock"] = lo) {}//this does not work.
  2. This pattern match design creates potential problems for codes which implement custom locks and want to take advantage of this new language feature. For example, suppose I implement my own lock:

    
    class MyLock
    {
    public LockHolder EntersLockWithHolder() {...}
    
    public struct LockHolder
    {
        public void Dispose() {...}
    }
    }

...

static readonly MyLock lo = new();

static void MyMethod() { lock(lo) { ... } }


Everything compiles even though the spelling `EntersLockWithHolder` is wrong and `lock(lo)` does not do what I intended.

This may also be problematic if `MyLock` is first written against .NET 8 and is later ported to .NET 7 or .NET Framework for whatever reason. In these cases, the codes would also break silently.

This problem could be solved by introducing a custom attribute only to be found in .NET 8 BCL that must be present on such lock types. The compiler would then detect any misspelling, and backporting to earlier version would result in a compilation error.

3. This new design creates inconsistency between newer lock types that should be used with `lock(...)` and older lock types like `Mutex` that should not be used with `lock(...)`.

4. If this new design is implemented in C#, it should also be implemented in VB.NET, as VB.NET has `SyncLock` which is basically equivalent to `lock` of C#.
vladd commented 1 year ago

If we consider implementing this feature, shouldn’t we consider its async counterpart which is lowered to await using, too?

HaloFour commented 1 year ago

@vladd

If we consider implementing this feature, shouldn’t we consider its async counterpart which is lowered to await using, too?

I believe that should follow a completely different pattern as locks should have to opt-in to being "async"-friendly.

HaloFour commented 1 year ago

@tfenise

If a System.Threading.Lock lo becomes typed as Object at compile-time for whatever reason, lock(lo) would break silently.

I have to agree, I think there's enough danger of these newer-style locks being accidentally treated as monitor locks by the compiler in any case where the pattern just happens to not match, with no warning to the developer. I think that needs to be added to the list of "Drawbacks" for this proposal.

Is the plan to also extend this to the myriad of other locks or synchronization primitives that exist in the BCL?

svick commented 1 year ago

Would it be feasible to introduce the ability of a type to forbid using Monitor on it into the runtime? Let's say this feature would be enabled by annotating the type as [ForbidMonitor]. Then the new Lock could look like this:

[ForbidMonitor]
class Lock
{
    public LockHolder EnterLockWithHolder();

    public struct LockHolder
    {
        public void Dispose();
    }
}

And used like this:

private readonly Lock _lock = new();

void Foo()
{
    lock (_lock) // lowers to EnterLockWithHolder()
    {
    }

    lock ((object)_lock) // lowers to Monitor.Enter, but throws at runtime
    {
    }
}
HaloFour commented 1 year ago

@svick

How would that be enforced? If you upcast the Lock it's just a normal object and the compiler wouldn't know to not use Monitor.

alexrp commented 1 year ago

@HaloFour As per the comment, it would be runtime-enforced. Presumably, if [ForbidMonitor] is applied to the type, some bit in the method table would be set to indicate that any Monitor methods called on the object should throw.

svick commented 1 year ago

@HaloFour I meant that the compiler would use Monitor, but the implementation of Monitor.Enter would detect that the type of the object is one of the annotated ones and would throw an exception.

But I don't know if that could be implemented without making every existing use of lock slower.

HaloFour commented 1 year ago

@alexrp @svick

Sorry, I see what you meant after another re-reading of the comment. It'd prevent a subtle break, but you could still end up with exceptions at runtime if you're not careful.

I'd love to see more language support around locking types. It's always annoying me that none of the lock types in the BCL even offer support for using, and I've written a bunch of extension methods specifically to enable that. It's nice to see that they're considering it for this new Lock class. I, personally, think that's sufficient. The problem with lock is that it's always going fallback to Monitor, and unless we want to make generic support for these locks a part of the runtime itself it feels a bit too dangerous to me to try to expand on that, at least without some novel guardrails. Honestly, I wish .NET could deprecate Monitor in general.

timcassell commented 1 year ago

@vladd

If we consider implementing this feature, shouldn’t we consider its async counterpart which is lowered to await using, too?

I believe that should follow a completely different pattern as locks should have to opt-in to being "async"-friendly.

Perhaps await lock?

Also, though I have seen await using used for an async lock before, I don't think it makes much sense. The popular AsyncEx library just uses using. So await lock (_lock) would lower to using (await _lock.EnterLockWithHolderAsync()).

TahirAhmadov commented 1 year ago

Hmm, my initial reaction is, not only we shouldn't add to lock() {} in C#, but we should discourage its use, and instead recommend the using(...) {} approach, even for the Monitor-based locking. This seems to be too much of a specific type of feature to be part of the language. And I also don't like the lock suddenly doing different things depending on the type's surface area. However, I don't think the Monitor locking is bad idea by itself, using something like using(Monitor.Lock(obj)) { ... } is perfectly fine with me and I use lock currently in many places, it's just that it's really not a language-level feature in my opinion. PS. To clarify, from a clean slate perspective, I wouldn't have made all .NET objects "lockable" with Monitor; it should be a feature offered by a special type, similar to Mutex/etc. But given the legacy, the Monitor just isn't going anywhere any time soon.

TahirAhmadov commented 1 year ago

Having thought about it, I would have liked in a "perfect world" to have lock (and await lock) statement in C#, which would do something similar to what the OP proposes, as syntactic sugar, and then a few different types could be offered in the BCL to address various common usage scenarios. However, given how lock is now tightly related to Monitor, it's not a good idea to expand it. Perhaps, we can introduce a new keyword here? Like synch and await synch? Then lock and Monitor can both be obsoleted over time with suggestions/warnings, auto-fixers, etc.

jaredpar commented 1 year ago

If a System.Threading.Lock lo becomes typed as Object at compile-time for whatever reason, lock(lo) would break silently. For example:

Correct and that is an explicit design choice. This proposal breaks compat for any type which is being locked on today that just happens to fit the new locking pattern. Casting to object in a lock is the mechanism for restoring the existing behavior. Essentially, it's a key part of the proposal that it be allowed and have this behavior.

lock(dic["my lock"] = lo) {}//this does not work.

This particular case is not very realistic to me. In practice, it's very uncommon for the expression inside a lock to be anything other than a variable (local or field) so the chance of this type of problem feels very low. I think the more realistic case is a Lock instance getting converted to object as a part of a more vanilla call and the receiver later decides to lock on it. Even that seems fairly unlikely cause locking on instances you don't own is a dangerous behavior.

HaloFour commented 1 year ago

@jaredpar

This proposal breaks compat for any type which is being locked on today that just happens to fit the new locking pattern. Casting to object in a lock is the mechanism for restoring the existing behavior.

Sounds like something which should be much more explicit. As described, it's one accidental behavior that has an accidental fallback; a pit of failure in a pit of failure.

omariom commented 1 year ago

Casting to object in a lock is the mechanism for restoring the existing behavior.

Wouldn't it go against the principle of least surprise?

CyrusNajmabadi commented 1 year ago

tbh, i would expect people to be more surprised by how things work today. Intending 'lock' to have the semantics appropraite for the lock-instance they're working with, rather than it having Monitor semantics.

TahirAhmadov commented 1 year ago

tbh, i would expect people to be more surprised by how things work today. Intending 'lock' to have the semantics appropraite for the lock-instance they're working with, rather than it having Monitor semantics.

For a c# programmer who knows all about await, foreach, using, and their surface area semantics, but absolutely nothing about lock, it would indeed be extremely weird to realize lock is so different from these other constructs. But that's a very small (if at all existent) demographic. Most people are very much used to lock relying on Monitor and this is a major departure. Having said that, it is weird how lock works currently, which is why I think a new keyword and an orderly, safe transition is in order.

jaredpar commented 1 year ago

Sounds like something which should be much more explicit

As I mentioned the far more common case of locking is done against a variable: a local or field.

lock (guard) { 

In that case the compat fix, casting to object, is very explicit and easy to identify

lock ((object)guard)

As described, it's one accidental behavior that has an accidental fallback; a pit of failure in a pit of failure.

I don't agree this is an accidental fallback, it's a very deliberate one. This new behavior is entirely about API matching. Code that wants to take advantage of that needs to be careful to maintain the correct type.

The alternative is to essentially provide a diagnostic whenever a lockable type is cast to a base type or interface. That seems quite extreme and prevents these types from having other useful behaviors. Customers who want to use the new locking pattern and are warry of this problem can simply implement it on a ref struct.

HaloFour commented 1 year ago

In that case the compat fix, casting to object, is very explicit and easy to identify

Except when the cast (or generic code) is far removed from the lock keyword. That's where I have a problem with it, an accidental case of falling back to Monitor.

Admittedly, as with Monitor-based locks, I'd think it'd be somewhat uncommon for these instances to be passed around, so maybe the concern is somewhat minimal.

The alternative is to essentially provide a diagnostic whenever a lockable type is cast to a base type or interface.

If the fallback is explicitly intended to be used as described then the diagnostic would only ever apply when passing the Lock instance somewhere it probably shouldn't go, which sounds like a good use case for an analyzer anyway. For anyone who wants those "other useful behaviors", they can silence the analyzer.

Customers who want to use the new locking pattern and are warry of this problem can simply implement it on a ref struct.

Customers aren't writing their own lock types, they're relying on the myriad of types provided by the BCL, none of which are ref struct or have been deemed important enough to warrant language support. It'll be really confusing as to why lock works with System.Threading.Lock but doesn't work with System.Threading.ReaderWriterLock or any of the other lock types, nor could any of those locks be "updated" without breaking a ton of existing code.

timcassell commented 1 year ago

It'll be really confusing as to why lock works with System.Threading.Lock but doesn't work with System.Threading.ReaderWriterLock or any of the other lock types, nor could any of those locks be "updated" without breaking a ton of existing code.

I think existing locks could be "updated" by providing an extension that returns a light wrapper that implements the new API. That would be required to work with ReaderWriterLock anyway.

lock (rwl.ReaderLock()) { }
lock (rwl.WriterLock()) { }
lock (rwl.UpgradeableReaderLock()) { }

And perhaps even do the same thing with object, and add an analyzer suggestion to convert existing locks to the extension:

lock (obj.MonitorLock()) { }
HaloFour commented 1 year ago

@timcassell

I think existing locks could be "updated" by providing an extension that returns a light wrapper that implements the new API.

Yes, although that suffers from the problem in that if you forget to bring that extension method into scope you'll accidentally fallback to Monitor again. Feels like you'd want analyzers that would also detect Monitor locks against common lock classes to warn of such.

And perhaps even do the same thing with object, and add an analyzer suggestion to convert existing locks to the extension:

I kind of wish we could deprecate the current behavior with lock and also put it behind this pattern, but even if that could be done with zero overhead I kind of doubt that there would be any appetite to do it.

timcassell commented 1 year ago

Yes, although that suffers from the problem in that if you forget to bring that extension method into scope you'll accidentally fallback to Monitor again.

Just add them directly as members to those types, then. The analyzer should warn against any lock that uses the old style Monitor, whether those are implemented as extensions or type members either way.

Feels like you'd want analyzers that would also detect Monitor locks against common lock classes to warn of such.

Absolutely! Just do it with any type that implements the lock pattern (or contains an instance method or extension that returns a type that implements the lock pattern).

And perhaps even do the same thing with object, and add an analyzer suggestion to convert existing locks to the extension:

I kind of wish we could deprecate the current behavior with lock and also put it behind this pattern, but even if that could be done with zero overhead I kind of doubt that there would be any appetite to do it.

Why not? It seems like most people here are worried about compatibility issues, not that they don't want the pattern updated. I think an analyzer warning should fix that. Even if a System.Threading.Lock instance is passed around as a System.Object, with the new pattern, any lock (state) { } will warn, so you must explicitly state how you want the lock to behave.

HaloFour commented 1 year ago

@timcassell

Just add them directly as members to those types, then.

That'd be up to the BCL team, and that would potentially change the meaning of existing code.

Absolutely! Just do it with any type that implements the lock pattern.

If the type already implemented the lock pattern then the fallback to Monitor would no longer be a risk.

Why not? It seems like most people here are worried about compatibility issues, not that they don't want the pattern updated.

Right, that due to the existing Monitor behavior that any accidental miss of the lock pattern would still be valid code that would compile, leading to subtle bugs at runtime. I'd be game for a more explicit approach, such as the one that you're describing, which would discourage the current behavior.

timcassell commented 1 year ago

Another thing, could the lock pattern be updated to be able to use the returned disposable? Like this with a using statement:

using (var key = _lock.EnterLockWithHolder())
{
    // do something with key
}

Maybe something like this?

lock (_lock; var key)
{
    // do something with key
}
jaredpar commented 1 year ago

If the fallback is explicitly intended to be used as described then the diagnostic would only ever apply when passing the Lock instance somewhere it probably shouldn't go, ...

Disagree. As I mentioned, this diagnostic would fire when you cast a lock instance into an interface it implemented. I would not classify that as somewhere it probably shouldn't go. The author implemented the interface, it should be a valid target. Further such a diagnostic can't even be completely accurate. It won't catch cases like the following:

object SneakyCast<T>(T t) => (object)t;

Customers aren't writing their own lock types, they're relying on the myriad of types provided by the BCL, none of which are ref struct or have been deemed important enough to warrant language support.

It's fairly trivial to wrap the BCL types in a ref struct should a customer want that level of protection

HaloFour commented 1 year ago

@jaredpar

It's fairly trivial to wrap the BCL types in a ref struct should a customer want that level of protection

The customers sophisticated enough to know to do that are not the customers I would be worried about.

TahirAhmadov commented 1 year ago

I think even if the incident rate of misusing lock is low, the fact that we're going to have one statement do two different things, is going to prove confusing over time. When you read code, every lock can be one of two things, unless you inspect the type of the expression.

timcassell commented 1 year ago

@jaredpar Why would you warn on a cast? Just warn when the lock statement is used on an object that doesn't implement the lock pattern.

I think even if the incident rate of misusing lock is low, the fact that we're going to have one statement do two different things, is going to prove confusing over time. When you read code, every lock can be one of two things, unless you inspect the type of the expression.

@TahirAhmadov I would be fine with a new keyword. But lock is such a nice keyword. 😞

Joe4evr commented 1 year ago

I suggested this in the thread about the Lock type but what if pattern-based locking is done with using lock? This would be an error if the supplied object doesn't match the required shape (good diagnostic that the object in question doesn't support it, instead of silently falling back to Monitor), and Monitor-based locking is as simple as leaving off the using.

timcassell commented 1 year ago

I suggested this in the thread about the Lock type but what if pattern-based locking is done with using lock? This would be an error if the supplied object doesn't match the required shape (good diagnostic that the object in question doesn't support it, instead of silently falling back to Monitor), and Monitor-based locking is as simple as leaving off the using.

That's an interesting idea, but I think that leads to another pit of failure if one forgets to add using before lock. I think warning on existing lock usage is clearer (even if we still use using lock and just deprecate lock entirely).

tfenise commented 1 year ago

If developers have to write lock (rwl.ReaderLock()) { } or lock (obj.MonitorLock()) { } or using lock (_lock) { } to avoid any ambiguity and confusion, which are not really more concise than using (myLock.EnterLock()), I think it would be better not to introduce a new language feature, but just have an analyzer to warn on lock (_lock) where _lock is a System.Threading.Lock to deal with developers mistakenly thinking a System.Threading.Lock may be used with lock.

TahirAhmadov commented 1 year ago

Now that we're taking a look at lock, we should also try to make the new surface-area based version work in async methods.

PS. Actually this is one more reason not to "reuse" lock - the Monitor doesn't work if await exists inside of the code block, but with a "Lock" type, await could be possible. So in the future, the compiler would have to check the type that's passed in to lock() and allow/prohibit await in the code block. Sounds weird to me...

HaloFour commented 1 year ago

@TahirAhmadov

Now that we're taking a look at lock, we should also try to make the new surface-area based version work in async methods.

If we go there I think we should have separate pattern(s) that indicate whether a particular lock can work in async methods. It's very common for locks to rely on TLS for bookkeeping which would not be safe.

TahirAhmadov commented 1 year ago

If we go there I think we should have separate pattern(s) that indicate whether a particular lock can work in async methods. It's very common for locks to rely on TLS for bookkeeping which would not be safe.

Perhaps await lock (or my preference await sync?) would demand Task<LockHolder> EnterLockWithHolderAsync() in exchange for allowing await ... in the code block, and those types are "expected" to not rely on TLS?

PS. Come to think of it, this is the first concrete reason that I can think of (other than "syntactic sugar" and elegance) to use a special construct and not just using(...) { ... } - the ability to differentiate between async and regular locking.

jaredpar commented 1 year ago

Why would you warn on a cast? Just warn when the lock statement is used on an object that doesn't implement the lock pattern.

That would cause 100% of existing lock statements to issue a diagnostic. That's not a suitable trade off here. Pretty much every non-trivial customer code base would get new warnings on upgrade and the dissat from that would be extremely high.

TahirAhmadov commented 1 year ago

@timcassell

@jaredpar Why would you warn on a cast? Just warn when the lock statement is used on an object that doesn't implement the lock pattern.

@TahirAhmadov I would be fine with a new keyword. But lock is such a nice keyword. 😞

One approach is keep lock as is, and warn when lock is used with a type which fits the lock-type surface area. The idea would be to remind the developer to use using for these locker types. This is what I think some others mean when they're talking about adding an analyzer. Another approach (which I think you're talking about) is to endeavor on a full transition from the lock/Monitor approach, and push the ecosystem to use lock/"locker type" approach. The benefit of this is we get rid of the legacy, and keep the lock keyword; but the cost is pretty high - literally this is a big breaking change, comparable to .NET Core and NRTs. And if we compare the cost/benefit, the extra benefit of continuing to use the lock keyword but with different semantics going forward (which would be nice, admittedly) comes at a very high cost of forcing users to change millions of lines of code.

timcassell commented 1 year ago

That would cause 100% of existing lock statements to issue a diagnostic. That's not a suitable trade off here. Pretty much every non-trivial customer code base would get new warnings on upgrade and the dissat from that would be extremely high.

It's fairly trivial to disable warnings. I don't think that should be a show-stopper.

literally this is a big breaking change, comparable to .NET Core and NRTs.

Right, and just like NRTs, it could be disabled by default until it's ready to be enabled by default.

timcassell commented 1 year ago

Perhaps await lock (or my preference await sync?) would demand Task<LockHolder> EnterLockWithHolderAsync() in exchange for allowing await ... in the code block, and those types are "expected" to not rely on TLS?

Yes, but use the awaitable pattern, not just Task<>.

TahirAhmadov commented 1 year ago

@timcassell

It's fairly trivial to disable warnings. I don't think that should be a show-stopper.

Right, and just like NRTs, it could be disabled by default until it's ready to be enabled by default.

The problem is that this is effectively a new language dialect, and it's a huge cost just for the benefit of keeping the lock keyword.

Yes, but use the awaitable pattern, not just Task<>.

Yes of course.

TahirAhmadov commented 1 year ago

How about this? If we implement these steps over a few versions, I think it can be a viable transition plan:

  1. v13: A new warning is added when a "locker type" is used in a lock statement;
  2. v13 (or v14): A new syntax is introduced, something like aforementioned using lock(...) { ... } or maybe lock(using ...) { ... }, which works with "locker types" (and potentially the await counterparts);

At this point, there are 2 options: the good old lock(...) { ... } with Monitor, and the new lock(using ...) { ... } with "locker types". Admittedly, the following steps are a bit far fetched:

  1. v14 (or v15): A new warning is added when the old lock(...) { ... } is used (relying on Monitor), instructing users to use one of the new "locker types", as needed;
  2. v15 (or v16): The lock(...) { ... } is made an error and no longer a valid language construct;
  3. v16 (or v17): The using in lock (using ...) { ... } is made optional, and a fixer is introduced to remove it.

Now, we have a unified approach: lock(...) { ... } with "locker types", and no warts.

timcassell commented 1 year ago

I don't like the idea of making it an error to use the keyword, then later making it not an error. Especially since I maintain a library that spans multiple language versions. At that point, we may as well just retire the lock keyword permanently (as a warning, not an error), and either introduce a new keyword (sync as you suggested), or encourage the using pattern for locks (already supported by the new lock type, but it doesn't prevent awaits inside the lock body).

kouvel commented 1 year ago

Updated proposal based on API review in https://github.com/dotnet/runtime/issues/34812#issuecomment-1503911687.

brantburnett commented 1 year ago

Everything compiles even though the spelling EntersLockWithHolder is wrong and lock(lo) does not do what I intended.

What if the runtime proposal were to include an interface, something like System.Threading.ILock. While the interface is not required for the C# compiler, it could help ensure that custom implementations correctly follow the pattern. Much like IDisposable is not required for a using clause but it's nice to have.

HaloFour commented 1 year ago

Much like IDisposable is not required for a using clause but it's nice to have.

Nitpick, IDisposable is required for the using pattern, except where the target type is a ref struct (which cannot implement interfaces). You may be thinking of foreach/IEnumerable<T>?

KalleOlaviNiemitalo commented 1 year ago

Is there going to be a compile-time warning for

class C {
    void M<T>(T val) where T : class {
        lock(val) {}
    }
}

where the C# compiler cannot know that T has a EnterLockScope method, and thus generates code that calls Monitor?

CyrusNajmabadi commented 1 year ago

If T is a class, then the above is completely reasonable (and correct) code today. It would likely be bad if we broke that :)

kouvel commented 1 year ago

where the C# compiler cannot know that T has a EnterLockScope method, and thus generates code that calls Monitor?

Good point, yes I think there should be an analyzer warning for this too. I was generally thinking that there could be an analyzer warning for when a type that matches the lock pattern is implicitly casted to a type that does not match the pattern. In this case since T doesn't match the pattern, then it should be flagged when a call is made to the method where the parameter is of type Lock. Something like that anyway.

It may also be useful to have an interface for the lock pattern, such that the analyzer can use that as a trigger and it could be applied to any type that implements the interface.

Xyncgas commented 1 year ago

Cool idea, if you use the object you first need to acquire it and that locks it for you. Is there asynclock alternative or lock is blocking the current thread in C#, because if so I would rather not use async lock is inside .NET right? getting response from network makes a call to the coroutine that was suspended with its previous state, should already be possible in .NET

Meanwhile there's alternative for example SemaphoreSlim.WaitAsync() Yet, if we already have lock and it works in every scenario because it has a very generic name lock, why do I need to use semaphore

gfraiteur commented 1 year ago

I think it's a dangerous direction to take for a language to invest in purpose-specific constructs like lock. What if we want a begin/commit/rollback transaction pattern instead of an acquire/release pattern? Will you add a keyword for this?

You see that there are similar issues with .NET events: they are based on a design pattern from the late 1990s, and it was noticed later that the design pattern was suboptimal. However, it was baked in the language and the core framework.

I think a language should be as agnostic about patterns as it can.

Alternative designs are:

These designs are more flexible and do not bind the language to peculiar needs that may seem obsolete in a few years.

TonyValenti commented 1 year ago

It would be really nice if the lock syntax was similar to the new using syntax so I could simply say:

lock myLock;

And have it lock till the end of the scope. Instead of:

lock(myLock) {

}

If I want a custom scope, then: { lock myLock; }