Objective: manage multiple threads from C# with a responsive GUI
Description:: In this example, I simulate encoding from a source that is splittable in segments. It could be the b-frames between I-frames in a video or even some sort of audio codec. To simulate the workload, I use Thread.Sleep(TimeInMs).
For this code, I have 4 threads working on a source to encode of 10 segments. Each threads will take a slice to encode, process it and reassemble the end result in an array, that can be utilized by another process to unify the encoded version, for example.
A word of caution though: although working with threads can be useful in some cases it also complicates the code quite a bit, especially at debugging. It’s much harder to keep track of what is going on.
Here is the code in the windows form (download the whole project in VS2008 at the end):
namespace bgGUI { using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; public partial class Form1 : Form { delegate void setTextBoxTextCallback(ArrayList a); delegate void setLabelTextCallback(string s, Label l); //delegate used to set the text on a label in a Invoke operation ArrayList videoSegments; //Imagine this as a collection video/audio segments we need to compute. int workingIndex; // This is to track where we're at in the videoSegments during the process. object lockObject = new object(); // the lock object used for the thread to safely pick a new segment to compute. public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { //Here we're gonna fake that the stack of videoSegements is full of 10 slice to encode. videoSegments = new ArrayList(); for (int i = 0; i < 20; i++) { videoSegments.Add("this is segement " + i);//for the purpose of the demonstration I'm adding a string and its index. } workingIndex = videoSegments.Count - 1; label5.Text = videoSegments.Count.ToString(); //We're starting 4 different thread with parameters (hence the use of the ParameterizedThreadStart Delegate) //Becareful though as ParameterizedThreadStart only takes method with an objet argument. Therefore it's not type safe. //an alternative, type safe way of doing it is to create an helper class. and start it with ThreadStart //more details here: http://msdn.microsoft.com/en-us/library/ts553s52.aspx //In my example, I pass as arguments a Label and an int, a time interval in ms. Thread t1 = new Thread(new ParameterizedThreadStart(tsChangeLabelText)); t1.Start(new object[] { label1, 2500 }); Thread t2 = new Thread(new ParameterizedThreadStart(tsChangeLabelText)); t2.Start(new object[] { label2, 3500 }); Thread t3 = new Thread(new ParameterizedThreadStart(tsChangeLabelText)); t3.Start(new object[] { label3, 1000 }); Thread t4 = new Thread(new ParameterizedThreadStart(tsChangeLabelText)); t4.Start(new object[] { label4, 1250 }); } private void tsChangeLabelText(object o) { //we cast the object as an array. //then we exatract the first object at index[x], then we cast it to what we want //as i said earlier, it's not type safe. use it with caution! Label l = (Label)((object[])o)[0]; int ms = (int)((object[])o)[1];// the ms is just to simulate heavy load on the thread int insideIndex = 0; // This will be the index we're going to compute while (workingIndex >= 0)// the thread will go until all the segments have been computed { lock (lockObject) // we request a lock on the object. { //We have to check again if there are segments remaining! //indeed, another thread could have snatched the last one between our while and this lock! if (workingIndex > -1) { insideIndex = workingIndex;//We're going to compute the segment located at this index in our collection; workingIndex--;//The other threads will be able to go to the next one after the end of the lock } }// now all other threads (those waiting for permission to use the lock) // will be able to check what the working index is and pick the next segment. //we display infos on the GUI using invoke since it's not the owner thread. setLabelTextCallback d = new setLabelTextCallback(setLabelText); setTextBoxTextCallback z = new setTextBoxTextCallback(setTextBoxText); this.Invoke(d, new object[] { "Working on segment #" + insideIndex, l }); //--------------------------- Thread.Sleep(ms); // simulate the heavy load of the Encoding //--------------------------- this.Invoke(d, new object[] { "DONE WITH segment #" + insideIndex, l }); videoSegments[insideIndex] = (string)videoSegments[insideIndex] + " - COMPUTED"; // let's say that we replaced the old segement with the new one this.Invoke(z, new object[] { videoSegments }); this.Invoke(d, new object[] { (workingIndex + 1).ToString(), label5 }); Thread.Sleep(3000);//this is only for you to see what's going on } } private void setLabelText(string s, Label l) { l.Text = s.ToString(); } private void setTextBoxText(ArrayList a) { //to check the order of the ouput textBox1.Text = ""; for (int i = 0; i < a.Count; i++) { textBox1.Text += a[i].ToString() + Environment.NewLine; } } } }
