Подтвердить что ты не робот

Используйте Unity API из другого потока или вызовите функцию в главной теме

Моя проблема в том, что я пытаюсь использовать сокет Unity для реализации чего-либо. Каждый раз, когда я получаю новое сообщение, мне нужно обновить его до текста обновления (это текст Unity). Однако, когда я делаю следующий код, void update не вызывает каждый раз.

Причина, по которой я не включаю updatetext.GetComponent<Text>().text = "From server: "+tempMesg; в void getInformation, заключается в том, что эта функция находится в потоке, и когда я включаю ее в getInformation(), она приводит к ошибке:

getcomponentfastpath can only be called from the main thread

Я думаю, что проблема в том, что я не знаю, как запустить основной поток и дочерний поток в С# вместе? Или, может быть, другие проблемы.

Вот мой код:

using UnityEngine;
using System.Collections;
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine.UI;


public class Client : MonoBehaviour {

    System.Net.Sockets.TcpClient clientSocket = new System.Net.Sockets.TcpClient();
    private Thread oThread;

//  for UI update
    public GameObject updatetext;
    String tempMesg = "Waiting...";

    // Use this for initialization
    void Start () {
        updatetext.GetComponent<Text>().text = "Waiting...";
        clientSocket.Connect("10.132.198.29", 8888);
        oThread = new Thread (new ThreadStart (getInformation));
        oThread.Start ();
        Debug.Log ("Running the client");
    }

    // Update is called once per frame
    void Update () {
        updatetext.GetComponent<Text>().text = "From server: "+tempMesg;
        Debug.Log (tempMesg);
    }

    void getInformation(){
        while (true) {
            try {
                NetworkStream networkStream = clientSocket.GetStream ();
                byte[] bytesFrom = new byte[10025];
                networkStream.Read (bytesFrom, 0, (int)bytesFrom.Length);
                string dataFromClient = System.Text.Encoding.ASCII.GetString (bytesFrom);
                dataFromClient = dataFromClient.Substring (0, dataFromClient.IndexOf ("$"));
                Debug.Log (" >> Data from Server - " + dataFromClient);

                tempMesg = dataFromClient;

                string serverResponse = "Last Message from Server" + dataFromClient;

                Byte[] sendBytes = Encoding.ASCII.GetBytes (serverResponse);
                networkStream.Write (sendBytes, 0, sendBytes.Length);
                networkStream.Flush ();
                Debug.Log (" >> " + serverResponse);

            } catch (Exception ex) {
                Debug.Log ("Exception error:" + ex.ToString ());
                oThread.Abort ();
                oThread.Join ();
            }
//          Thread.Sleep (500);
        }
    }
}
4b9b3361

Ответ 1

Unity не является Thread безопасным, поэтому они решили сделать невозможным вызов своего API из другого Thread путем добавления механизма для исключения исключения, когда его API используется из другого Thread.

Этот вопрос задавался так много раз, но не было правильного решения/ответа на любой из них. Ответы обычно "используют плагин" или делают что-то не потокобезопасное. Надеюсь, это будет последний.

Решение, которое вы обычно видите на веб-сайте Stackoverflow или Unity, - это просто использовать переменную boolean, чтобы основной поток знал, что вам нужно выполнить код в главном Thread. Это неверно, поскольку оно не является потокобезопасным и не дает вам контроля над тем, какую функцию вызывать. Что делать, если у вас есть несколько Threads, которые должны уведомить основной поток?

Другим решением, которое вы увидите, является использование сопрограммы вместо Thread. Это работает не. Использование сопрограммы для сокетов ничего не изменит. Вы все равно столкнетесь с проблемами замораживания. Вы должны придерживаться своего кода Thread или использовать Async.

Один из правильных способов сделать это - создать коллекцию, такую ​​как List. Когда вам нужно что-то выполнить в главном потоке, вызовите функцию, которая хранит код для выполнения в Action. Скопируйте List из Action в локальный List из Action, затем выполните код из локального Action в этом List, затем очистите это List. Это препятствует тому, чтобы другой Threads мог дождаться завершения его выполнения.

