Avoid using long for disposed flag#13069
Conversation
|
Since most of systems is x64 I don't value in the change. |
surely there is value for x86 and arm32? |
| get | ||
| { | ||
| return Interlocked.Read(ref this._disposed) == 1; | ||
| return Interlocked.CompareExchange(ref this._disposed, 0, 0) == 1; |
There was a problem hiding this comment.
I am not sure why it is so important to do atomic read on each get. But this is old code and I'd rather not make logic changes. It seems like _disposed should be tagged 'volatile' if it is that sensitive.
Anyway, what I don't like about this is that most of the time _disposed is '0' and so presumably each get will result in a write operation. It would be better to test for the final value of '1', which only happens once, and avoid multiple writes.
return Interlocked.CompareExchange(ref this._disposed, 1, 1) == 1;
There was a problem hiding this comment.
@PaulHigin that code will overwrite the value with 1 on the first read.
EDIT: never mind, misunderstood what was happening here. Would recommend naming the arguments, though in this kind of case there's not a lot to be gleaned without reading over documentation.
There was a problem hiding this comment.
Replacement only occurs if the value and comparison value are equal. The _disposed value is '0' and the comparison value is '1'.
But this illustrates a serious downside to this change. It is difficult to read and reason about, and so difficult to maintain. There is not a large upside with this change, so I feel it should not be taken.
/cc @daxian-dbw
There was a problem hiding this comment.
Interlocked APIs are never fun to reason about. An additional comment should be added, I feel, and arguments named for clarity, but the change itself is sound IMO.
There was a problem hiding this comment.
@PaulHigin I don't think the atomic read using CompareExchange is necessary, it is just being used to ensure a full fence. volatile does not protect against a read after write so it is no use here.
There was a problem hiding this comment.
It is not just using Interlocked APIs, but using them in an unusual way. This makes the code hard to read and maintain. I agree that the atomic read is probably not necessary, but I also see no great harm in it. It is old code and there is no real upside in changing it. Therefore I won't approve the change and instead suggest the code be left as is.
/cc @daxian-dbw
There was a problem hiding this comment.
I had a think about this and we can eliminate the dummy CompareExchange by using an explicit memory barrier (see 7fc1f28). This no longer relies upon side effects from CompareExchange.
This code is not in a hot way. We won’t get any winnings. |
| get | ||
| { | ||
| return Interlocked.CompareExchange(ref this._disposed, 0, 0) == 1; | ||
| Interlocked.MemoryBarrier(); |
There was a problem hiding this comment.
This method has a different purpose.
There was a problem hiding this comment.
Right, but since _disposed is now Int32 atomicity is guaranteed and we don't need this method.
However, a side effect of CompareExchange is to provide a memory barrier, for clarity's sake we can use an explicit barrier rather then rely on the side effect.
There was a problem hiding this comment.
@xtqqczze it might be a good idea to add a comment there to explain what that actually is being used to ensure if you can. I checked the documentation and the description for that is... obtuse at best, though I think I have an OK understanding of what it's doing.
There was a problem hiding this comment.
Right, but since _disposed is now Int32 atomicity is guaranteed and we don't need this method.
If Int32 is atomicity we could remove all Interlocked.* in the code and use volatile.
There was a problem hiding this comment.
Right, but since _disposed is now Int32 atomicity is guaranteed and we don't need this method.
If Int32 is atomicity we could remove all Interlocked.* in the code and use
volatile.
Atomicity is guaranteed for a single read operation. CompareExchange is needed for atomic read-modify-write.
I don't think replacing Interlocked.* with volatile would be a good idea.
daxian-dbw
left a comment
There was a problem hiding this comment.
I agree with @PaulHigin that there doesn't seem to be a reason to use atomic read/write for keeping track of the disposed information. A bool field is usually for this purpose and works well, an example.
Like @PaulHigin said, this is old code, and this PR is not fixing a bug or a noticeable perf issue, so I also prefer to leave the code as is, with one exception -- the change at line 55 is correcting a mistake, so I would like to keep it as the only change in this PR. @xtqqczze thoughts?
| get | ||
| { | ||
| return Interlocked.Read(ref this._disposed) == 1; | ||
| // Ensure the read of _disposed doesn't move up before the write in the Dispose method. |
There was a problem hiding this comment.
A memory barrier only guarantees the memory access order in the current thread, but Disposed method is very possible to be called in a different thread, so it doesn't seem useful to add the call to Interlocked.MemoryBarrier().
There was a problem hiding this comment.
@daxian-dbw Yes it would seem we would need Interlocked.MemoryBarrierProcessWide instead, but according to documentation this is a very expensive operation.
The most correct method would be to use a dummy CompareExchange operation like in 6d1e962 but @PaulHigin had concerns this would be using the Interlocked API in an unusual way.
It would be great if we could do return Interlocked.Read(ref this._disposed) == 1; but alas this is not defined for an Int32. I am considering opening a new issue at dotnet/runtime to add this API to the BCL.
daxian-dbw
left a comment
There was a problem hiding this comment.
I'm fine with the current change too, with 1 comment.
|
🎉 Handy links: |
PR Summary
Avoid unecessary
Int64for disposed flag inCimAsyncOperation.csPR Context
The original intent appears to be to allow use of
Interlocked.Read(Int64). However this is implemented in the BCL withInterlocked.CompareExchange(ref location, 0, 0). We can use that pattern with anInt32.https://stackoverflow.com/a/24893231
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerWIP:or[ WIP ]to the beginning of the title (theWIPbot will keep its status check atPendingwhile the prefix is present) and remove the prefix when the PR is ready.