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

kouvel commented 11 months ago

Updated the proposal in the top post to reflect some recent discussions.

TahirAhmadov commented 11 months ago

IMO this is just too complicated. lock can do one of two things, but it can only do the second thing if everything is just right, and then we have to add errors and warnings to communicate to the developer "how wrong" the second thing is being attempted.

All of this becomes much easier and straightforward with new syntax for the ILockPattern like sync(x) or using lock(x) or lock (using x). Then the compatibility is very clearly defined, and it's a simple yes or no question - compiles if yes, errors if not. (The new syntax is not even listed as an alternative...)

Also, I'm not sure how using a marker interface helps with inheritance. If the public surface area of a class supports the new pattern, how can child classes become "incompatible"? What does the interface add here? WRT excluding extensions - this may be valid, but it has to be noted it introduces a new quirk - for enumerables ans tasks extensions are included, but here it's proposed that they are not - definitely an inconsistency - I hope there is a good reason for it.

brantburnett commented 11 months ago

I want to raise one more concern that I don't think has been mentioned, which is that the behavior is call-site specific at compile-time, not run time. Imagine this scenario:

In this scenario, Library B and Application C will be taking different, non-exclusive locks on instances of Foo. This is because the recompiled Application C will see the new members and take locks using EnterLockScope while Library B will still be calling Monitor.TryEnter.

Of course, this pattern of public locking is generally not recommended, but it doesn't mean it isn't being done.

kouvel commented 11 months ago

All of this becomes much easier and straightforward with new syntax for the ILockPattern like sync(x) or using lock(x) or lock (using x).

Possibly, added as an alternative.

Also, I'm not sure how using a marker interface helps with inheritance. If the public surface area of a class supports the new pattern, how can child classes become "incompatible"? What does the interface add here?

Implementing an interface ensures that all derived types also implement the interface. Using an attribute instead would probably need additional checks on derived types to see if any base classes have the attribute to determine if it matches the lock pattern, or an additional rule that would require all derived types to also have the attribute, for consistency in locking behavior.

WRT excluding extensions - this may be valid, but it has to be noted it introduces a new quirk - for enumerables ans tasks extensions are included, but here it's proposed that they are not - definitely an inconsistency - I hope there is a good reason for it.

I just meant for the EnterLockScope method, that it can't be an extension method as it's sort of like an interface method, and to prevent different locking behaviors depending on whether the extension methods are available.

In this scenario, Library B and Application C will be taking different, non-exclusive locks on instances of Foo.

Yes it would be a breaking change to opt into the lock pattern for existing reference types. Added another note to the drawbacks.

TahirAhmadov commented 11 months ago

Implementing an interface ensures that all derived types also implement the interface. Using an attribute instead would probably need additional checks on derived types to see if any base classes have the attribute to determine if it matches the lock pattern, or an additional rule that would require all derived types to also have the attribute, for consistency in locking behavior.

Oh I see, I guess I missed the part where the other option was using an attribute - an interface is indeed better. I assume just relying on public surface area (like foreach) isn't acceptable here for some reason?

I just meant for the EnterLockScope method, that it can't be an extension method as it's sort of like an interface method, and to prevent different locking behaviors depending on whether the extension methods are available.

That makes sense if we try re-purposing the existing lock, but if we go with a separate syntax, then there is no longer an impediment to including extensions - or is there?

PS. Thanks for taking the time to read and consider my and others' feedback.

kouvel commented 11 months ago

I assume just relying on public surface area (like foreach) isn't acceptable here for some reason?

foreach is just a shortcut to a more verbose variant that does the same thing, all based on the same documented behavior. The lock keyword has specific behaviors currently and adding new behaviors appears to be a challenge, esp. in avoiding unintentional behaviors. A new syntax would likely simplify the problem, while perhaps adding some ambiguity between the current syntax with lock and a new syntax for the lock pattern.

That makes sense if we try re-purposing the existing lock, but if we go with a separate syntax, then there is no longer an impediment to including extensions - or is there?

It would probably be fine to allow extension methods with a new syntax because when they are not available it would fail to compile, so there wouldn't be any ambiguity.

PS. Thanks for taking the time to read and consider my and others' feedback.

