Home > Back-end >  Two timers causing conflict in WinForms application
Two timers causing conflict in WinForms application

Time:12-30

I have two timers (System.Windows.Forms.Timer) in a WinForms application.

Both timers start at the same time. One timer starts and stops throughout the life of the program updating three labels, the other just runs and does its work at every tick event updating one label. However, when the first timer is running code in its tick event the second timer isn't running.

In the first timer tick event code I have inserted multiple System.Threading.Thread.Yield(); statements, but the second timer is still being blocked. Searches for this comes up null.

I tried using system threading for the second timer, but it isn't doing anything.

I'm at a loss. Any ideas?

public partial class fMain2 : Form
{
    private System.Windows.Forms.Timer timer;
    private System.Windows.Forms.Timer timer2;
    private Thread tThread;
    
    private int totTime;
    private int curTime;
    private int exTime = 0;
    public int runTime = 0;
    
    private void cmdRun_Click(object sender, EventArgs e)
    {
        //calculate total time
        totTime = iCount * iDuration;
        lblTotTime.Text = totTime.ToString();
        lblTotEx.Text = exTime.ToString();
        System.Threading.Thread.Yield();

        curTime = int.Parse("0"   txtDuration.Text);
        
        System.Threading.Thread.Yield();

        this.Refresh();

        strFile = "Begin"   ".wav";
        snd.SoundLocation = strSoundFilePath   strFile;
        snd.PlaySync();
        
        //select first item in the listview
        lvTimer.Items[0].Selected = true;
        lvi = lvTimer.Items[0];
        lvTimer.Refresh();
        
        strFile = lvi.SubItems[1].Text   ".wav";
        snd.SoundLocation = strSoundFilePath   strFile;
        snd.PlaySync();
        System.Threading.Thread.Yield();

        strFile = "Go"   ".wav";
        snd.SoundLocation = strSoundFilePath   strFile;
        snd.PlaySync();
        
        //attempted using a thread for timer2
        timer.Start();
        //tThread = new Thread(new ThreadStart(timer2.Start));

        timer2.Start();
        //tThread.Start();
    }
        
    private void timerTick(object sender, EventArgs e)
    {
        string strFile;
        
        curTime -= 1;
        totTime -= 1;
        
        exTime  = 1;

        System.Threading.Thread.Yield();

        lblCurTime.Text = curTime.ToString();
        lblTotTime.Text = totTime.ToString();
        lblTotEx.Text = exTime.ToString();
        this.Refresh();
        
        System.Threading.Thread.Yield();

        if (curTime == 0)
        {
            timer.Stop();

            System.Threading.Thread.Yield();
            System.Threading.Thread.Yield();

            strFile = "Stop"   ".wav";
            snd.SoundLocation = strSoundFilePath   strFile;
            snd.PlaySync();

            System.Threading.Thread.Yield();
            System.Threading.Thread.Yield();
            
            if (totTime == 0)
            {
                //this marks the end of the program
                timer2.Stop();
                //tThread.Abort();
                
                //more code but not relevant
                return;
            }
            else
            { //we are still working down the listview
                
                try
                {
                    lvi = lvTimer.Items[lvi.Index   1];
                    lvTimer.Items[lvi.Index].Selected = true;
                    lvTimer.FocusedItem = lvTimer.Items[lvi.Index];
                    lvTimer.Refresh();
                    System.Threading.Thread.Yield();
                    System.Threading.Thread.Yield();
                    System.Threading.Thread.Yield();
                }
                catch (IndexOutOfRangeException ei)
                {
                    strFile = "End"   ".wav";
                    snd.SoundLocation = strSoundFilePath   strFile;
                    snd.PlaySync();
                    bRunning = false;
                    ResetTime();
                    return;
                }

                curTime = int.Parse("0"   txtDuration.Text);

                lblCurTime.Text = curTime.ToString();
                lblTotTime.Text = totTime.ToString();

                System.Threading.Thread.Yield();
                System.Threading.Thread.Yield();
                
                //I'm wondering if the soundplayer is causing the problem
                strFile = lvi.SubItems[1].Text   ".wav";
                snd.SoundLocation = strSoundFilePath   strFile;
                System.Threading.Thread.Yield();
                System.Threading.Thread.Yield();

                snd.PlaySync();

                System.Threading.Thread.Yield();
                System.Threading.Thread.Yield();
                
                strFile = "Go"   ".wav";
                snd.SoundLocation = strSoundFilePath   strFile;
                snd.PlaySync();
                
                System.Threading.Thread.Yield();
                System.Threading.Thread.Yield();
                
                System.Threading.Thread.Yield();
                
                timer.Start();
            }
        }
    }
    