Вам также нужно добавить volatile boolean, чтобы уведомить функцию Update, что в List есть код, который должен быть выполнен. При копировании List в локальный List он должен быть обернут вокруг ключевого слова lock, чтобы предотвратить запись другого потока.

A script, который выполняет то, что я упомянул выше:

UnityThread Script:

#define ENABLE_UPDATE_FUNCTION_CALLBACK
#define ENABLE_LATEUPDATE_FUNCTION_CALLBACK
#define ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK

using System;
using System.Collections;
using UnityEngine;
using System.Collections.Generic;


public class UnityThread : MonoBehaviour
{
    //our (singleton) instance
    private static UnityThread instance = null;


    ////////////////////////////////////////////////UPDATE IMPL////////////////////////////////////////////////////////
    //Holds actions received from another Thread. Will be coped to actionCopiedQueueUpdateFunc then executed from there
    private static List<System.Action> actionQueuesUpdateFunc = new List<Action>();

    //holds Actions copied from actionQueuesUpdateFunc to be executed
    List<System.Action> actionCopiedQueueUpdateFunc = new List<System.Action>();

    // Used to know if whe have new Action function to execute. This prevents the use of the lock keyword every frame
    private volatile static bool noActionQueueToExecuteUpdateFunc = true;


    ////////////////////////////////////////////////LATEUPDATE IMPL////////////////////////////////////////////////////////
    //Holds actions received from another Thread. Will be coped to actionCopiedQueueLateUpdateFunc then executed from there
    private static List<System.Action> actionQueuesLateUpdateFunc = new List<Action>();

    //holds Actions copied from actionQueuesLateUpdateFunc to be executed
    List<System.Action> actionCopiedQueueLateUpdateFunc = new List<System.Action>();

    // Used to know if whe have new Action function to execute. This prevents the use of the lock keyword every frame
    private volatile static bool noActionQueueToExecuteLateUpdateFunc = true;



    ////////////////////////////////////////////////FIXEDUPDATE IMPL////////////////////////////////////////////////////////
    //Holds actions received from another Thread. Will be coped to actionCopiedQueueFixedUpdateFunc then executed from there
    private static List<System.Action> actionQueuesFixedUpdateFunc = new List<Action>();

    //holds Actions copied from actionQueuesFixedUpdateFunc to be executed
    List<System.Action> actionCopiedQueueFixedUpdateFunc = new List<System.Action>();

    // Used to know if whe have new Action function to execute. This prevents the use of the lock keyword every frame
    private volatile static bool noActionQueueToExecuteFixedUpdateFunc = true;


    //Used to initialize UnityThread. Call once before any function here
    public static void initUnityThread(bool visible = false)
    {
        if (instance != null)
        {
            return;
        }

        if (Application.isPlaying)
        {
            // add an invisible game object to the scene
            GameObject obj = new GameObject("MainThreadExecuter");
            if (!visible)
            {
                obj.hideFlags = HideFlags.HideAndDontSave;
            }

            DontDestroyOnLoad(obj);
            instance = obj.AddComponent<UnityThread>();
        }
    }

    public void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }

    //////////////////////////////////////////////COROUTINE IMPL//////////////////////////////////////////////////////
#if (ENABLE_UPDATE_FUNCTION_CALLBACK)
    public static void executeCoroutine(IEnumerator action)
    {
        if (instance != null)
        {
            executeInUpdate(() => instance.StartCoroutine(action));
        }
    }

    ////////////////////////////////////////////UPDATE IMPL////////////////////////////////////////////////////
    public static void executeInUpdate(System.Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        lock (actionQueuesUpdateFunc)
        {
            actionQueuesUpdateFunc.Add(action);
            noActionQueueToExecuteUpdateFunc = false;
        }
    }

    public void Update()
    {
        if (noActionQueueToExecuteUpdateFunc)
        {
            return;
        }

        //Clear the old actions from the actionCopiedQueueUpdateFunc queue
        actionCopiedQueueUpdateFunc.Clear();
        lock (actionQueuesUpdateFunc)
        {
            //Copy actionQueuesUpdateFunc to the actionCopiedQueueUpdateFunc variable
            actionCopiedQueueUpdateFunc.AddRange(actionQueuesUpdateFunc);
            //Now clear the actionQueuesUpdateFunc since we've done copying it
            actionQueuesUpdateFunc.Clear();
            noActionQueueToExecuteUpdateFunc = true;
        }

        // Loop and execute the functions from the actionCopiedQueueUpdateFunc
        for (int i = 0; i < actionCopiedQueueUpdateFunc.Count; i++)
        {
            actionCopiedQueueUpdateFunc[i].Invoke();
        }
    }