Thanks for that, much appreciated!

TonyValenti commented 11 months ago

I kind of think this should be left alone and not implemented. I use @stephencleary 's async locking and don't find using IDisposables burdensome at all. I actually prefer it over the existing lock statement because it doesn't require parenthesis or a nested scope.

kouvel commented 11 months ago

It would probably be fine to allow extension methods with a new syntax because when they are not available it would fail to compile, so there wouldn't be any ambiguity.

That said, thinking about it more, it's remotely possible for two or more different behaviors that satisfy the lock pattern to exist simultaneously. Given that, I think extension methods need to be excluded.

kouvel commented 11 months ago

I kind of think this should be left alone and not implemented.

I don't disagree, I actually see more things that bother me than clean ways to make this happen. On one hand, if the integration were not to happen, the lock statement would not work with the Lock type, which is rather odd. On the other hand, lots have been mentioned and discussed about the drawbacks of this type of proposal. In any case I think it's worthy of a discussion.

TahirAhmadov commented 11 months ago

That said, thinking about it more, it's remotely possible for two or more different behaviors that satisfy the lock pattern to exist simultaneously. Given that, I think extension methods need to be excluded.

Isn't that also a potential problem with instance methods?

On one hand, if the integration were not to happen, the lock statement would not work with the Lock type, which is rather odd.

If we were to not change the lock statement to handle the new type/pattern, it should at least be changed to produce a warning that it's using the Monitor - perhaps as an analyzer shipped together with the new Lock type.

HaloFour commented 11 months ago

@kouvel

On one hand, if the integration were not to happen, the lock statement would not work with the Lock type, which is rather odd.

The lock statement already doesn't work with the variety of lock types provided in the BCL. Supporting one but not the rest would, if anything else, be a lot more confusing.

kouvel commented 11 months ago

Isn't that also a potential problem with instance methods?

If you are referring to derived types changing the behavior by implementing a different behavior for the lock pattern API, then yes, but that's a clear choice and not likely to be an accident, and at that point all bets are off. I might have missed your point though, can you clarify?

If we were to not change the lock statement to handle the new type/pattern, it should at least be changed to produce a warning that it's using the Monitor - perhaps as an analyzer shipped together with the new Lock type.

It seems difficult to spec-out an analyzer or compiler warning behavior to differentiate between a type that is ok to use with the lock keyword and one that isn't, without some of the other suggested things in this proposal. Some of those cases may not even be determinable statically unless there are protections that prevent it.

kouvel commented 11 months ago

The lock statement already doesn't work with the variety of lock types provided in the BCL. Supporting one but not the rest would, if anything else, be a lot more confusing.

The lock keyword is currently very limited in scope, and for good reason. There would likely not be many types that would even want to opt in to these behaviors. SpinLock may be one of them, but even for that there is a drawback.

HaloFour commented 11 months ago

@kouvel

The lock keyword is currently very limited in scope, and for good reason. There would likely not be many types that would even want to opt in to these behaviors. SpinLock may be one of them, but even for that there is a drawback.

That's kinda my point. Someone who learns how to use lock with Lock might be quite surprised when it doesn't work as they might expect with ReaderWriterLock or Mutex. I believe I'm just repeating the points that others are making, though.

TahirAhmadov commented 11 months ago

If you are referring to derived types changing the behavior by implementing a different behavior for the lock pattern API, then yes, but that's a clear choice and not likely to be an accident, and at that point all bets are off. I might have missed your point though, can you clarify?

I think I understand what you mean - it's impossible to overload on return type in the same type, but possible with extensions and in child classes. I tend to agree with excluding extensions more than before now.

It seems difficult to spec-out an analyzer or compiler warning behavior to differentiate between a type that is ok to use with the lock keyword and one that isn't, without some of the other suggested things in this proposal. Some of those cases may not even be determinable statically unless there are protections that prevent it.

I was thinking something really easy and straightforward - if it is an analyzer, just warn on certain types - of which the analyzer, having been shipped together with the types, should be aware of. Or am I missing anything?

I still think this area is worth exploring and a new syntax would be great to natively support these new types. Meaning, I'm not advocating for doing nothing here.

