In this post, I’m going to explore the different threading options on 3- and 4-series processors. I find it difficult to keep everything straight when working in different Crestron environments, so hopefully this post will be a good refresher when I need it.
What are threads?
Threads are an execution path through your program. See Wikipedia for an exhaustive description. The OS kernel (Windows CE on 3-series and Linux on 4-series) manages our running program as a process. A process might have one or more threads running that can all access the same resources: memory, file handles, etc.
There are several thread types: kernel thread, user thread, and fiber. Kernel threads are scheduled by the OS whereas user threads are scheduled by a library or our program. Fibers are a way for user threads to cooperatively multitask (they run until they yield to the next fiber).
Why do we need them?
See this page for a list of advantages and disadvantages to writing multithreaded programs. Summarized, the advantages are:
- Improved performance
- Simplified coding for messaging
- GUI responsiveness
- Better use of CPU
The disadvantages are:
- Complex debugging and testing
- Context switching overhead
- Deadlocks
- Unpredictable results
I would argue the advantages outweigh the disadvantages. And the Crestron platform is already multithreaded, so we’ll have to contend with that anyway.
For example: if we didn’t have a multithreaded architecture for our programs, we would have to check serial ports for new data on every pass through our program. If new data arrived, we would have to route it to the correct logic for processing. This might introduce lag if a user is trying to press buttons on a touchpanel at the same time since we won’t process those messages until after we’re done handling the serial data.
3-Series Sandbox
I first want to walk through the sandboxed threading options. Two of these examples will be SIMPL Windows programs, and one will be SIMPL# Pro. Code is available on GitHub if you’d like to follow along.
SIMPL Windows
We can’t use SIMPL# Pro classes if our code is going to be part of a SIMPL Windows program. From what I can see in the SIMPL# reference, that gives us two options:
- CTimer
- BeginInvoke
We’ll write some toy programs to play around with both of these.
CTimer
Our first example will use the CTimer class. Lets create a new SIMPL# Library in VS2008 and create a new class named Test1:
using System;
using Crestron.SimplSharp;
namespace ThreadTest
{
public class TimerEventArgs : EventArgs
{
public string Message;
public TimerEventArgs()
{
Message = "No message.";
}
public TimerEventArgs(string msg)
{
Message = msg;
}
}
public class Test1
{
private CTimer _timer;
private string _name;
private long _frequency;
private long _counter;
private bool _running;
public event EventHandler<TimerEventArgs> TimerEvent;
public Test1()
{
}
public void Initialize(string name, short frequency)
{
_name = name;
_frequency = frequency;
_timer = new CTimer(DoWork, this, Timeout.Infinite);
}
public void Start()
{
if (!_running)
{
_running = true;
_counter = 0;
_timer.Reset(_frequency, _frequency);
}
}
public void Stop()
{
if (_running)
{
_running = false;
_timer.Stop();
}
}
private void DoWork(object userObj)
{
if (_running)
{
_counter++;
if (TimerEvent != null)
{
TimerEvent(userObj, new TimerEventArgs(String.Format("{0}: counting {1}", _name, _counter)));
}
}
}
}
}
I’ve put another class named TimerEventArgs in here as well. We’re going to use this to pass a message during timer events to SIMPL+. It only has one field named Message that we’ll access inside of our event handler later.
One thing to remember about SIMPL# libraries is the limitation that SIMPL+ can only call a default constructor (without parameters). That’s why we’ve settled on using a default constructor and a method named Initialize. We’ll call Initialize from SIMPL+ with the values we want.
public Test1()
{
}
public void Initialize(string name, short frequency)
{
_name = name;
_frequency = frequency;
_timer = new CTimer(DoWork, this, Timeout.Infinite);
}
When we create our CTimer, we pass in a callback delegate named DoWork, a reference to this object, and specify Timeout.Infinite so the timer doesn’t automatically start. DoWork doesn’t actually do much work in our example other than counting and telling SIMPL+ about it:
private void DoWork(object userObj)
{
if (_running)
{
_counter++;
if (TimerEvent != null)
{
TimerEvent(userObj, new TimerEventArgs(String.Format("{0}: counting {1}",
_name, _counter)));
}
}
}
The last two methods, Start and Stop, fire up our timer that will repeat after a set amount of milliseconds (saved in _frequency when we called Initialize):
public void Start()
{
if (!_running)
{
_running = true;
_counter = 0;
_timer.Reset(_frequency, _frequency);
}
}
public void Stop()
{
if (_running)
{
_running = false;
_timer.Stop();
}
}
Next, we’ll create a new SIMPL+ module that imports our library and gives us access to the methods we created in our Test1 class:
// COMPILER DIRECTIVES /////////////////////////////////////
#ENABLE_DYNAMIC
#DEFAULT_VOLATILE
#ENABLE_STACK_CHECKING
#ENABLE_TRACE
// LIBRARIES ///////////////////////////////////////////////
#USER_SIMPLSHARP_LIBRARY "ThreadTest"
// INPUTS //////////////////////////////////////////////////
DIGITAL_INPUT Initialize;
DIGITAL_INPUT Run;
// OUTPUTS /////////////////////////////////////////////////
STRING_OUTPUT Message;
// PARAMETERS //////////////////////////////////////////////
STRING_PARAMETER My_Name[50];
INTEGER_PARAMETER My_Frequency;
// GLOBAL VARIABLES ////////////////////////////////////////
Test1 myTest;
// EVENT HANDLERS //////////////////////////////////////////
PUSH Initialize
{
myTest.Initialize(My_Name, My_Frequency);
}
PUSH Run
{
myTest.Start();
}
RELEASE Run
{
myTest.Stop();
}
// CALLBACKS AND DELEGATES /////////////////////////////////
EVENTHANDLER MyTimerEvent (Test1 sender, TimerEventArgs e)
{
Message = e.Message;
}
// MAIN ////////////////////////////////////////////////////
FUNCTION Main()
{
RegisterEvent(myTest, TimerEvent, MyTimerEvent);
WaitForInitializationComplete();
}
myTest actually gets created when we define it (the default constructor is called). We provide an Initialize input that passes in the My_Name and My_Frequency parameters. When Run is high, our timer is started, and when it goes low, it stops.
We can now add this to a SIMPL Windows program. Here, I’ve created two timers to test:

When we pulse Initialize, both timers will get created: one with a frequency of 1 second, the other with a frequency of 2 seconds. If we hold Run high, both timers will start at the same time and we can watch them pass messages to our program:

There are (at least) 3 threads running here: our program, Timer1, and Timer2. Timer1 and Timer2 have no knowledge of each other, and they can’t step on each other’s state. There may be a slight delay in either of them calling their event handlers because the 3-series can only execute a single thread at a time (it doesn’t have a multithreaded processor). In practice though, it appears that both threads are running concurrently (at least their timestamps in debugger appear to be the same).
Our next example will look at threads that need access to a common resource, and the precautions we need to take to make sure they don’t deadlock.
BeginInvoke
BeginInvoke is another way to hand off control to another thread. I’ve added a Test2 class to the same project as Test1:
using System;
using Crestron.SimplSharp;
namespace ThreadTest
{
public class Test2
{
private static CMutex _lock;
private static int _counter;
private string _name;
private int _frequency;
private bool _running;
public event EventHandler<TimerEventArgs> TimerEvent;
public Test2()
{
if (_lock == null)
{
_lock = new CMutex();
}
}
public void Initialize(string name, short frequency)
{
_name = name;
_frequency = (int)frequency;
}
public void Start()
{
if (!_running)
{
_running = true;
CrestronInvoke.BeginInvoke(DoWork);
}
}
public void Stop()
{
_running = false;
}
private void DoWork(object userObj)
{
while (_running)
{
CrestronEnvironment.Sleep(_frequency);
if (_running)
{
try
{
if (_lock.WaitForMutex(100))
{
_counter++;
if (TimerEvent != null)
{
TimerEvent(userObj,
new TimerEventArgs(String.Format("{0}: counter = {1}",
_name, _counter)));
}
_lock.ReleaseMutex();
}
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in {0}.DoWork: {1}",
_name,
e.Message);
}
}
}
}
}
}
In this example, I want to have some shared state between all of our Test2 objects. That means our class needs to block when accessing the shared _counter using _lock:
public class Test2
{
private static CMutex _lock;
private static int _counter;
private string _name;
private int _frequency;
private bool _running;
Our DoWork method gets a little easier in this example; it’s just a loop with a delay in it, no need to rely on the inner workings of CTimer Reset and Stop. But we do need to coordinate shared state amongst all our threads by waiting for a lock:
private void DoWork(object userObj)
{
while (_running)
{
CrestronEnvironment.Sleep(_frequency);
if (_running)
{
try
{
if (_lock.WaitForMutex(100))
{
_counter++;
if (TimerEvent != null)
{
TimerEvent(userObj,
new TimerEventArgs(String.Format("{0}: counter = {1}",
_name, _counter)));
}
_lock.ReleaseMutex();
}
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in {0}.DoWork: {1}",
_name,
e.Message);
}
}
}
}
Now we can have 3 timers all running at the same time, but waiting for each other as they access the same shared counter:

Timer1 fires roughly every second:

Timer2 roughly every 2 seconds:

Timer3 roughly every 3 seconds:

There’s a slight drift in time as the different timers all try to synchronize their access to the static _counter variable. I also take timestamps in SIMPL debugger as a guideline, not an absolute.
SIMPL# Pro
Moving out of SIMPL into SIMPL# Pro gives us access to the Thread class. These threads are not managed by the logic engine at all, so it’s up to us to make sure we clean up after our program.
Non-Obvious Threaded Example
Here’s a program that might be using threads under the hood to perform asynchronous actions, but we don’t explicitly define a thread anywhere. We should be careful not to cause any contention with shared state. Fortunately, this example is trivial enough not to run into that problem:
using System;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;
namespace Example3
{
public class ControlSystem : CrestronControlSystem
{
public const int MAX_CONNECTIONS = 5;
private TCPServer _server;
public ControlSystem() : base()
{
try
{
Thread.MaxNumberOfUserThreads = 25;
CrestronEnvironment.ProgramStatusEventHandler += HandleProgramStatus;
}
catch (Exception e)
{
ErrorLog.Error("Error in the constructor: {0}", e.Message);
}
}
public override void InitializeSystem()
{
try
{
_server = new TCPServer("0.0.0.0", 9999, 1000,
EthernetAdapterType.EthernetLANAdapter, MAX_CONNECTIONS);
_server.WaitForConnectionsAlways(ClientConnectionHandler);
CrestronConsole.PrintLine("\nListening for connections on {0}:{1}",
_server.AddressToAcceptConnectionFrom, _server.PortNumber);
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
private void HandleProgramStatus(eProgramStatusEventType status)
{
switch (status)
{
case (eProgramStatusEventType.Stopping):
if (_server != null)
{
CrestronConsole.PrintLine("Disconnecting all clients...");
_server.DisconnectAll();
}
break;
}
}
private void ClientConnectionHandler(TCPServer srv, uint index)
{
if (index > 0)
{
if (srv.GetServerSocketStatusForSpecificClient(index) == SocketStatus.SOCKET_STATUS_CONNECTED)
{
CrestronConsole.PrintLine("Accepted connection from {0}, client index is {1}",
srv.GetAddressServerAcceptedConnectionFromForSpecificClient(index), index);
try
{
var msg = Encoding.ASCII.GetBytes(String.Format("Hello, you are client #{0}. Type HELP if lost.\r\n", index));
srv.SendData(index, msg, msg.Length);
srv.ReceiveDataAsync(index, ClientDataReceive, 0, null);
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in ClientConnectionHandler: {0}", e.Message);
}
}
else
{
CrestronConsole.PrintLine("Client #{0} status is {1}", index, srv.GetServerSocketStatusForSpecificClient(index));
}
}
}
private void ClientDataReceive(TCPServer srv, uint index, int bytesReceived, object userObj)
{
if (bytesReceived > 0)
{
try
{
var data = srv.GetIncomingDataBufferForSpecificClient(index);
var str = Encoding.ASCII.GetString(data, 0, bytesReceived).Trim();
CrestronConsole.PrintLine("Received {0} bytes from client #{1}:", bytesReceived, index);
CrestronConsole.PrintLine(" {0}", str);
var words = str.Split(' ');
var cmd = words[0].ToUpper();
if (cmd == "BYE")
{
var msg = Encoding.ASCII.GetBytes("Bye!\r\n");
srv.SendData(index, msg, msg.Length);
CrestronConsole.PrintLine("Disconnecting client #{0}...", index);
srv.Disconnect(index);
}
else
{
if (cmd == "HELP")
{
var help = Encoding.ASCII.GetBytes("List of commands I know:\r\n" +
"BYE Disconnect from server\r\n" +
"HELP Print this help message\r\n");
srv.SendData(index, help, help.Length);
}
else
{
if (cmd == "")
{
var hello = Encoding.ASCII.GetBytes("Hello???\r\n");
srv.SendData(index, hello, hello.Length);
}
else
{
var wah = Encoding.ASCII.GetBytes(String.Format("I don't know how to {0}!\r\n", cmd));
srv.SendData(index, wah, wah.Length);
}
}
var msg = Encoding.ASCII.GetBytes("\r\n>");
srv.SendData(index, msg, msg.Length);
srv.ReceiveDataAsync(index, ClientDataReceive, 0, userObj);
}
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in ClientDataReceive: {0}", e.Message);
}
}
}
}
}
The TCPServer class creates a pool of listening sockets. Once we hit our max connections though, it won’t accept another connection until one disconnects:
public override void InitializeSystem()
{
try
{
_server = new TCPServer("0.0.0.0", 9999, 1000,
EthernetAdapterType.EthernetLANAdapter, MAX_CONNECTIONS);
_server.WaitForConnectionsAlways(ClientConnectionHandler);
CrestronConsole.PrintLine("\nListening for connections on {0}:{1}",
_server.AddressToAcceptConnectionFrom, _server.PortNumber);
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
Each time a client connects, ClientConnectionHandler gets called:
private void ClientConnectionHandler(TCPServer srv, uint index)
{
if (index > 0)
{
if (srv.GetServerSocketStatusForSpecificClient(index) == SocketStatus.SOCKET_STATUS_CONNECTED)
{
CrestronConsole.PrintLine("Accepted connection from {0}, client index is {1}",
srv.GetAddressServerAcceptedConnectionFromForSpecificClient(index), index);
try
{
var msg = Encoding.ASCII.GetBytes(String.Format("Hello, you are client #{0}. Type HELP if lost.\r\n", index));
srv.SendData(index, msg, msg.Length);
srv.ReceiveDataAsync(index, ClientDataReceive, 0, null);
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in ClientConnectionHandler: {0}", e.Message);
}
}
else
{
CrestronConsole.PrintLine("Client #{0} status is {1}", index, srv.GetServerSocketStatusForSpecificClient(index));
}
}
}
index tells us which socket accepted the connection. We have to make sure we send data back to the correct client by passing index to SendData. We also setup a handler to receive data:
private void ClientDataReceive(TCPServer srv, uint index, int bytesReceived, object userObj)
{
if (bytesReceived > 0)
{
try
{
var data = srv.GetIncomingDataBufferForSpecificClient(index);
var str = Encoding.ASCII.GetString(data, 0, bytesReceived).Trim();
CrestronConsole.PrintLine("Received {0} bytes from client #{1}:", bytesReceived, index);
CrestronConsole.PrintLine(" {0}", str);
var words = str.Split(' ');
var cmd = words[0].ToUpper();
if (cmd == "BYE")
{
var msg = Encoding.ASCII.GetBytes("Bye!\r\n");
srv.SendData(index, msg, msg.Length);
CrestronConsole.PrintLine("Disconnecting client #{0}...", index);
srv.Disconnect(index);
}
else
{
if (cmd == "HELP")
{
var help = Encoding.ASCII.GetBytes("List of commands I know:\r\n" +
"BYE Disconnect from server\r\n" +
"HELP Print this help message\r\n");
srv.SendData(index, help, help.Length);
}
else
{
if (cmd == "")
{
var hello = Encoding.ASCII.GetBytes("Hello???\r\n");
srv.SendData(index, hello, hello.Length);
}
else
{
var wah = Encoding.ASCII.GetBytes(String.Format("I don't know how to {0}!\r\n", cmd));
srv.SendData(index, wah, wah.Length);
}
}
var msg = Encoding.ASCII.GetBytes("\r\n>");
srv.SendData(index, msg, msg.Length);
srv.ReceiveDataAsync(index, ClientDataReceive, 0, userObj);
}
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in ClientDataReceive: {0}", e.Message);
}
}
}
Again, we have to pay special attention to which client we send responses to. If we receive a BYE command, we disconnect the client and don’t call ReceiveDataAsync.
Thread
One thing that bothers me about our server example is: if a client disconnects without saying BYE, it can block other clients from connecting (if we’ve reached the limit). It would be great to have an inactivity timer automatically disconnect abandoned clients.
Let’s tweak the control system class to add inactivity timers to our connected clients:
public class ControlSystem : CrestronControlSystem
{
public const int MAX_CONNECTIONS = 5;
public const int INACTIVITY_DELAY_MS = 10000;
private TCPServer _server;
private Thread[] _timers;
private int[] _lastActivity;
public ControlSystem() : base()
{
We’ll need to instantiate our new arrays, too:
public override void InitializeSystem()
{
try
{
_timers = new Thread[MAX_CONNECTIONS];
_lastActivity = new int[MAX_CONNECTIONS];
_server = new TCPServer("0.0.0.0", 9999, 1000,
EthernetAdapterType.EthernetLANAdapter, MAX_CONNECTIONS);
_server.WaitForConnectionsAlways(ClientConnectionHandler);
CrestronConsole.PrintLine("\nListening for connections on {0}:{1}",
_server.AddressToAcceptConnectionFrom, _server.PortNumber);
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
When a client connects, we can start up a new thread to check for stale connections:
private void ClientConnectionHandler(TCPServer srv, uint index)
{
if (index > 0)
{
if (srv.GetServerSocketStatusForSpecificClient(index) == SocketStatus.SOCKET_STATUS_CONNECTED)
{
CrestronConsole.PrintLine("Accepted connection from {0}, client index is {1}",
srv.GetAddressServerAcceptedConnectionFromForSpecificClient(index), index);
try
{
var msg = Encoding.ASCII.GetBytes(String.Format("Hello, you are client #{0}. Type HELP if lost.\r\n", index));
srv.SendData(index, msg, msg.Length);
_lastActivity[index - 1] = CrestronEnvironment.TickCount;
_timers[index - 1] = new Thread(CheckForActivity, index);
srv.ReceiveDataAsync(index, ClientDataReceive, 0, null);
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in ClientConnectionHandler: {0}", e.Message);
}
}
else
{
CrestronConsole.PrintLine("Client #{0} status is {1}", index, srv.GetServerSocketStatusForSpecificClient(index));
}
}
}
Our new thread is a simple loop that checks if the client is still connected (did they already say bye?) and how long ago we received input from them:
private object CheckForActivity(object userObj)
{
var index = (uint)userObj;
CrestronConsole.PrintLine("Started inactivity thread for client #{0}...", index);
while (_server.ClientConnected(index))
{
Thread.Sleep(INACTIVITY_DELAY_MS);
if (_lastActivity[index - 1] + INACTIVITY_DELAY_MS < CrestronEnvironment.TickCount)
{
var msg = Encoding.ASCII.GetBytes("\r\nGoodbye?\r\n");
_server.SendData(index, msg, msg.Length);
_server.Disconnect(index);
break;
}
}
CrestronConsole.PrintLine("Leaving inactivity thread for client #{0}...", index);
return null;
}
The last piece of this example if updating the time for the last received data:
private void ClientDataReceive(TCPServer srv, uint index, int bytesReceived, object userObj)
{
if (bytesReceived > 0)
{
try
{
var data = srv.GetIncomingDataBufferForSpecificClient(index);
var str = Encoding.ASCII.GetString(data, 0, bytesReceived).Trim();
CrestronConsole.PrintLine("Received {0} bytes from client #{1}:", bytesReceived, index);
CrestronConsole.PrintLine(" {0}", str);
_lastActivity[index - 1] = CrestronEnvironment.TickCount;
var words = str.Split(' ');
Now, if the user forgets to type BYE, they’ll be disconnected after 10 seconds of inactivity. We could extend that period longer if this wasn’t just an example.
4-Series C#
I’ll be honest, I can’t think of a great example for multithreading, so I did some searching online for good examples and they all seem pretty trivial. So, I think I’ll just rewrite our Example3 program into a more multithreaded example that doesn’t need Crestron’s sandboxed classes.
Thread
In our last example, we’ll see much more clearly where new threads get created. I’ve removed the inactivity timer since nothing will block new connections, but we should probably add some logic to limit how many concurrent connections we have:
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
namespace Example4
{
public class ControlSystem : CrestronControlSystem
{
private TcpListener _server;
private bool _listening;
private List<TcpClient> _clients;
public ControlSystem()
: base()
{
try
{
CrestronEnvironment.ProgramStatusEventHandler += HandleProgramEvent;
}
catch (Exception e)
{
ErrorLog.Error("Error in the constructor: {0}", e.Message);
}
}
public override void InitializeSystem()
{
try
{
// Start listening on port 9999
_server = new TcpListener(System.Net.IPAddress.Parse("0.0.0.0"), 9999);
_server.Start();
// Keep track of all connected clients
_clients = new List<TcpClient>();
// Handle incoming connections on a different thread
var t = new Thread(new ThreadStart(HandleConnections));
t.Start();
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
void HandleProgramEvent(eProgramStatusEventType status)
{
if (status == eProgramStatusEventType.Stopping)
{
_listening = false;
// Gracefully close all connected clients
foreach (var c in _clients)
{
c.Close();
}
// Stop the server
_server.Stop();
}
}
void HandleConnections()
{
_listening = true;
CrestronConsole.PrintLine("\n\rListening for connections on {0}",
_server.LocalEndpoint.ToString());
while (_listening)
{
try
{
// Check for pending connections and handle them
if (_server.Pending())
{
var client = _server.AcceptTcpClient();
_clients.Add(client);
// Pass client into new thread
var t = new Thread(new ParameterizedThreadStart(ClientSession));
t.Start(client);
}
else
{
// Have a snooze
Thread.Sleep(1000);
}
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in HandleConnections: {0}", e.Message);
}
}
CrestronConsole.PrintLine("\n\rNo longer accepting connections on {0}",
_server.LocalEndpoint.ToString());
}
void ClientSession(object userObj)
{
try
{
var client = (TcpClient)userObj;
CrestronConsole.PrintLine("\n\rConnected!");
using (var stream = client.GetStream())
{
SendMessage(stream, "Hello! Type HELP if you are lost.");
SendMessage(stream, "\n\r> ", false);
// Set aside some memory for incoming data
var data = new byte[256];
while (client.Connected)
{
if (stream.DataAvailable)
{
// Read in available data
var length = stream.Read(data, 0, data.Length);
if (length > 0)
{
// Convert bytes to ASCII string and split on word boundaries
var text = Encoding.ASCII.GetString(data, 0, length).Trim();
var words = text.Split(' ');
if (text.Length < 3)
{
CrestronConsole.Print("Received (hex): ");
foreach (var c in text.ToCharArray())
{
CrestronConsole.Print(" {0:X} ", c);
}
CrestronConsole.PrintLine("");
}
else
{
CrestronConsole.PrintLine("Received: \"{0}\"", text);
CrestronConsole.Print(" ");
foreach (var w in words)
{
CrestronConsole.Print(w.ToUpper() + " ");
}
CrestronConsole.PrintLine("({0})", words.Length);
// Make sure we have at least 1 word
if (words.Length > 0)
{
var cmd = words[0].ToUpper();
if (cmd == "BYE")
{
SendMessage(stream, "Bye!");
break;
}
else
{
if (cmd == "HELP")
{
SendMessage(stream, "List of commands that I know:");
SendMessage(stream, " BYE - Disconnect from server");
SendMessage(stream, " HELLO [name] - Say hello to all other connected users");
SendMessage(stream, " HELP - Print this help message");
}
else if (cmd == "HELLO")
{
foreach (var c in _clients)
{
if (!c.Equals(client))
{
var otherStream = c.GetStream();
SendMessage(otherStream, String.Format("** Greetings from {0}! **", words[1]));
}
}
}
else
{
SendMessage(stream, "What are you yammering about?");
}
}
}
}
// Ready for more input
SendMessage(stream, "\n\r> ", false);
}
}
else
{
// Have a snooze
Thread.Sleep(100);
}
}
}
if (client.Connected)
{
client.Close();
}
_clients.Remove(client);
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in ClientSession: {0}", e.Message);
}
}
void SendMessage(NetworkStream stream, string msg, bool newline=true)
{
var crlf = newline ? "\r\n" : "";
var bytes = Encoding.ASCII.GetBytes(msg + crlf);
stream.Write(bytes, 0, bytes.Length);
}
}
}
On 4-series, we can use the TcpListener class. Note that parsing the IP address, we have to specify which class to use since there is a naming conflict with Crestron’s namespace. We’ll keep a list of all connected clients, and this is an area where we should probably lock access since we’ll be touching it from multiple threads. To keep our time in InitializeSystem short, we’ll hand control off to another thread to manage incoming connections.
public override void InitializeSystem()
{
try
{
// Start listening on port 9999
_server = new TcpListener(System.Net.IPAddress.Parse("0.0.0.0"), 9999);
_server.Start();
// Keep track of all connected clients
_clients = new List<TcpClient>();
// Handle incoming connections on a different thread
var t = new Thread(new ThreadStart(HandleConnections));
t.Start();
}
catch (Exception e)
{
ErrorLog.Error("Error in InitializeSystem: {0}", e.Message);
}
}
The HandleConnections method checks to see if there are any pending connections then accepts them:
void HandleConnections()
{
_listening = true;
CrestronConsole.PrintLine("\n\rListening for connections on {0}",
_server.LocalEndpoint.ToString());
while (_listening)
{
try
{
// Check for pending connections and handle them
if (_server.Pending())
{
var client = _server.AcceptTcpClient();
_clients.Add(client);
// Pass client into new thread
var t = new Thread(new ParameterizedThreadStart(ClientSession));
t.Start(client);
}
else
{
// Have a snooze
Thread.Sleep(1000);
}
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in HandleConnections: {0}", e.Message);
}
}
CrestronConsole.PrintLine("\n\rNo longer accepting connections on {0}",
_server.LocalEndpoint.ToString());
}
Each time a client connects, we spin up a new thread to handle the session. There are likely much better ways to handle an interactive prompt, but I tried to keep it simple:
void ClientSession(object userObj)
{
try
{
var client = (TcpClient)userObj;
CrestronConsole.PrintLine("\n\rConnected!");
using (var stream = client.GetStream())
{
SendMessage(stream, "Hello! Type HELP if you are lost.");
SendMessage(stream, "\n\r> ", false);
// Set aside some memory for incoming data
var data = new byte[256];
while (client.Connected)
{
if (stream.DataAvailable)
{
// Read in available data
var length = stream.Read(data, 0, data.Length);
if (length > 0)
{
// Convert bytes to ASCII string and split on word boundaries
var text = Encoding.ASCII.GetString(data, 0, length).Trim();
var words = text.Split(' ');
if (text.Length < 3)
{
CrestronConsole.Print("Received (hex): ");
foreach (var c in text.ToCharArray())
{
CrestronConsole.Print(" {0:X} ", c);
}
CrestronConsole.PrintLine("");
}
else
{
CrestronConsole.PrintLine("Received: \"{0}\"", text);
CrestronConsole.Print(" ");
foreach (var w in words)
{
CrestronConsole.Print(w.ToUpper() + " ");
}
CrestronConsole.PrintLine("({0})", words.Length);
// Make sure we have at least 1 word
if (words.Length > 0)
{
var cmd = words[0].ToUpper();
if (cmd == "BYE")
{
SendMessage(stream, "Bye!");
break;
}
else
{
if (cmd == "HELP")
{
SendMessage(stream, "List of commands that I know:");
SendMessage(stream, " BYE - Disconnect from server");
SendMessage(stream, " HELLO [name] - Say hello to all other connected users");
SendMessage(stream, " HELP - Print this help message");
}
else if (cmd == "HELLO")
{
foreach (var c in _clients)
{
if (!c.Equals(client))
{
var otherStream = c.GetStream();
SendMessage(otherStream, String.Format("** Greetings from {0}! **", words[1]));
}
}
}
else
{
SendMessage(stream, "What are you yammering about?");
}
}
}
}
// Ready for more input
SendMessage(stream, "\n\r> ", false);
}
}
else
{
// Have a snooze
Thread.Sleep(100);
}
}
}
if (client.Connected)
{
client.Close();
}
_clients.Remove(client);
}
catch (Exception e)
{
CrestronConsole.PrintLine("Exception in ClientSession: {0}", e.Message);
}
}
I’ve highlighted a new command we added to say hello to all other connected clients. So you could end up with something like:

Summary
I know this post was a bit long but I wanted to make sure I got enough general information about multithreaded programming into one place. I think there are plenty of opportunities to write about specific scenarios that involve multithreaded code in the future.
To recap, we covered:
- 3-series Sandbox Options
- SIMPL Windows
- CTimer
- BeginInvoke
- SIMPL# Pro
- Crestron Thread
- SIMPL Windows
- 4-series Options
- .NET Thread
Writing the 4-series example program ended up being much easier because I could ignore Crestron documentation and just read what Microsoft has available online.
Thanks for reading! If you want the code, it’s available in my GitHub.