dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.12k stars 4.7k forks source link

GC.Resurrect & Better Disposable support around ref struct #39146

Closed juliusfriedman closed 4 years ago

juliusfriedman commented 4 years ago

Background and Motivation

Sometimes you finalize an object and you change you mind, perhaps you should be using pools but that is a different issue.

Proposed API

Ideally also a base disposable class with full async support but is not required.

A newer this

See the interface

As well as the following methods.

public static class GC {
+bool Resurrect<T>(ref something) where T : unmanaged;
+bool Resurrect<T>(something) where T : class;
}

Usage Examples

Wait:
///....
var scope = new CommonDisposable()
Operation:
using(new CommonDisposable()){

}
Done:
GC.Resurrect(scope);
goto Operation;
GC.Resurrect<T>(ref something) where T : unmanaged;

and

GC.Resurrect<T>(something) where T : class;

Alternative Designs

ReRegisterForFinalize<T> same overloads

Pooling.

Risks

Low, better than people getting it wrong.

Dotnet-GitSync-Bot commented 4 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

juliusfriedman commented 4 years ago

cc @danmosemsft @jkotas

jkotas commented 4 years ago

I do not understand what would Object.Resurrect do. For example, how is it different from GC.ReRegisterForFinalize ?

juliusfriedman commented 4 years ago

It basically doesn't differ at all from GC.ReRegisterForFinalize I was just proposing an API which would do that work for the user with respect to the disposable instance (hence where the CommonDispoable comes in).

I also think that having the mechanism already in place for users of ref structs is important and that GC.ReRegisterForFinalize is not generic.

The pattern above would cover both ref structs and other implementer of the IDisposable where as today you can't effectively do it without inter-op I don't think. (GC.ReRegisterForFinalize on a ref struct etc)

danmoseley commented 4 years ago

@juliusfriedman an API proposal should explicitly list the proposed API diff please.

juliusfriedman commented 4 years ago

Sorry wasn't totally sure where to plop them at first.

stephentoub commented 4 years ago

I don't understand. A ref struct can't be finalizable, isn't subject to GC, etc; what does it have to do with resurrection? What does something that meets the unmanaged constraint have to do with resurrection?

And what benefit is there to ReRegisterForFinalize being generic?

juliusfriedman commented 4 years ago

I don't understand. A ref struct can't be finalizable, isn't subject to GC, etc; what does it have to do with resurrection? What does something that meets the unmanaged constraint have to do with resurrection?

And what benefit is there to ReRegisterForFinalize being generic?

It also can't implement Equals right?

When I treat a ref struct as a disposable tis Dispose method is called without boxing it and I believe in some cases that call will be made from the finalizer queue [needs citation] so I treat that at a place where this would be beneficial although one can already work around that by implementing an interface on the ref struct with release semantics right? Not on a ref struct though... (Which I guess I understand...) [now we have these invisible interfaces or shapes which the CLR just happens to know about...]

For the most common cases the benefit is simply not boxing.

GrabYourPitchforks commented 4 years ago

For the most common cases the benefit is simply not boxing.

Ref structs can never end up on the heap (and as a result can never end up in the finalizer queue), and they can't be used as the T inside a generic method. For example:

public ref struct MyRefStruct { }

public void DoSomething<T>() { /* no-op */ }

public void Main()
{
    DoSomething<MyRefStruct>(); // compiler error; can't use MyRefStruct as 'T'
}
stephentoub commented 4 years ago

I believe in some cases that call will be made from the finalizer queue

No. A ref struct can't live on the heap, it can't be boxed, it can't have a finalizer, it won't ever be in the finalizer queue.

juliusfriedman commented 4 years ago

For the most common cases the benefit is simply not boxing.

Ref structs can never end up on the heap (and as a result can never end up in the finalizer queue), and they can't be used as the T inside a generic method. For example:

public ref struct MyRefStruct { }

public void DoSomething<T>() { /* no-op */ }

public void Main()
{
    DoSomething<MyRefStruct>(); // compiler error; can't use MyRefStruct as 'T'
}