kouvel commented 11 months ago

I was thinking something really easy and straightforward - if it is an analyzer, just warn on certain types - of which the analyzer, having been shipped together with the types, should be aware of. Or am I missing anything?

If we were to go down that road, I think a better option would be one of the alternatives mentioned in the proposal, for the compiler to treat the Lock type differently and special-case it.

jjonescz commented 11 months ago
  • If a value of type L is used with the lock keyword, or a value of type S is used with the using keyword, and the block contains an await, it would result in an error

How would this work for S exactly? How can compiler recognize that a value is of type S? Currently S seems to be defined as anything that qualifies for use with the using keyword which is too broad for the warning. Do you suggest looking for all references of S to see whether they match pattern for L? Or to only emit the error if the using is directly on the call to EnterLockScope? (Feels brittle as it could break with some casting or extracting to a variable.)

  • If a value of type L is implicitly or explicitly casted to another type that does not match the lock pattern (including generic types), it would result in a warning

What is the motivation for this? I see it could make sense for complex expressions like lock ((object)lock1 ?? lock2) but there aren't many details so I assume the warning only covers casts that are directly locked, like lock ((T)variable). But the cast there seems redundant. Except maybe to opt-out by casting a value of type L to object, for example, so the normal Monitor-style lock would be used for it, but then the warning seems unnecessary.

tfenise commented 11 months ago

Implementing an interface ensures that all derived types also implement the interface. Using an attribute instead would probably need additional checks on derived types to see if any base classes have the attribute to determine if it matches the lock pattern, or an additional rule that would require all derived types to also have the attribute, for consistency in locking behavior.

Doesn't AttributeUsageAttribute.Inherited = true solve the problem regarding using an attribute and inheritance?

kouvel commented 11 months ago

How would this work for S exactly? How can compiler recognize that a value is of type S? Currently S seems to be defined as anything that qualifies for use with the using keyword which is too broad for the warning. Do you suggest looking for all references of S to see whether they match pattern for L? Or to only emit the error if the using is directly on the call to EnterLockScope? (Feels brittle as it could break with some casting or extracting to a variable.)

I meant that S would only be the return type of L.EnterLockScope as in:

  • L has an accessible instance method S EnterLockScope() that returns a value of type S (Lock.Scope in this example).

I was imagining that it would be possible to track S vars as they are created by calls to EnterLockScope on a type L that matches the lock pattern, then when an S var is used with a using, the check could be triggered.

What is the motivation for this? I see it could make sense for complex expressions like lock ((object)lock1 ?? lock2) but there aren't many details so I assume the warning only covers casts that are directly locked, like lock ((T)variable). But the cast there seems redundant. Except maybe to opt-out by casting a value of type L to object, for example, so the normal Monitor-style lock would be used for it, but then the warning seems unnecessary.

I meant that any cast of a type L that matches the lock pattern to another type that doesn't match the pattern would be an error, such that it would be impossible to ever have lock (lockVar) where the actual type of lockVar is L that matches the lock pattern, but the static type of lockVar in the context is some other type that does not match the lock pattern. If that were possible, it could lead to accidental usage of Monitor, and this was intended to prevent that.

jjonescz commented 11 months ago

I was imagining that it would be possible to track S vars as they are created by calls to EnterLockScope on a type L that matches the lock pattern, then when an S var is used with a using, the check could be triggered.

Looks like quite complicated analysis for the compiler to perform just for this error. The compiler also cannot determine this in 100% cases, so the error reporting would not be perfect - for example if you get the S as public method argument.

such that it would be impossible to ever have lock (lockVar) where the actual type of lockVar is L that matches the lock pattern, but the static type of lockVar in the context is some other type that does not match the lock pattern

The compiler only knows the static type of lockVar, though, it cannot know the actual type in all cases (again for example if you get lockVar as an argument of some base type of L).

kouvel commented 11 months ago

Looks like quite complicated analysis for the compiler to perform just for this error. The compiler also cannot determine this in 100% cases, so the error reporting would not be perfect - for example if you get the S as public method argument.