#endif

    ////////////////////////////////////////////LATEUPDATE IMPL////////////////////////////////////////////////////
#if (ENABLE_LATEUPDATE_FUNCTION_CALLBACK)
    public static void executeInLateUpdate(System.Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        lock (actionQueuesLateUpdateFunc)
        {
            actionQueuesLateUpdateFunc.Add(action);
            noActionQueueToExecuteLateUpdateFunc = false;
        }
    }


    public void LateUpdate()
    {
        if (noActionQueueToExecuteLateUpdateFunc)
        {
            return;
        }

        //Clear the old actions from the actionCopiedQueueLateUpdateFunc queue
        actionCopiedQueueLateUpdateFunc.Clear();
        lock (actionQueuesLateUpdateFunc)
        {
            //Copy actionQueuesLateUpdateFunc to the actionCopiedQueueLateUpdateFunc variable
            actionCopiedQueueLateUpdateFunc.AddRange(actionQueuesLateUpdateFunc);
            //Now clear the actionQueuesLateUpdateFunc since we've done copying it
            actionQueuesLateUpdateFunc.Clear();
            noActionQueueToExecuteLateUpdateFunc = true;
        }

        // Loop and execute the functions from the actionCopiedQueueLateUpdateFunc
        for (int i = 0; i < actionCopiedQueueLateUpdateFunc.Count; i++)
        {
            actionCopiedQueueLateUpdateFunc[i].Invoke();
        }
    }
#endif

    ////////////////////////////////////////////FIXEDUPDATE IMPL//////////////////////////////////////////////////
#if (ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK)
    public static void executeInFixedUpdate(System.Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        lock (actionQueuesFixedUpdateFunc)
        {
            actionQueuesFixedUpdateFunc.Add(action);
            noActionQueueToExecuteFixedUpdateFunc = false;
        }
    }

    public void FixedUpdate()
    {
        if (noActionQueueToExecuteFixedUpdateFunc)
        {
            return;
        }

        //Clear the old actions from the actionCopiedQueueFixedUpdateFunc queue
        actionCopiedQueueFixedUpdateFunc.Clear();
        lock (actionQueuesFixedUpdateFunc)
        {
            //Copy actionQueuesFixedUpdateFunc to the actionCopiedQueueFixedUpdateFunc variable
            actionCopiedQueueFixedUpdateFunc.AddRange(actionQueuesFixedUpdateFunc);
            //Now clear the actionQueuesFixedUpdateFunc since we've done copying it
            actionQueuesFixedUpdateFunc.Clear();
            noActionQueueToExecuteFixedUpdateFunc = true;
        }

        // Loop and execute the functions from the actionCopiedQueueFixedUpdateFunc
        for (int i = 0; i < actionCopiedQueueFixedUpdateFunc.Count; i++)
        {
            actionCopiedQueueFixedUpdateFunc[i].Invoke();
        }
    }
#endif

    public void OnDisable()
    {
        if (instance == this)
        {
            instance = null;
        }
    }
}

ИСПОЛЬЗОВАНИЕ

Эта реализация позволяет вам вызывать функции в 3 наиболее используемых функциях Unity: Update, LateUpdate и FixedUpdate. Это также позволяет вам вызывать функцию сопрограммы в главном Thread. Он может быть расширен, чтобы иметь возможность вызывать функции в других функциях обратного вызова Unity, таких как OnPreRender и OnPostRender.

1. Сначала инициализируйте его из функции Awake().

void Awake()
{
    UnityThread.initUnityThread();
}