    private void timer2Tick(object sender, EventArgs e)
    {
        //this is all timer2 does.  It runs as long as the 
        //  program is running.
        runTime  = 1;
        lblTotTotal.Text = (runTime / 60).ToString()
              ":"   (runTime % 60).ToString("00");
    }
}

I am using VS 2017.

CodePudding user response:

System.Windows.Forms.Timer is designed to run code on the UI thread. Calling System.Threading.Thread.Yield() tells the system to run another thread that is ready to run on the current core, but your timers want to run on the UI thread so it doesn't help in any way.

It seems to me that you're blocking the UI thread playing your sound with .PlaySync().

I'm going to suggest that you push the playing of the sound to a background thread to free up the UI.

From what I can gather from your code, you're trying to play a series of sounds like this:

"Begin.wav", then "ListItem1.wav", then"Go.wav"
(wait a period of time)
"ListItem2.wav", then "Go.wav"
(wait a period of time)
"ListItem3.wav", then "Go.wav"
(wait a period of time)
"End.wav"

However, if a timer runs out then cancel playing these sounds and play a "Stop.wav" sound instead.

This structure can be modelled with a Queue<Queue<string>> and you just need to a nested dequeue and pay all of the sounds.

Queue<Queue<string>> queue =
    new Queue<Queue<string>>(new[]
    {
        new Queue<string>(new[] { "Begin.wav", "ListItem1.wav", "Go.wav" }),
        new Queue<string>(new[] { "ListItem2.wav", "Go.wav" }),
        new Queue<string>(new[] { "ListItem3.wav", "Go.wav" }),
        new Queue<string>(new[] { "End.wav" }),
    });
    

Here's the code to dequeue the queues:

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

Task task = Task.Run(async () =>
{
    while (queue.Any())
    {
        if (ct.IsCancellationRequested)
        {
            break;
        }
        Queue<string> inner = queue.Dequeue();
        while (inner.Any())
        {
            if (ct.IsCancellationRequested)
            {
                break;
            }
            string soundLocation = inner.Dequeue();
            using (System.Media.SoundPlayer sp = new System.Media.SoundPlayer())
            {
                sp.SoundLocation = soundLocation;
                sp.PlaySync();
            }
        }
        if (ct.IsCancellationRequested)
        {
            using (System.Media.SoundPlayer sp = new System.Media.SoundPlayer())
            {
                sp.SoundLocation = soundLocationStop;
                sp.PlaySync();
            }
        }
        else
        {
            await Task.Delay(TimeSpan.FromSeconds(2.0));
        }
    }
});

Note that this is all run in a Task.Run so it's not blocking the UI thread.

To stop the processing of the sound, just call cts.Cancel();.

You now just need to build your queue in the cmdRun_Click method.

private void cmdRun_Click(object sender, EventArgs e)
{
    string[][] soundLocationGroups =
    (
        from x in lvTimer.Items.Cast<ListViewItem>()
        from y in x.SubItems.Cast<ListViewItem.ListViewSubItem>()
        select new[] { Path.Combine(strSoundFilePath, $"{y.Text}.wav"), Path.Combine(strSoundFilePath, $"Go.wav") }
    ).ToArray();
        
    soundLocationGroups =
        soundLocationGroups
            .Take(1)
            .Select(xs => xs.Prepend(Path.Combine(strSoundFilePath, $"Begin.wav")).ToArray())
            .Concat(soundLocationGroups.Skip(1))
            .Append(new[] { Path.Combine(strSoundFilePath, $"End.wav") })
            .ToArray();
            
    string soundLocationStop = Path.Combine(strSoundFilePath, $"Stop.wav");             

    Queue<Queue<string>> queue = new Queue<Queue<string>>(soundLocationGroups.Select(x => new Queue<string>(x)));
    

You still need a single timer to know if you should call cts.Cancel().

I did test my Task.Run code before posting. It works.

CodePudding user response:

My suggestion is to use async/await.

Step 1: Get rid of all the System.Threading.Thread.Yield(); calls.
Step 2: Get rid of all the .Refresh(); calls.
Step 3: Make the timerTick async: private async void timerTick(object sender, EventArgs e)
Step 4: Replace every occurrence of snd.PlaySync(); with await Task.Run(() => snd.PlaySync());

The Task.Run method invokes the specified lambda on the ThreadPool. The await allows the current thread to continue doing other things, like responding to user input, while the lambda is running on the ThreadPool. After the lambda completes, the current thread continues executing the code that follows the await, until the next await, or until the end of the handler.

After doing these changes the UI should be responsive again, and the other timer should be ticking normally every second.

Be aware that by making the timerTick handler async, it is now possible to be invoked in an overlapping manner. It's up to you to prevent the overlapping, by checking and updating fields of the form.

  • Related