The lock statement's block currently blocks awaits. I was hoping it would be something similar to that, I don't think it's necessary to be 100%, just to catch the obvious in-method cases where a lock is being used around an await similarly to the lock statement's analysis. I guess it would involve a bit more tracking though. Given that we are expecting that locks would be used like using (_lock.EnterLockScope()) or equivalently with this proposal lock (_lock), without actually holding S in a var in code, it could even be limited to catching just those if that makes it simpler.

kouvel commented 11 months ago

The compiler only knows the static type of lockVar, though, it cannot know the actual type in all cases (again for example if you get lockVar as an argument of some base type of L).

The compiler should only need to know the static type of lockVar, and the error would be such that the static type would be sufficient to have the expected locking behavior be generated. That is, it would not be possible for the actual type to match the lock pattern and the static type to not match the lock pattern since any such cast would be an error.

kouvel commented 11 months ago

Doesn't AttributeUsageAttribute.Inherited = true solve the problem regarding using an attribute and inheritance?

It seems this attribute is only applicable to classes, which would reduce the scope of where the lock pattern could apply. Nevermind, I seem to have misunderstood it, this could work too with an attribute instead of a marker interface. The inheritance doesn't seem to work with interfaces though, that is if an interface has the attribute with Inherited = true, a class that implements the interface doesn't inherit the attribute.

TahirAhmadov commented 11 months ago

If we were to go down that road, I think a better option would be one of the alternatives mentioned in the proposal, for the compiler to treat the Lock type differently and special-case it.

That approach would make it easier for the compiler to determine which behavior lock should take, but there would still remain the smaller issue of readability - when looking at code, it's not immediately obvious what lock is doing - without checking the type of the expression in the parenthesis.

MichalPetryka commented 11 months ago

it's impossible to overload on return type in the same type

It is possible in IL for all methods (and properties and fields even), in C# currently only operators use it but I don't think there's any guarantee nothing else will emit it in the future?

Joe4evr commented 10 months ago

it's impossible to overload on return type in the same type

It is possible in IL for all methods (and properties and fields even), in C# currently only operators use it but I don't think there's any guarantee nothing else will emit it in the future?

Not to mention that if BinaryCompatOnlyAttribute ends up happening, C# may start allowing return type overloading limited to "of all candidates that differ only by the return type, there can be no more than one that is not annotated with BinaryCompatOnlyAttribute" (that sounds close enough to the appropriate spec'ese, at least).

TonyValenti commented 10 months ago

This would be awesome!

sab39 commented 9 months ago

Here's an idea that might potentially make for a convenient opt-in transition away from the old meaning of the lock statement, without needing a language-level sledgehammer like #nullable or any new language feature at all (other than the change to lock itself of course), although it would require a small change by the BCL people, and still doesn't solve the elephant-in-the-room issue with runtime types.

The starting point is the same idea discussed at length above: define lock (s) {...} to mean using (s.EnterLockScope()) {...} if the compiletime type of s matches the lock pattern, or to use the existing meaning otherwise. The key to this specific idea is that we do allow EnterLockScope to be an extension method.

The other key aspect is that the new lock type in the BCL lives in a new namespace - System.Threading.Locking.Lock instead of System.Threading.Lock. And in the same namespace is another class LockingExtensions which defines

[Obsolete]
public static ObjectLockScope EnterLockScope(this object o) { ... }

providing the new API pattern as a wrapper backed by the existing Monitor-based implementation.

This would mean that:

Edit: on second thought that last one might not be quite as ridiculous as I thought, it might be a helpful workaround for projects that want to make this migration but for some other reason can't turn all Obsolete warnings into errors.

sab39 commented 9 months ago

One way to approach the runtime type problem might be to have the new Lock type Monitor.Enter itself on construction. That wouldn't prevent code from compiling and trying to use it as a lock the old way, but it would prevent it from ever successfully obtaining that lock, and block indefinitely instead. That would allow finding any such issues pretty quickly in testing, and crucially, would not allow the lock to be silently violated by having old and new code trying to grab it different ways.

TahirAhmadov commented 9 months ago

@sab39 this is a great idea. One clarification - we would want to put the new Lock type's EnterLockScope method into an extension, too, right? This is because a Lock variable can be in scope - say, a field defined on the type in another partial file - but the namespace is not using on top in this partial file.