2. Чтобы выполнить код в главном Thread из другого потока:

UnityThread.executeInUpdate(() =>
{
    transform.Rotate(new Vector3(0f, 90f, 0f));
});

Это приведет к вращению текущего объекта, к которому привязан склип, до 90 град. Теперь вы можете использовать Unity API (transform.Rotate) в другом Thread.

3. Чтобы вызвать функцию в главном Thread из другого потока:

Action rot = Rotate;
UnityThread.executeInUpdate(rot);


void Rotate()
{
    transform.Rotate(new Vector3(0f, 90f, 0f));
}

Образцы # 2 и # 3 выполняются в Update.

4. Чтобы выполнить код в LateUpdate из другого потока:

Пример этого - код отслеживания камеры.

UnityThread.executeInLateUpdate(()=>
{
    //Your code camera moving code
});

5. Чтобы выполнить код в FixedUpdate из другого потока:

Пример этого при выполнении физических упражнений, таких как добавление силы к Rigidbody.

UnityThread.executeInFixedUpdate(()=>
{
    //Your code physics code
});

6. Чтобы запустить функцию сопрограммы в главном Thread из другого потока:

UnityThread.executeCoroutine(myCoroutine());

IEnumerator myCoroutine()
{
    Debug.Log("Hello");
    yield return new WaitForSeconds(2f);
    Debug.Log("Test");
}

Наконец, если вам не нужно ничего выполнять в LateUpdate и FixedUpdate, вы должны прокомментировать обе строки этого кода ниже:

//#define ENABLE_LATEUPDATE_FUNCTION_CALLBACK
//#define ENABLE_FIXEDUPDATE_FUNCTION_CALLBACK

Это повысит производительность.

Ответ 2

Многое из написанного о потоках в Unity на самом деле совершенно неверно.

Как так?

Фундаментальный факт заключается в том, что Unity, конечно, полностью основана на кадрах.

Когда вы работаете в системе на основе фреймов, проблемы с потоками существенно отличаются от обычных систем.

Это совершенно другая парадигма.

(Действительно, во многих отношениях это намного проще.)

Проблемы с многопоточностью в системе, основанной на кадрах, полностью, полностью отличаются от обычной системы. (В некоторых отношениях гораздо проще иметь дело, потому что некоторые концепции просто не существуют в системе на основе фреймов.)

Допустим, у вас есть дисплей термометра Unity, который показывает некоторое значение

Thermo.cs

enter image description here

Так что у него будет функция, которая вызывается в Update, например,

func void ShowThermoValue(float fraction) {
   display code, animates a thermometer
}

Это выполняется только один раз за кадр, и это то.

очень легко забыть этот основной факт . Конечно, он работает только в "главном потоке".

(Там нет ничего другого в Unity! Там просто............... "Нить Unity"!)

Но следует помнить о ключевой парадигме: она запускается один раз за кадр.

Теперь где-то еще, в Thermo.cs, у вас будет функция, которая обрабатывает концепцию "пришло новое значение":

[MonoPInvokeCallback(typeof(ipDel))]
public static void NewValueArrives(float f) {

    ... ???
}

Обратите внимание, что, конечно,

это функция класса! Вы никак не можете "достучаться" до обычной функции Unity, такой как ShowThermoValue !!!!!! Footnote 1

Это совершенно, совершенно бессмысленное понятие. вы не можете "добраться до" ShowThermoValue.

Любые примеры кода, связанные с потоками, которые вы видите, для Unity, которые пытаются "достучаться" до "потока", полностью и полностью ошибочны.

Нет тем, к которым можно обратиться!

Опять же - и это удивительно - если вы просто гуглите все статьи на темы о Unity, написанные на www, многие статьи и посты абсолютно концептуальны.

Допустим, значения приходят очень часто и нерегулярно.

Вы можете подключить различные научные устройства к стойке ПК и айфонов, и новые значения "температуры"

могут появляться очень часто... десятки раз за кадр ... или, возможно, только каждые несколько секунд. Так что вы делаете со значениями?

Это не может быть проще.

