0

I have a class which contains a method for receiving UDP data in a separate thread. I do this to avoid the main application (which is running in Unity3D) from stalling.

I need to pass the data that is received in the separate thread to another class, which runs on the original thread and, as such, is able to interact with Unity3D.

Here is roughly what the UDPReceiver looks like:

public class UDPReciever {

    //...

    public UDPReciever() {
        m_Port = 12345;
        m_Worker = new Thread(new ThreadStart(recvData));
        m_Worker.IsBackground = true;
        m_Worker.Start();
    }

    void recvData() {
        m_UDPClient = new UdpClient(m_Port);
        while (true) {
            try {
                IPEndPoint anyIP = new IPEndPoint(IPAddress.Any, 0);
                byte[] data = (m_UDPClient.Receive(ref anyIP));  

                // TODO: Hand 'data' to NetworkController class (running in the original thread) for processing

            } catch (Exception err) {
                    print(err.ToString());
            }
        }
    }   

}

This is roughly what the NetworkController class needs to look like. Ideally the "OnNewData" method would be called every time a new packet is received with the data passed as an argument.

public class NetworkController {

    //...

    void OnNewData(pData) {
        // Process the data in this thread
    }

}

How would I go about achieving this? Thanks in advance.

Paul Allen
  • 45
  • 7
  • Why not simply: **OnNewData(data)** + some **Invoke**? Sorry if I'm missing something...Is there something special with Unity? – Pragmateek Jun 19 '13 at 14:29
  • @Pragmateek How would I invoke the method? My threading knowledge is pretty limited. – Paul Allen Jun 19 '13 at 14:38
  • The only issue you could have is the update of some UI components with **thread affinity**, this requires using special method to push your treatment into the UI thread queue, otherwise this is as simple as calling the method: `byte[] data = (m_UDPClient.Receive(ref anyIP)); networkController.OnNewData(data);` – Pragmateek Jun 19 '13 at 15:06
  • Won't this still be running in the same child thread? NetworkController should be running in the same thread as UDPReceiver, not the child thread that UDPReceiver starts. – Paul Allen Jun 19 '13 at 17:46
  • Why do you absolutely want to do that? :) As I've said one of the only reason you'd want to use a specific thread is **thread affinity**. Just try it and check all is alright. :) – Pragmateek Jun 19 '13 at 18:09
  • Ah.. sorry, I might not have been clear. Unity3D doesn't allow you to access its framework from a separate thread. It performs thread checks and errors out if you attempt to do it as it's not thread-safe. The NetworkController object calls functions used in the Unity framework. – Paul Allen Jun 19 '13 at 19:13
  • Nothing is stopping you from having your thread add data to temporary buffer that `Update()` could check and, if new data is found, react. – Jerdak Jun 19 '13 at 19:22
  • So here we are, again some thread affinity :) As mentionned by *Jerdak* you could use a **queue** to communicate with the Unity main thread: http://answers.unity3d.com/questions/124361/monobehaviourinvoke-and-threading.html But IMHO you should replace the collections used in the samples with some synchronized ones, like a **BlockingCollection** (http://msdn.microsoft.com/en-us/library/dd267312.aspx). – Pragmateek Jun 19 '13 at 19:29

1 Answers1

1

Here is how it could be done (not tested):

public class Dispatcher : MonoBehaviour
{       
    private static readonly BlockingCollection<Action> tasks = new BlockingCollection<Action>();

    public static Dispatcher Instance = null;

    static Dispatcher()
    {
        Instance = new Dispatcher();
    }

    private Dispatcher()
    {
    }

    public void InvokeLater(Action task)
    {
        tasks.Add(task);
    }

    void FixedUpdate()
    {
        if (tasks.Count > 0)
        {
            foreach (Action task in tasks.GetConsumingEnumerable())
            {
                task();
            }
        }
    }
}
...
NetworkController networkControllerInstance;

void recvData()
{
    m_UDPClient = new UdpClient(m_Port);
    while (true)
    {
        try
        {
            IPEndPoint anyIP = new IPEndPoint(IPAddress.Any, 0);
            byte[] data = (m_UDPClient.Receive(ref anyIP));  

            Dispatcher.Instance.InvokeLater(() => networkControllerInstance.OnNewData(data));
        }
        catch (Exception err)
        {
            print(err.ToString());
        }
    }
}

EDIT:

A version that should be compliant with .Net 3.5:

public class Dispatcher : MonoBehaviour
{       
    private static readonly Queue<Action> tasks = new Queue<Action>();

    public static Dispatcher Instance = null;

    static Dispatcher()
    {
        Instance = new Dispatcher();
    }

    private Dispatcher()
    {
    }

    public void InvokeLater(Action task)
    {
        lock (tasks)
        {
            tasks.Enqueue(task);
        }
    }

    void FixedUpdate()
    {
        while (tasks.Count > 0)
        {
            Action task = null;

            lock (tasks)
            {
                if (tasks.Count > 0)
                {
                    task = tasks.Dequeue();
                }
            }

            task();
        }
    }
}

EDIT 2:

if you want to avoid freezing the main thread during a too long period:

void FixedUpdate()
{
    if (tasks.Count != 0)
    {
        Action task = null;

        lock (tasks)
        {
            if (tasks.Count != 0)
            {
                task = tasks.Dequeue();
            }
        }

        task();
    }
}
Pragmateek
  • 13,174
  • 9
  • 74
  • 108
  • Unity only supports up to Mono 2.6 (.net 3.5 I believe), and of that only a subset. Meaning no BlockingCollections for us poor Unity users. – Jerdak Jun 19 '13 at 20:59
  • @Jerdak: I've updated my answer with something that should work with your antique version ;) – Pragmateek Jun 19 '13 at 21:11
  • Can you fax the update to me? :) One thing to note for other users: if the worker thread is assigning tasks faster than `task()` can execute, this code might end up locking Unity's main thread. – Jerdak Jun 19 '13 at 21:15
  • Yes, so if you want you can use a finer execution policy: only one at a time to let a chance for other behaviors to run. I'll update... – Pragmateek Jun 19 '13 at 21:22
  • Note that in your case if no other thread is reading the queue you can avoid the second `if (tasks.Count != 0)`. – Pragmateek Jun 19 '13 at 21:23