sab39 commented 9 months ago

@TahirAhmadov No, I'd say we definitely don't want the new type's EnterLockScope to be an extension - otherwise code that tried to lock it without importing the namespace would silently get the old behavior, which is absolutely not what we want for the new lock type. The idea is that "real" new-style lock types will always get their "real" lock methods called (because extension methods can't supersede real ones), including having that behavior picked up by existing code on recompilation. That way code that does nothing but consume locks provided by another API will lock them the right way, whatever kind of lock they are.

tl;dr - if the namespace is in scope:

if the namespace is not in scope:

TahirAhmadov commented 9 months ago

OK I think I see what you mean. It's a little difficult to wrap my head around it because it's a little complicated :) Basically the really big difference is the global using (it almost becomes a language dialect at this point), because individual using per file is almost like #nullable enable in that file. Wouldn't it be more "natural" to just introduce an analyzer which would warn on lock(object)?

sab39 commented 9 months ago

@TahirAhmadov I guess it is kind of like a language dialect, but done in a way that the behavior doesn't change, only the warning, and built purely on existing language constructs. It'd certainly be possible to do with an analyzer, but that presumes that every developer is aware of the analyzer and knows to turn it on; my approach will kind of automatically activate the analyzer based on the heuristic of having the new namespace in scope, which is a pretty good proxy for "code that's actively using the new locking system", and turns out to give quite a lot of fine grained control of where the analyzer should be active and how severe the errors should be both globally and on a per-file basis, without needing to add all that flexibility into the analyzer or remember to activate the analyzer in the first place.

Mr0N commented 9 months ago

https://sharplab.io/#gist:1dee0bd0e21c175c239d6cdf9658a40f image Perhaps this structure can be replaced with a class because structures have the property of being cloned arbitrarily, which can be problematic in "multi-threading" situations.

Mr0N commented 9 months ago

can replace that pattern with such an interface.

image

public interface ILockPattern<T> where T:IDisposable
{ 
     public T EnterLockScope();
}
Mr0N commented 9 months ago

class AsyncLock : ILockPattern<Lock>
{
    public Lock EnterLockScope()
    {
        return new Lock();
    }
}

class Lock : ILock
{
    public void Dispose()
    {

    }
}

public interface ILockPattern<T> where T : ILock
{
    public T EnterLockScope();
}
public interface ILock : IDisposable
{

}
jaredpar commented 9 months ago

Perhaps this structure can be replaced with a class because structures have the property of being cloned arbitrarily, which can be problematic in "multi-threading" situations.

The type in question here is a ref struct so while it can indeed be cloned arbitrarily it can only be done so on the current thread. It cannot be placed in the heap hence doesn't suffer from race conditions on access. The other reason for struct vs. class here is avoiding an allocation on lock enter.

Xyncgas commented 9 months ago

Basically a lock that looks like this I guess :

    let mutable lock = 0
    let myFunction () =
        if Interlocked.CompareExchange(&lock, 1, lock) = 0 then
            ....
            Interlocked.CompareExchange(&lock, 0, lock)

The type in question here is a ref struct

Why do we use ref struct for locks they live in a thread only

elachlan commented 3 months ago

When is this expected to come out of preview?

jjonescz commented 3 months ago

When is this expected to come out of preview?

In C# 13 and .NET 9.

KennethHoff commented 3 months ago

To be specific, the new System.Threading.Lock type - and its special-cased interaction with the lock keyword - is expected in C#13.

The actual proposal - a lock pattern - is not coming, at least not in C# 13

julealgon commented 3 months ago

Is there any reason why we still use the lock keyword now that the language has using disposal blocks? Wouldn't it just be better to encourage developers to use the slightly more explicit using approach and mark the lock keyword as deprecated or something along these lines?