Из потока поступающих значений все, что вы делаете, это................. ждете этого............. установите переменную в компоненте !!

WTF? Все, что вы делаете, это устанавливаете переменную? Это? Как это может быть так просто?

Это одна из тех необычных ситуаций:

Большая часть написанного на потоках в Unity просто

  1. совершенно неверна . Это не так, как будто есть "одна ошибка" или "что-то, что нужно исправить". Это просто "совершенно и совершенно неправильно": на уровне базовой парадигмы. Удивительно, но фактический подход абсурдно прост.

  2. Это так просто, что вы можете подумать, что делаете что-то не так.

  3. Итак, переменная...

[System.Nonserialized] public float latestValue;

Установите его из "поступающей нити"...

[MonoPInvokeCallback(typeof(ipDel))]
public static void NewValueArrives(float f) {

    ThisScript.runningInstance.latestValue = f; // done
}

Честно говоря, это так.

По сути, для того, чтобы стать величайшим в мире экспертом в "потоке в Unity", который, очевидно, основан на фреймах, ничего не нужно делать, как описано выше.

И всякий раз, когда вызывается ShowThermoValue, каждый кадр...................... просто отображать это значение!

Действительно, что это!

[System.Nonserialized] public float latestValue;
func void ShowThermoValue() { // note NO arguments here!
   display code, animates a thermometer
   thermo height = latestValue
}

Вы просто отображаете "последнее" значение.

latestValue мог быть установлен один, два, десять или сто раз больше этого кадра............ но вы просто отображаете, какое бы значение не было, когда ShowThermoValue запускает этот кадр!

Что еще вы могли бы показать?

Термометр обновляется на 60fps на экране. Footnote 2

Это на самом деле так просто. Это так просто. Удивительно, но это правда.