Well then I suppose my proposed API won't be needed or even beneficial for ref structs 👍

ATP the only benefit of the proposed API (for the class type) would be the non boxing of the object which I myself don't believe is needed in this scenario.

Also thank you for the clarification on the ref struct finalizer, not sure where I got that from...

juliusfriedman commented 4 years ago

I believe in some cases that call will be made from the finalizer queue

No. A ref struct can't live on the heap, it can't be boxed, it can't have a finalizer, it won't ever be in the finalizer queue.

noted, Thank you!

juliusfriedman commented 4 years ago

You guys are free to close this one, sorry for any detritus. (Still feel like the CommonDisposable and the IDisposed are important pieces here, especially when sharing resources within the ref structs) Disposable Ref Structs Article

In summary We can treat disposable ref structs as lightweight types that have A REAL destructor, known from C++. It will be executed as soon as the corresponding instance goes out of the scope of the underlying using statement (or enclosing scope in case of using declaration). For sure it is not a feature that will suddenly become extremely popular in writing regular, business-driven code. But few layers below, when writing high-performant low-level code, it is worth to know about such possibility!

The important distinction here is that a ref struct is never on the finalizer queue and therefor won't benefit from the API I proposed. ( I guess struct in general can't really get on the finalizer queue, please correct me if I am wrong as when it's boxed only the box would end up on the finalier queue and then noop if anything. )

Unfortunately that still doesn't solve the sharing of resources issue, e.g. I can only implement Dispose for IDisposable, not my own interface e.g IDisposed or any other variation.

Structs can't inherit either so that means I have to own a lot of repeated code (violation of DRY) or I can opt to use a generic struct to which I can still use ref to pass it around like a ref struct as well as have the ability to implement interfaces. (talk about splitting hairs) IStruct

Do with this issue as you will, close it / break it down into other issues etc.

I just had these thoughts while I was writing / reviewing some code and wanted to share my thoughts.

GrabYourPitchforks commented 4 years ago

I guess struct in general can't really get on the finalizer queue

Good question. It's not the Dispose method that adds something to the finalizer queue, but rather the existence of a Finalize method. I don't know if it's legal in .NET Core for a struct to have a finalizer. But assuming for a minute that it were legal, IMO it'd be truly bizarre if anybody were ever to do this.

juliusfriedman commented 4 years ago

I guess struct in general can't really get on the finalizer queue

Good question. It's not the Dispose method that adds something to the finalizer queue, but rather the existence of a Finalize method. I don't know if it's legal in .NET Core for a struct to have a finalizer. But assuming for a minute that it were legal, IMO it'd be truly bizarre if anybody were ever to do this.

Thank you.

I do not believe a struct can ever have a finalizer yet an object does which is where I got confused I guess. (ValueType)

I am also not sure if this is a C# limitation e.g. if in IL you can definite a struct with a finalizer... but I am fairly certain that no type of struct can have a finalizer as written from the C# compiler.

jkotas commented 4 years ago

Thank you for sharing the idea. I think it is best to close this one.

Joe4evr commented 4 years ago

I don't know if it's legal in .NET Core for a struct to have a finalizer.

@GrabYourPitchforks For the record, it isn't. I want to say this limitation exists since C#/.NET 1.0, but it would be great if I could find a reference to back that up.

juliusfriedman commented 4 years ago

I don't know if it's legal in .NET Core for a struct to have a finalizer.

@GrabYourPitchforks For the record, it isn't. I want to say this limitation exists since C#/.NET 1.0, but it would be great if I could find a reference to back that up.

Let me know if you ever find that reference.

jkotas commented 4 years ago

Runtime ignores finalizers on structs: https://github.com/dotnet/runtime/blob/225673de28e67caa6a7fb6cf275b566996fc1570/src/coreclr/src/vm/methodtablebuilder.cpp#L11173 .

It has been like that since forever. You can see the same check in SSCLI sources: https://github.com/SSCLI/sscli20_20060311/blob/master/clr/src/vm/methodtable.cpp#L1073