I assume when lock was introduced it made more sense because there was no "similarly simple" construct that would support the same logic, but now (unless I'm missing something obvious) it feels superfluous.

CyrusNajmabadi commented 3 months ago

Is there any reason why we still use the lock keyword now that the language has using disposal blocks?

Yes. The lock statement clearly and concisely indicates the intent. It is a region of mutual exclusion. A using block does not indicate that.

Wouldn't it just be better to encourage developers to use the slightly more explicit using approach and mark the lock keyword as deprecated or something along these lines?

We do not think so. The language has supported locking since v1. It is a widely used construct. We see no reason to move away from that

I assume when lock was introduced it made more sense because there was no "similarly simple" construct that would support the same logic, but now (unless I'm missing something obvious) it feels superfluous.

The language supported both lock and using Since v1. We have never felt that the latter supplants the former.

julealgon commented 3 months ago

Is there any reason why we still use the lock keyword now that the language has using disposal blocks?

Yes. The lock statement clearly and concisely indicates the intent. It is a region of mutual exclusion. A using block does not indicate that.

A using block indicates whatever the call indicates, IMHO.

using (lock.BeginMutualExclusionSection())
{
    ...
}

Would be even more explicit and obvious to me than a custom lock (lock) (inherently confusing and redundant) call.

But I guess this aspect is still subjective, so fair enough.

Wouldn't it just be better to encourage developers to use the slightly more explicit using approach and mark the lock keyword as deprecated or something along these lines?

We do not think so.

Fair enough.

The language has supported locking since v1.

Just because something is supported since X, doesn't mean it is inherently good.

It is a widely used construct.

Just because something is widely used doesn't mean it is good or that it should never be reconsidered.

We see no reason to move away from that

Again, fair enough... to me it's a shame, as it makes the language unnecessarily more complex/less orthogonal. It introduces a special case for something that clearly didn't need it.

I assume when lock was introduced it made more sense because there was no "similarly simple" construct that would support the same logic, but now (unless I'm missing something obvious) it feels superfluous.

The language supported both lock and using Since v1. We have never felt that the latter supplants the former.

Oh, my bad on this one. I thought the using block was introduced after C# 1. I stand corrected. In that case, looks like we just missed an opportunity to avoid introducing the lock keyword altogether. But I can see people used to that word in other languages could be put off at the time.

CyrusNajmabadi commented 3 months ago

A using block indicates whatever the call indicates, IMHO.

That's more complex and less clear to me than just the lock statement. A bread and butter statement with clear and simple meaning since 1.0.

Just because something is supported since X, doesn't mean it is inherently good.

I disagree. This is a good part of hte langauge that people have been using for 25 years. Removing support is just adding friction and complexity. Foolish consistencies, hobgoblins, and all that.

looks like we just missed an opportunity to avoid introducing the lock keyword altogether.

We didn't miss an opportunity. The possibility of using using was well understood. After all, these are both about nicer patterns around try/finally. It's an intentional and continued view that lock has significant value for the language and moving away from that would be a net negative.

CyrusNajmabadi commented 3 months ago

It introduces a special case for something that clearly didn't need it.

We didn't need using in the first place either. It was a special case for having a try/finally with a particular null+dispose check. But we still like it.

There are lots of special one-off lang features that are just sugar over other things. The vast majority of hte language is actually just that. Yes, we could go the route of other languages and whittle that away to a core kernel that you use for everything. But we actually view that as a negative. We think this features and sugar are the value and niceness that makes our language more pleasant and more desirable for many users.

brantburnett commented 3 months ago

It introduces a special case for something that clearly didn't need it.

I would also point out that lock does have additional semantics that are important, beyond what using offers. For example, in async methods you can't include await calls within the lock block because locks are thread-specific, but you can use using statements.

marchewek commented 2 weeks ago

Isn't it just a matter of convention? Some will prefer a more technical focus "I want lock and await lock to use different verbs, because they will work differently and I want this to be explicit" while others will prefer a more functional focus "I want lock and await lock to use the same verb, because I want to protect a critical section of my code regardless of technical differences between sync and async version". Personally I dislike the await using used for locking a critical section. Even if the former convention should stay in place, I would prefer some await lockasync to await using, because this is about locking a critical section - using a critical section single-use-lock is not as straightforward.

Mr0N commented 2 weeks ago
var obj = new Lock();
using(obj)
{

}
lock(obj)
{
}

I don't quite understand the meaning of such a construction. Essentially, the compiler will just replace using with lock, but a lot of additional information is needed.