(Помимо всего прочего - не забывайте, что vector3 и т.д. НЕ являются атомарными в Unity/С#)


Как указал пользователь @dymanoid (прочитайте важное обсуждение ниже), важно помнить, что хотя float является атомарным в среде Unity/С#, все остальное (скажем, Vector3 и т.д.) НЕ АТОМНО. Как правило (как в примере здесь) вы передаете плавающие числа только из вычислений, скажем, от собственных плагинов. Но важно знать, что векторы и т.д. НЕ являются атомарными.

Иногда опытные программисты с многопоточностью сталкиваются с системой, основанной на фреймах, потому что: в системе, основанной на фреймах, большинство проблем, вызванных проблемами ипподрома и блокировки... концептуально не существует.


В системе, основанной на кадрах, любые игровые элементы должны просто отображаться или вести себя на основе некоторого "текущего значения", которое где-то установлено. Если информация поступает из других потоков, просто установите эти значения - готово.

Вы

не можете осмысленно "общаться с основным потоком" вUnity, потому что этот основной поток............. основан на фреймах! Оставляя в стороне проблемы с многопоточностью, непрерывная система

не может осмысленно общаться с системой фрейм-парадигмы. Большинство проблем с блокировками, блокировками и ипподромом

не существует в парадигме на основе фреймов, потому что: если вы устанавливаете lastValue десять раз, миллион раз, миллиард раз, в одном конкретном кадре... что вы можете сделать делать?.. вы можете отобразить только одно значение! Подумайте о старомодной пластиковой пленке. У тебя буквально просто...... кадр, и вот оно. Если в одном конкретном кадре задано значение lastValue триллион раз, ShowThermoValue просто покажет (для этой 60-й секунды) одно значение, которое он захватывает при запуске.

Все, что вы делаете: оставляете где-нибудь информацию, и система фрейм-парадигмы будет использовать этот фрейм, если захочет.

Это в двух словах.

Таким образом, большинство "проблем с потоками" исчезают в Unity.

Все, что вы можете сделать из

другие темы расчета или

  • из тем плагина,

  • это просто "пропущенные значения", которые может использовать игра.

Вот оно!

Сноски


1 Как ты мог? В качестве мысленного эксперимента забудьте о том, что вы находитесь в другой теме. ShowThermoValue запускается один раз фреймом с помощью движка фреймов. Вы не можете "назвать" это каким-либо осмысленным способом. В отличие от обычного программного обеспечения ОО, вы не можете, например, создать экземпляр класса (Компонент - бессмысленный) и запустить эту функцию - это совершенно бессмысленно.


В "нормальном" многопоточном программировании потоки могут взаимодействовать друг с другом и так далее, и при этом у вас возникают проблемы с блокировкой, ипподромом и так далее. Но это все

бессмысленно в системе ECS, основанной на кадрах. Там нет ничего, чтобы "поговорить". Допустим, что Unity на самом деле был многопоточным !!!! Таким образом, ребята из Unity работают многопоточным образом. Это не имело бы никакого значения - вы не можете войти в ShowThermoValue каким-либо значимым способом! Это Компонент, который движок кадра запускает один раз в кадр и что то.

Так что NewValueArrives нигде нет - это функция класса!

Давайте ответим на вопрос в заголовке:

"Использовать API Unity из другого потока или вызвать функцию в основном потоке?"

Концепция

> совершенно бессмысленна & lt; & lt;. Unity (как и все игровые движки) основан на фреймах. Нет понятия "вызова" функции в главном потоке. Чтобы сделать аналогию: это было бы как кинематографист в эпоху целлулоидных фильмов, спрашивающий, как "переместить" что-то на самом деле на один из кадров.

enter image description here

Конечно, это бессмысленно. Все, что вы можете сделать, это изменить что-то для следующей фотографии, следующего кадра.


2 Я имею в виду "поток поступающих значений"... на самом деле! NewValueArrives может или не может работать в главном потоке !!!! Он может работать в потоке плагина или в другом потоке! На самом деле он может быть полностью однопоточным, когда вы имеете дело с вызовом NewValueArrives! Это просто не имеет значения! То, что вы делаете, и все, что вы можете делать в рамках основанной на кадрах парадигмы, - это "оставлять лежать вокруг" информации, которую могут использовать компоненты, такие как ShowThermoValue, по своему усмотрению.

Ответ 3

Я использовал это решение этой проблемы. Создайте скрипт с этим кодом и прикрепите его к игровому объекту:

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using UnityEngine;

public class ExecuteOnMainThread : MonoBehaviour {

    public readonly static ConcurrentQueue<Action> RunOnMainThread = new ConcurrentQueue<Action>();

    void Update()
    {
        if(!RunOnMainThread.IsEmpty())
        {
           while(RunOnMainThread.TryDequeue(out action))
           {
             action.Invoke();
           }
        }
    }

}

Затем, когда вам нужно что-то вызвать в главном потоке и получить доступ к Unity API из любой другой функции в вашем приложении:

ExecuteOnMainThread.RunOnMainThread.Enqueue(() => {

    // Code here will be called in the main thread...

});

Ответ 4

Причиной этого исключения является то, что UnityEngine не является потокобезопасным. Используйте сопрограмму вместо потока:

void Start () {
    updatetext.GetComponent<Text>().text = "Waiting...";
    clientSocket.Connect("10.132.198.29", 8888);
    StartCoroutine(getInformation());
    Debug.Log ("Running the client");
}

// Update is called once per frame
void Update () {
    updatetext.GetComponent<Text>().text = "From server: "+tempMesg;
    Debug.Log (tempMesg);
}

IEnumerator getInformation(){
    while (true) {
        //...
        yield return null;
        if(done) break;//terminate condition
    }
}

поместите свой код до //... и установите boolean done, чтобы указать, когда информация будет готова и метод должен завершиться.

Ответ 5

Я всегда так делаю. Я надеюсь, что это поможет вам тоже.

List<string> methodsToCall = new List<string>();

void CallingFromAnotherThread(){
    // do all your stuff here.
    //And when you want to call main thread API add the method name to the list

    methodsToCall.Add("MyMainThreadMethod");
}

void Update(){
    if( methodsToCall.Count > 0 ){
        foreach( string s in methodsToCall ){
            Invoke(s,0f);
        }
    methodsToCall.Clear();
    }
}

void MyMainThreadMethod(){
    // your Unity main thread API here.
}