Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C#: avoid shutdown crash on iOS #16308

Merged
merged 3 commits into from Aug 10, 2018

Conversation

jtattermusch
Copy link
Contributor

By skipping grpc_shutdown invocation. This is basically a hack, but it only affects iOS which is still supported by gRPC C# experimentally and this PR should increase the usability.

Tentative fix for #16294.

@jtattermusch jtattermusch added lang/C# release notes: yes Indicates if PR needs to be in release notes labels Aug 10, 2018
@grpc-testing
Copy link

Objective-C binary sizes
*****************STATIC******************
  New size                      Old size
 1,950,932      Total (=)      1,950,932

 No significant differences in binary sizes

***************FRAMEWORKS****************
  New size                      Old size
10,669,325      Total (<)     10,669,329

 No significant differences in binary sizes


@jtattermusch
Copy link
Contributor Author

Copy link
Member

@yashykt yashykt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the syntax that well, but I guess the compiler does that check for me.

If checking that the type is available or not is enough, LGTM

isUnity = Type.GetType(UnityEngineApplicationClassName) != null;

// Unity
var unityApplicationClass = Type.GetType(UnityEngineApplicationClassName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this enough of a check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, there's no official way so we just chose an approach that seemed most reasonable (also the general approach stays the same in this PR, so not really an issue we need to solve right now).

isUnityIOS = false;
}

// Xamarin
isXamarinIOS = Type.GetType(XamarinIOSObjectClassName) != null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we won't have these types for anything else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems very unlikely someone would use that class name and assembly name. Also, see comment above.

// Normally grpc_init and grpc_shutdown calls should come in pairs (C core does reference counting),
// but in case we avoid grpc_shutdown calls altogether, calling grpc_init has no effect
// besides incrementing an internal C core counter that could theoretically overflow.
// NOTE: synchronization not necessary here as we are only trying to avoid calling grpc_init
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the idea here that a race won't happen because it will never overflow quickly enough? In that case, even so, I think it would make it easier to read if alreadyInvokedNativeInit was under a lock

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that it's not guaranteed that if we set alreadyInvokedNativeInit, other thread will immediately see that, but they will see it "very soon" and we don't mind invoking grpc_init once or twice in the meantime. In the worst case, each thread will run grpc_init once and that's still far from risking the owerflow.
The idea is the same as Joshua Bloch's "Racy-Single-Check" idiom -
http://javaagile.blogspot.com/2013/05/the-racy-single-check-idiom.html
which is used e.g. in Java String to initialize the hashCode field.

But you're right that this technique is a bit of an overkill for this situation. I'll double check that GrpcNativeInit only happens when a GrpcEnvironment gets initialized and probably use a lock.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd tend to err on the side of caution around this. And I really have no idea if this is an issue in C#, but see benign data races blog post

@jtattermusch
Copy link
Contributor Author

@yashykt thanks for review! (I forgot to mention that I added @apolcyn for the C# code changes and only needed a general LGTM from you).

NativeMethods.Get().grpcsharp_init();
nativeInitCounter.Increment();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apolcyn because the GrpcNativeInit() is invoked for every channel creation / shutdown and server creation / shutdown, I wanted to avoid a global lock, so I used an AtomicCounter, to determine if grpcsharp_init needs to be invoked.
PTAL.

@@ -360,12 +361,25 @@ internal static string GetCoreVersionString()

internal static void GrpcNativeInit()
{
if (!IsNativeShutdownAllowed && nativeInitCounter.Count > 0)
{
// Normally grpc_init and grpc_shutdown calls should come in pairs (C core does reference counting),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Just another idea though: if going with this lock-free approach, I wonder if some kind of a "lock free once" algorithm would be better here. Also, the mutex vs. lock free counter thing seems like an interesting comparison to run/validate in the C# microbenchmarks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you in general, but I didn't have a "lock free once" algorithm in mind (besides utilizing Lazy, which probably does doubly checked locking) and in the end using the AtomicCounter seemed like the simplest and most foolproof approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I can't really point to one.

Actually, can we not do something like:

{
   if (nativeInitCounter.Increment() == 0) {
     ... call grpc_init()
   }
   return;
}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought of that but I think it's incorrect:

  1. thread 1 calls Increment, gets 0, then yields
  2. thread 2 calls Increment, gets 1 and thus skips grpc_init
  3. thread 2 does something assuming grpc_init has already been invoked, but that' not the case
  4. thread 1 finally invokes grpc_init, but it's too late

In another words, we must not increment before grpc_init has returned.

@grpc-testing
Copy link

Objective-C binary sizes
*****************STATIC******************
  New size                      Old size
 1,950,932      Total (=)      1,950,932

 No significant differences in binary sizes

***************FRAMEWORKS****************
  New size                      Old size
10,662,143      Total (<)     10,662,145

 No significant differences in binary sizes


@jtattermusch jtattermusch merged commit 46f8637 into grpc:master Aug 10, 2018
@lock lock bot locked as resolved and limited conversation to collaborators Nov 8, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
lang/C# release notes: yes Indicates if PR needs to be in release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants