0

I've made a real simple console app that I am using to break down a problem I am having on a larger application. I feel like I must have missed something in thread 101 class but I'm at a loss of what it is. Basically, the whole premise is to have a BackgroundWorker check on a timer a collection and then if another collection doesn't contain something from the first collection, start a new Task. If any anytime the Task isn't in running or created status, I want to cancel the task and create a new one.

The two behaviors I am noticing is one when there is an exception inside of the task, I am getting errors for Car Models that don't have an error (even though the task is new), and if I use a for loop my i iteration goes to 5 and the collection never has more than 4. I've also noticed that all 4 make an error at the same time, but I know there are some weird things with Random but I'm just not sure if that's the issue at this point.

If you want to see the for iteration go to 5, just use Y for the console readline, but this only happens in debug, running the application this doesn't seem to occur. The next thing I want to make sure of is that I am canceling the task properly.

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using System.ComponentModel;
using System.IO;

namespace ThreadTest
{
    class Program
    {
        private static BackgroundWorker _backgroundWorker;

        private static void _EstablishBackgroundWorker()
        {
            _backgroundWorker = new BackgroundWorker();
            if (UseFor)
                _backgroundWorker.DoWork += _UpdateCarsFor;
            else
                _backgroundWorker.DoWork += _UpdateCars;
            Timer timer = new Timer(10000);
            timer.Elapsed += _Timer_Elapsed;
            timer.Start();
        }


        private static void _Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            while (_backgroundWorker.IsBusy)
            {

            }

            if (!_backgroundWorker.IsBusy)
                _backgroundWorker.RunWorkerAsync();
        }


        private static void _UpdateCarsFor(object sender, DoWorkEventArgs e)
        {

            string[] myCars = File.ReadAllLines(@"C:\cars\mycarfile.txt");
            for (int i = 0; i < myCars.Length; i++)
                if (!_cars.Where(x => x.Model == myCars[i].ToUpper()).Any())
                    _cars.Add(new Model.Car() { Model = myCars[i].ToUpper() });
            for (int i = 0; i < _cars.Count; i++)
            {
                if (_cars[i].Task != null && _cars[i].Task.Status != TaskStatus.Running && _cars[i].Task.Status != TaskStatus.Created)
                {
                    Console.WriteLine($"Making new task for { _cars[i].Model }");
                    _cars[i].tokenSource.Cancel();
                    _cars[i].RefreshToken();
                    _cars[i].Task = null;
                }
                if (_cars[i].Task == null)
                {
                    _cars[i].Task = new Task(() => new Manufacture.Build().MakeCar(_cars[i].Model), _cars[i].cancellationToken);
                    _cars[i].Task.Start();
                }

            }



        }

        private static void _UpdateCars(object sender, DoWorkEventArgs e)
        {
            //string[] myCars = File.ReadAllLines(@"C:\cars\mycarfile.txt");
            string[] myCars = new string[] { "F150", "RAM", "M3", "NSX" };
            for (int i = 0; i < myCars.Length; i++)
                if (!_cars.Where(x => x.Model == myCars[i].ToUpper()).Any())
                    _cars.Add(new Model.Car() { Model = myCars[i].ToUpper() });
            foreach (var car in _cars)
            {
                if (car.Task != null && car.Task.Status != TaskStatus.Running && car.Task.Status != TaskStatus.Created)
                {
                    Console.WriteLine($"Making new task for { car.Model }");
                    car.tokenSource.Cancel();
                    car.RefreshToken();
                    car.Task = null;
                }
                if (car.Task == null)
                {
                    car.Task = new Task(() => new Manufacture.Build().MakeCar(car.Model), car.cancellationToken);
                    car.Task.Start();
                }

            }



        }
        private static List<Model.Car> _cars { get; set; }
        private static bool UseFor { get; set; }
        static void Main(string[] args)
        {
            _cars = new List<Model.Car>();
            Console.WriteLine("Use for iteration? y/n");
            var result = Console.ReadLine();
            if (result.ToUpper() == "Y")
                UseFor = true;



            _EstablishBackgroundWorker();


            Console.ReadLine();
        }
    }
}

Car.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ThreadTest.Model
{
    class Car
    {
        public Car()
        {
            RefreshToken();
        }

        public string Model { get; set; }
        public Task Task { get; set; }

        public CancellationToken cancellationToken { get; set; }

        public CancellationTokenSource tokenSource { get; set; }

        public void RefreshToken()
        {
            tokenSource = new CancellationTokenSource();
            cancellationToken = tokenSource.Token;
        }

    }
}

Build.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace ThreadTest.Manufacture
{
    class Build
    {
        public void MakeCar(string car)
        {
            try
            {
                while (true)
                {
                    Random r = new Random();
                    int chaos = r.Next(0, 100);

                    Console.WriteLine($"Building {car}");

                    Thread.Sleep(2000);
                    if (chaos >= 90) throw new Exception($"Something went wrong with {car}");
                }
                

            }
            catch(Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
                throw;
            }
        }

    }
}

Here is an example of all threads erroring at the same time, but even if i turn random down to a 1% chance this still happens. All errors

In the comments with Henk, the mention of closing over the loop variable I don't believe applies because this method present a foreach and the issue of all threads erroring at the same time occurs, and the question of property canceling a task still remains.

private static void _UpdateCars(object sender, DoWorkEventArgs e)
        {
            //string[] myCars = File.ReadAllLines(@"C:\cars\mycarfile.txt");
            string[] myCars = new string[] { "F150", "RAM", "M3", "NSX" };
            for (int i = 0; i < myCars.Length; i++)
                if (!_cars.Where(x => x.Model == myCars[i].ToUpper()).Any())
                    _cars.Add(new Model.Car() { Model = myCars[i].ToUpper() });
            foreach (var car in _cars)
            {
                if (car.Task != null && car.Task.Status != TaskStatus.Running && car.Task.Status != TaskStatus.Created)
                {
                    Console.WriteLine($"Making new task for { car.Model }");
                    car.tokenSource.Cancel();
                    car.RefreshToken();
                    car.Task = null;
                }
                if (car.Task == null)
                {
                    car.Task = new Task(() => new Manufacture.Build().MakeCar(car.Model), car.cancellationToken);
                    car.Task.Start();
                }

            }



        }
  • 2
    A mix of tasks, backgroundworkers and timers is not something one should find themselves using. I will argue that there is no reason to spend time debugging it, and the right thing to do is to rewrite everything using only one of the three. – GSerg Oct 16 '21 at 15:03
  • Probably you should also provide the "C:\cars\mycarfile.txt" file. – Theodor Zoulias Oct 16 '21 at 15:12
  • 1
    `if (_UpdateBusy == false) { _UpdateBusy = true; ... }` is not thread-safe. – H H Oct 16 '21 at 15:16
  • 1
    There is a lot wrong here, and a Console app is not the ideal place for threading. Your 'foregroud' is a ReadLine(). You only need a Timer, and a lot more synchronization help. – H H Oct 16 '21 at 15:18
  • @TheodorZoulias that should be commented out and hard coded strings are there – user1257758 Oct 16 '21 at 15:18
  • @HenkHolterman This is just an app I am using to run through the threads, and I would argue the _UpdateBusy could be completely removed and still would present the issue. Plenty of apps are used to have multiple threads check 1 thing to see if it should proceed. – user1257758 Oct 16 '21 at 15:19
  • @GSerg the backgroundworker is irrelevant to the issue, the background worker is working as intended, this is about the task and cancellation. – user1257758 Oct 16 '21 at 15:20
  • 1
    We call this an [xy question](https://en.wikipedia.org/wiki/XY_problem). – H H Oct 16 '21 at 15:22
  • It seems that many things are unneeded in the example you provided, for the purpose of demonstrating the core problem. I would suggest to remove those things. The more minimal the example, the more "appetizing" will be for people how would like to answer the question. – Theodor Zoulias Oct 16 '21 at 15:23
  • @HenkHolterman What do you possibly mean, I clearly show a new Task which is threaded safe cancelling all 4 at the same time, and ask if this is an appropriate way of cancelling a task. – user1257758 Oct 16 '21 at 15:24
  • @TheodorZoulias What is unneeded? – user1257758 Oct 16 '21 at 15:25
  • Three things that are known to be unneeded so far: 1. *"that should be commented out and hard coded strings are there"* 2. *"the _UpdateBusy could be completely removed and still would present the issue."* 3. *"the backgroundworker is irrelevant to the issue"* – Theodor Zoulias Oct 16 '21 at 15:27
  • 1
    A backgroundworker is intended to interface with a synchronization context that runs a messagepump. Ie, WinForms or WPF. That is an X. – H H Oct 16 '21 at 15:28
  • But for your main problem, google "closing over the loop variable" – H H Oct 16 '21 at 15:32
  • @TheodorZoulias Well okay, since you're playing a holier than thou argument, let me explain. 1) I presented a completely working solution instead of someone creating a text file, 2) if you read the question you clearly will see that somehow my iteration in debug goes to 5 when there are only 4 arguments in the string[] and that was an attempt to relieve any chance of the background worker interfering, 3) and maybe it isn't.. Maybe you're just the smartest guy on earth I need an autograph from. – user1257758 Oct 16 '21 at 15:33
  • @HenkHolterman intended doesn't mean it has no other application of it. Hashsets can be used in very unique interesting says. – user1257758 Oct 16 '21 at 15:34
  • 1
    But a bgw without a Form is utterly silly. – H H Oct 16 '21 at 15:35
  • @HenkHolterman I'm not disputing a task or thread could be used in its place. I'm trying to examine the problem as is and I don't believe the issue has to do with a background worker adding to the collection. I kept it there because maybe someone would say oh because of this your tasks are actually handled differently. – user1257758 Oct 16 '21 at 15:39
  • @HenkHolterman I'm assuming you were the one who added the already answered, but based on jonskeet answer here: https://stackoverflow.com/a/271447/16418126 I would argue the foreach iteration doesn't require a copy. foreach (var car in _cars) and the closing over the loop variable error doesnt apply – user1257758 Oct 16 '21 at 15:45
  • 1
    Yes, old answrs. foreach() was fixed, but you still have a captured `[i]`. – H H Oct 16 '21 at 16:46

0 Answers0