简体   繁体   中英

System.Threading.Tasks.Task executed in a loop strange behavior

What I try to achieve is executing a method in a different thread for each value in a collection. I would like to measure elapsed time until all tasks are completed. My code is as follows:

private ConcurrentQueue<string> Results { get; set; }
public System.Threading.Timer Updater { get; set; }
private Dictionary<int, string> Instances { get; set; }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            this.Instances = new Dictionary<int, string>();

            this.Instances.Add(01, "A");
            this.Instances.Add(02, "B");
            this.Instances.Add(03, "C");
            this.Instances.Add(04, "D");
            this.Instances.Add(05, "E");
            this.Instances.Add(06, "F");
            this.Instances.Add(07, "G");
            this.Instances.Add(08, "H");
            this.Instances.Add(09, "I");
            this.Instances.Add(10, "J");

            this.Updater = 
            new System.Threading.Timer(new TimerCallback(Updater_CallBack), null, 0, 1000);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="State"></param>
        void Updater_CallBack(object State)
        {
            this.Operation();
        }

        private void Operation()
        {
            var Watcher = new System.Diagnostics.Stopwatch();
            Watcher.Restart();

            this.Results = new ConcurrentQueue<string>();

            var Tasks = new System.Threading.Tasks.Task[this.Instances.Count];

            int i = 0;

            foreach (var Pair in this.Instances)
            {
                Tasks[i] = Task.Factory.StartNew(() => { this.Tasker(Pair.Value); });
                i++;
            }

            System.Threading.Tasks.Task.WaitAll(Tasks);

            Watcher.Stop();

            var Text = new StringBuilder();
            foreach (var Result in Results) Text.Append(Result);
            System.IO.File.AppendAllText(@"D:\Tasks.TXT", Watcher.ElapsedMilliseconds.ToString() + "   " + Text.ToString() + Environment.NewLine);           
        }

        private void Tasker(string Id)
        {
            switch (Id)
            {
                case "A": Thread.Sleep(100); this.Results.Enqueue("a"); break;
                case "B": Thread.Sleep(100); this.Results.Enqueue("b"); break;
                case "C": Thread.Sleep(100); this.Results.Enqueue("c"); break;
                case "D": Thread.Sleep(100); this.Results.Enqueue("d"); break;
                case "E": Thread.Sleep(100); this.Results.Enqueue("e"); break;
                case "F": Thread.Sleep(100); this.Results.Enqueue("f"); break;
                case "G": Thread.Sleep(100); this.Results.Enqueue("g"); break;
                case "H": Thread.Sleep(100); this.Results.Enqueue("h"); break;
                case "I": Thread.Sleep(100); this.Results.Enqueue("i"); break;
                case "J": Thread.Sleep(100); this.Results.Enqueue("j"); break;
            }
        }

What I expect here is execution of the Tasker method 10 times but each time for a different result. When I have a look to my results I see following:

200   jjjjjjjjjj
101   jjjjjjgjjj
101   hhhhhhhhjj
101   hjjjjjjjjj
101   jjjjjjjjhj
100   jjjjjjjjjh
101   jjjjjjjjjj

Tasker method is executed for 'j' multiple times. I cannot figure out why the method is not executed for other letters in the collection. What am I missing here?

I think essentially the issue is that you're accessing the Pair variable from within the action you are passing to Task.Factory.StartNew.

foreach (var Pair in this.Instances)
{
    Tasks[i] = Task.Factory.StartNew(() => { this.Tasker(Pair.Value); });
    i++;
}

You have to realize that this means you aren't reading the variable "Pair" right away, you're telling the task factory to queue a thread pool thread. And then when that thread gets started (which could happen after an indeterminate interval) you are accessing a variable in the local scope of that thread, which has already changed because the foreach loop wasn't waiting around for the task to start.

You can fix it like this:

foreach (var Pair in this.Instances)
{
    var currentValue = Pair.Value;
    Tasks[i] = Task.Factory.StartNew(() => { this.Tasker(currentValue); });
    i++;
}

Now you're making a separate locally scoped variable for each iteration of the foreach which shouldn't change on you.

As an aside, I think a separate logical problem with your code is that you are reusing the same ConcurrentQueue reference per iteration of the outer loop, but you are changing it each time. You should not have this line at all:

private ConcurrentQueue<string> Results { get; set; }

You should be using some better way of managing the results of each thread group. You could use a ConcurrentQueue of ConcurrentQueues in this case... but I'd personally instead use task parameters. Create the ConcurrentQueue locally in your Operation method and pass it to the Tasker methods as a secondary parameter.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM