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

TrySetSetString can only be called from the main thread. #261

Closed
ColorTwist opened this issue Dec 3, 2018 · 7 comments
Closed

TrySetSetString can only be called from the main thread. #261

ColorTwist opened this issue Dec 3, 2018 · 7 comments

Comments

@ColorTwist
Copy link

ColorTwist commented Dec 3, 2018

I am using the Auth Sample in there to Signing an email/pass user:

I receive an error:

TrySetSetString can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

` // Create a user with the email and password.
public Task CreateUserWithEmailAsync(UserLoginDetails userLogin)
{
Debug.Log(String.Format("Attempting to create user {0}...", email));
// DisableUI();

    // This passes the current displayName through to HandleCreateUserAsync
    // so that it can be passed to UpdateUserProfile().  displayName will be
    // reset by AuthStateChanged() when the new user is created and signed in.
    string newDisplayName = userLogin.name;
    return auth.CreateUserWithEmailAndPasswordAsync(userLogin.email, userLogin.password)
      .ContinueWith((task) =>
      {
          //  EnableUI();
          if (LogTaskCompletion(task, "User Creation"))
          {
              var user = task.Result;
              DisplayDetailedUserInfo(user, 1);
            //  DataController.Instance.LoginProvider = ProviderEnum.password.ToString();
              DataBaseModel.Instance.SaveUserPersonalInfoData(userLogin.country, userLogin.gender, userLogin.email, user.UserId);
              ExecuteNativePopup("Welcome " + displayName, "Email Sign up successfully completed.");
              uiController.OnClick_CloseAllActiveSubMenus();
              return UpdateUserProfileAsync(newDisplayName: newDisplayName);
          }
          return task;
      }).Unwrap();
}
  // Called when a sign-in without fetching profile data completes.
    void HandleSignInWithUser(Task<Firebase.Auth.FirebaseUser> task)
    {
       // EnableUI();
        if (LogTaskCompletion(task, "Sign-in"))
        {
            Debug.Log(String.Format("{0} signed in", task.Result.DisplayName));
            DataController.Instance.LoginProvider = ProviderEnum.password.ToString(); //ERROR IS HERE
            uiController.OnClick_CloseAllActiveSubMenus(); //ALSO CAN HAPPEN HERE
        }
    }
`

Problem is that you cannot execute Unity related outside Unity main thread.
What is the correct method to execute Unity tasks after signing ?

@a-maurice
Copy link
Contributor

Hi @ColorTwist,

Sorry about the confusion, this has become an issue with newer versions of Unity that we did not originally plan for, as previously the ContinueWith would typically occur on the main thread as well. While we are looking into better support for handling this, something you can do in the meantime is set it up to execute on the main thread yourself. Unfortunately Unity doesn't provide a built in way to do this, but you can set up a method yourself, to queue up Actions that can then be executed during a MonoBehaviour's Update call, which will be on the Unity main thread.

If that doesn't make sense, I can provide an example if you need one.

@ColorTwist
Copy link
Author

ColorTwist commented Dec 3, 2018

Thanks for the reply.
Actually i think it would be good to have an example in the samples for this , because i think most users will need to do something after login/sign in, etc.. inside main thread.
I will be happy for an example.
I also read somewhere that await can be used instead of ContinueWith(). but i am unsure how to use it with Firebase.

@a-maurice
Copy link
Contributor

a-maurice commented Dec 3, 2018

Yeah, no worries. So, to just provide you with what we do with our libraries, we have a class that to queue up actions, and provides a method to run on the owning thread. Note that you probably would not normally need something this complex, but for a general solution that handles a lot of cases, this should work.

// Enables callbacks to be dispatched from any thread and be handled on
// the thread that owns the instance to this class (eg. the UIThread).
internal class ThreadDispatcher {
  private int ownerThreadId;
  private System.Collections.Generic.Queue<System.Action> queue =
      new System.Collections.Generic.Queue<System.Action>();

  public ThreadDispatcher() {
    ownerThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
  }

  private class CallbackStorage<TResult> {
    public TResult Result { get; set; }
    public System.Exception Exception { get; set; }
  }

  // Triggers the job to run on the main thread, and waits for it to finish.
  public TResult Run<TResult>(System.Func<TResult> callback) {
    if (ManagesThisThread()) {
      return callback();
    }

    var waitHandle = new System.Threading.EventWaitHandle(
        false, System.Threading.EventResetMode.ManualReset);

    var result = new CallbackStorage<TResult>();
    lock(queue) {
      queue.Enqueue(() => {
        try {
          result.Result = callback();
        }
        catch (System.Exception e) {
          result.Exception = e;
        }
        finally {
          waitHandle.Set();
        }
      });
    }
    waitHandle.WaitOne();
    if (result.Exception != null) throw result.Exception;
    return result.Result;
  }

  // Determines whether this thread is managed by this instance.
  internal bool ManagesThisThread() {
    return System.Threading.Thread.CurrentThread.ManagedThreadId == ownerThreadId;
  }

  // This dispatches jobs queued up for the owning thread.
  // It must be called regularly or the threads waiting for job will be
  // blocked.
  public void PollJobs() {
    System.Diagnostics.Debug.Assert(ManagesThisThread());

    System.Action job;
    while (true) {
      lock(queue) {
        if (queue.Count > 0) {
          job = queue.Dequeue();
        } else {
          break;
        }
      }
      job();
    }
  }
}

In order to use that class, in a MonoBehaviour you would do:

private ThreadDispatcher MyDispatcher;

public Awake() {
  // Create the ThreadDispatcher on a call that is guaranteed to run on the main Unity thread.
  MyDispatcher = new ThreadDispatcher();
}

public Update() {
  MyDispatcher.PollJobs();
}

public TResult RunOnMainThread<TResult>(System.Func<TResult> f) {
  return MyDispatcher.Run(f);
}

And then finally, you could use that with your Auth call like:

return auth.CreateUserWithEmailAndPasswordAsync(userLogin.email, userLogin.password)
      .ContinueWith((task) => {
    return RunOnMainThread(HandleCreateUser(task));
  });

As for await vs ContinueWith, you can think of await as just a different way to format ContinueWith, where instead of the code being within the ContinueWith call, it is instead just after the await. Unfortunately, I believe it would have the same problem that the ContinueWith has, that the await is not guaranteed to return on the main Unity thread.

@ColorTwist
Copy link
Author

Thanks for the example! Appreciated 👍
Really hope we can find a cleaner solution for this as for me it caused a lot of time wasting since of the unexpected behaviour. I understand it's Unity change i guess related to the Script Runtime version 4.x and threading.

@JustinSchneider
Copy link

@a-maurice Thanks for providing this workaround. I'm still a bit confused as to how to use it correctly. As in, what should the signature be of methods passed into RunOnMainThread? I'm getting an ambiguous type error and not really sure where to go from here, being pretty new to threading and all. Any help is appreciated.

@robertlair
Copy link

@a-maurice , I too am having some issues getting this working. I have created the script above, and added the code to the MonoBehavior that contains the ContinueWith. Then in the ContinueWith, I have

return RunOnMainThread(HandleAction(task as Task<Firebase.Auth.FirebaseUser>))

I added the cast because I was getting a conversion error. But I am still getting the error "The type arguments for method 'AuthManager.RunOnMainThread(Func)' cannot be inferred from the usage. Try specifying the type arguments explicitly."

Any thoughts on what I need to do here?

Thanks!
Bob

@a-maurice
Copy link
Contributor

Hi @robertlair

Sorry for the delay in response. Not entirely sure what that error could mean, unfortunately. My best guess would be that the type of HandleAction isn't the correct type for it. Basically, you would want to be calling it with something like:
RunOnMainThread(() => { return 0; });
In that you give RunOnMainThread a function to run later.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants