Zmiana wartości zmiennej po przekazaniu do funkcji

0

Próbuję napisać aplikację, która dzieli zadanie obliczeniowe na 4 części i uruchamia je w 4 wątkach, potem przechodzi dalej po skończeniu tych 4 wątków.

Jak czytałem w Google, jak uruchomić wątek z parametrem, to trafiłem na postać new Thread(() => ThrProc(i)). Z tym wiąże się jeden problem.

Problem polega na tym, że obiekt typu Thread zostaje utworzony w momencie, gdy zmienna i ma pewną wartość, ale jak wątek wystartuje przy następnej iteracji pętli lub po jej skończeniu, to zmiana zmiennej pętli powoduje zmianę parametru funkcji. Z czego to może wynikać? Przecież w danym momencie zmienna i ma określoną wartość i z taką wartością tworzę obiekt, a potem uruchamiam wątek. Co ma stan zmiennej pętli do obiektu wątku, który został już zbudowany, tym bardziej, że zmienną przekazuje się przez wartość, a nie przez referencję.

A jak się zadeklaruje dodatkową zmienną i do nie przypisze parametr, to tego problemu już nie ma. A może nadal jest, tylko prawdopodobieństwo jego wystąpienia jest marginalne i nie miałem przypadku, żeby problem wystąpił (bo jednak dokończenie iteracji pętli, inkrementacja iteratora, sprawdzenie warunku zakończenia i rozpoczęcie nowej iteracji trochę trwa, dopiero potem dodatkowa zmienna dostaje nową wartość)?

Czy powyższy sposób i sposób z ParameterizedThreadStart są równoważne?

Jak widać, są 4 warianty tego samego.
Wariant 1: W nim następuje zmiana argumentu po podaniu do funkcji
Wariant 2: Dodatkowa zmienna rozwiązuje problem w wariancie 1
Wariant 3: Inny sposób uruchamiania wątku z parametrem (znaleziony później za pomocą google), nie ma problemu występującego w wariancie 1.
Wariant 4: Sposób taki, jak w wariancie 3, przekazywana zmienna typu obiekt jest zmienną zmienianą w pętli (chodziło o to, żeby nie następowała zmiana z int do object przy przekazywaniu do funkcji), tu problem nie występuje.

using System;
using System.Diagnostics;
using System.Threading;

namespace TestThr
{
    class ObjX
    {
        Thread[] Thr;
        int ThrNum = 4;

        int[][] TestData;

        // Wartosc dobrana doświadczanie, aby wątek trwał kilka sekund
        int TestLen = 100000000;

        void ThrProcObj(object N)
        {
            ThrProc((int)N);
        }

        void ThrProc(int N)
        {
            Stopwatch SW = new Stopwatch();
            SW.Start();
            Console.WriteLine("Thread " + N + " start");
            if (N < ThrNum)
            {
                // Jakieś przypadkowe obliczenia mające zająć czas procesorowi
                for (int i = 0; i < TestLen; i++)
                {
                    TestData[N][i + 2] = (TestData[N][i] + TestData[N][i + 1]) % 1000;
                }
                for (int i = 0; i < TestLen; i++)
                {
                    TestData[N][i] = (TestData[N][i] * 27) % 1000;
                }
            }
            Console.WriteLine("Thread " + N + " stop " + SW.ElapsedMilliseconds);
        }

        public void Start()
        {
            Thr = new Thread[ThrNum];
            TestData = new int[ThrNum][];
            Random TestR = new Random();
            for (int i = 0; i < ThrNum; i++)
            {
                TestData[i] = new int[TestLen + 2];
                TestData[i][0] = TestR.Next(1000);
                TestData[i][1] = TestR.Next(1000);
            }

            Stopwatch SWX = new Stopwatch();
            Console.WriteLine("Multi thread 1 start");
            SWX.Reset();
            SWX.Start();
            for (int i = 0; i < ThrNum; i++)
            {
                Thr[i] = new Thread(() => ThrProc(i));
                Thr[i].Start();
            }
            for (int i = 0; i < ThrNum; i++)
            {
                Thr[i].Join();
            }
            SWX.Stop();
            Console.WriteLine("Multi thread 1 stop " + SWX.ElapsedMilliseconds);
            Console.WriteLine("Multi thread 2 start");
            SWX.Reset();
            SWX.Start();
            for (int i = 0; i < ThrNum; i++)
            {
                int i_ = i;
                Thr[i] = new Thread(() => ThrProc(i_));
                Thr[i].Start();
            }
            for (int i = 0; i < ThrNum; i++)
            {
                Thr[i].Join();
            }
            SWX.Stop();
            Console.WriteLine("Multi thread 2 stop " + SWX.ElapsedMilliseconds);
            Console.WriteLine("Multi thread 3 start");
            SWX.Reset();
            SWX.Start();
            for (int i = 0; i < ThrNum; i++)
            {
                Thr[i] = new Thread(new ParameterizedThreadStart(ThrProcObj));
                Thr[i].Start(i);
            }
            for (int i = 0; i < ThrNum; i++)
            {
                Thr[i].Join();
            }
            SWX.Stop();
            Console.WriteLine("Multi thread 3 stop " + SWX.ElapsedMilliseconds);
            Console.WriteLine("Multi thread 4 start");
            SWX.Reset();
            SWX.Start();
            object i_obj = 0;
            for (int i = 0; i < ThrNum; i++)
            {
                Thr[i] = new Thread(new ParameterizedThreadStart(ThrProcObj));
                Thr[i].Start(i_obj);
                i_obj = ((int)i_obj) + 1;
            }
            for (int i = 0; i < ThrNum; i++)
            {
                Thr[i].Join();
            }
            SWX.Stop();
            Console.WriteLine("Multi thread 4 stop " + SWX.ElapsedMilliseconds);
        }
    }

    class MainClass
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("START");
            ObjX X = new ObjX();
            X.Start();
            Console.WriteLine("STOP");
        }
    }
}

1
andrzejlisek napisał(a):

Próbuję napisać aplikację, która dzieli zadanie obliczeniowe na 4 części i uruchamia je w 4 wątkach, potem przechodzi dalej po skończeniu tych 4 wątków.

A czemu wątki, a nie Taski?

Problem polega na tym, że obiekt typu Thread zostaje utworzony w momencie, gdy zmienna i ma pewną wartość, ale jak wątek wystartuje przy następnej iteracji pętli lub po jej skończeniu, to zmiana zmiennej pętli powoduje zmianę parametru funkcji. Z czego to może wynikać? Przecież w danym momencie zmienna i ma określoną wartość i z taką wartością tworzę obiekt, a potem uruchamiam wątek. Co ma stan zmiennej pętli do obiektu wątku, który został już zbudowany, tym bardziej, że zmienną przekazuje się przez wartość, a nie przez referencję.

Ależ tutaj: new Thread(() => ThrProc(i)) nie przekazujesz tam żadnej zmiennej, tylko delegat do kodu (w formie lambdy), który ma się wykonać w przyszłości. Ten kod korzysta z jakiejś zmiennej, a że zaczyna się wykonywać później, to ta zmienna ma inną wartość.
Obejście tego problemu jak rozumiem już znalazłeś, wartość trzeba skopiować.

0

A czemu wątki, a nie Taski?

Bo mam pewne doświadczenie i wyczucie w wątkach, nigdy nie korzytałem z Tasków. Jednakże poczytałem i sprawdziłem, jak robi się Taski i mam wrażenie, że jest to prawie to samo z tą różnicą, że jeżeli uruchomi się kilkakrotnie więcej zadań naraz niż rdzeni procesora, to system będzie kolejkować niektóre z nich. Jak się uruchomi wątki/zadania obliczeniowe, to procesor pracuje na 100% i w praktyce nie ma różnicy, czy będą to wątki, czy zadania, w obu przypadkach czas wykonania będzie ten sam. natomiast, jeżeli procedura wątku oczekuje na coś, np. otrzymanie danych przez sieć lub otrzymanie odpowiedzi na SQL, to wspomniane kolejkowanie powoduje wydłużenie czasu, przy założeniu braku ograniczenia liczby jednoczesnych połączeń i innych tego typu czynności.

Ależ tutaj: new Thread(() => ThrProc(i)) nie przekazujesz tam żadnej zmiennej, tylko delegat do kodu (w formie lambdy), który ma się wykonać w przyszłości. Ten kod korzysta z jakiejś zmiennej, a że zaczyna się wykonywać później, to ta zmienna ma inną wartość.
Obejście tego problemu jak rozumiem już znalazłeś, wartość trzeba skopiować.

Faktycznie, znalazłem obejście problemu, w przypadku Task jest dokładnie ten sam problem i w ten sam sposób można go obejść.

Koniec końców wygląda na to, że nie ma lepszego mechanizmu. Przy zadaniach oczekujących lepsze wątki, a przy zadaniach obliczeniowych wszystko jedno.

Ulepszyłem swój program testowy i dostałem taki output przy uruchamianiu 20 wątków/zadań (nazwa wariantu, liczba różnych wątków, czas wykonania):

ThreadWait=False
Single thread  1  38143
Multi thread 1  20  8513
Multi thread 2  20  9713
Multi thread 3  20  9843
Multi thread 4  20  9777
Task 1  6  11
Task 2  7  11051
Task 3  8  9692
ThreadWait=True
Single thread  1  20005
Multi thread 1  20  1057
Multi thread 2  20  1035
Multi thread 3  20  1040
Multi thread 4  20  1040
Task 1  5  1000
Task 2  9  3000
Task 3  10  2002

W przypadku 3 wątków nie ma większej różnicy, czy Thread czy Task.

ThreadWait=False
Single thread  1  5766
Multi thread 1  3  1806
Multi thread 2  3  2058
Multi thread 3  3  2070
Multi thread 4  3  2049
Task 1  3  9
Task 2  3  2027
Task 3  3  1870
ThreadWait=True
Single thread  1  3000
Multi thread 1  3  1002
Multi thread 2  3  1002
Multi thread 3  3  1002
Multi thread 4  3  1001
Task 1  2  0
Task 2  3  1000
Task 3  3  1000

Wariant Multi thread 1 i Task 1 nie liczą się, bo są uruchomione nieprawidłowo z powodu opisanego problemu.

W przypadku wątków, to użycie new Thread(new ParameterizedThreadStart(ThrProcObj)) zamiast lambda całkowicie eliminuje problem, natomiast nie udało mi się znaleźć, jak uruchomić zadanie bez użycia lambda. W dokumentacji od M$ w każdym przykładzie jest lambda.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace TestThr
{
    class ObjX
    {
        // https://docs.microsoft.com/pl-pl/dotnet/api/system.threading.tasks.task?view=net-5.0
        // https://docs.microsoft.com/pl-pl/dotnet/standard/parallel-programming/task-based-asynchronous-programming

        // Drukowanie szczegolow watkow
        bool PrintDetails = false;

        // true - symulacja procesu polegajacego na oczekiwaniu na cos
        // false - symulacja procesu polegajacego na wykonaniu obliczen
        bool ThreadWait = false;

        // Liczba watkow
        int ThrNum = 20;

        Thread[] Thr;
        Task[] ThrTask;
        List<int> ThrIdList = new List<int>();

        int[][] TestData;

        // Wartosc dobrana doswiadczanie, aby watek trwal kilka sekund
        int TestLen = 100000000;

        void ThrProcObj(object N)
        {
            ThrProc((int)N);
        }

        void ThrProc(int N)
        {
            Stopwatch SW = new Stopwatch();
            SW.Start();
            if (!ThrIdList.Contains(Thread.CurrentThread.ManagedThreadId))
            {
                ThrIdList.Add(Thread.CurrentThread.ManagedThreadId);
            }
            if (PrintDetails) Console.WriteLine("[" + Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(3) + "] Thread " + N + " start");
            if (N < ThrNum)
            {
                if (ThreadWait)
                {
                    // Oczekiwanie w bezczynnosci okreslony czas
                    Thread.Sleep(1000);
                }
                else
                {
                    // Jakies przypadkowe obliczenia majace zajac czas procesorowi
                    for (int i = 0; i < TestLen; i++)
                    {
                        TestData[N][i + 2] = (TestData[N][i] + TestData[N][i + 1]) % 1000;
                    }
                    for (int i = 0; i < TestLen; i++)
                    {
                        TestData[N][i] = (TestData[N][i] * 27) % 1000;
                    }
                }
            }
            SW.Stop();
            if (PrintDetails) Console.WriteLine("[" + Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(3) + "] Thread " + N + " stop " + SW.ElapsedMilliseconds);
        }

        class TestInfo
        {
            public string Desc;
            public int Threads;
            public long Time;
            public TestInfo(string Desc_, int Threads_, long Time_)
            {
                Desc = Desc_;
                Threads = Threads_;
                Time = Time_;
            }
            public TestInfo(string Desc_)
            {
                Desc = Desc_;
                Threads = -1;
                Time = -1;
            }
        }

        public void Start()
        {
            List<TestInfo> TestInfoList = new List<TestInfo>();

            Thr = new Thread[ThrNum];
            ThrTask = new Task[ThrNum];
            TestData = new int[ThrNum][];
            Random TestR = new Random();
            for (int i = 0; i < ThrNum; i++)
            {
                TestData[i] = new int[TestLen + 2];
                TestData[i][0] = TestR.Next(1000);
                TestData[i][1] = TestR.Next(1000);
            }
            Stopwatch SWX = new Stopwatch();

            for (int ii = 0; ii < 2; ii++)
            {
                ThreadWait = (ii == 1);
                Console.WriteLine("ThreadWait=" + ThreadWait.ToString());
                TestInfoList.Add(new TestInfo("ThreadWait=" + ThreadWait.ToString()));

                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Single thread start");
                    SWX.Reset();
                    SWX.Start();
                    for (int i = 0; i < ThrNum; i++)
                    {
                        ThrProc(i);
                    }
                    SWX.Stop();
                    Console.WriteLine("Single thread stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Single thread", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }


                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Multi thread 1 start");
                    SWX.Reset();
                    SWX.Start();
                    for (int i = 0; i < ThrNum; i++)
                    {
                        Thr[i] = new Thread(() => ThrProc(i));
                        Thr[i].Start();
                    }
                    for (int i = 0; i < ThrNum; i++)
                    {
                        Thr[i].Join();
                    }
                    SWX.Stop();
                    Console.WriteLine("Multi thread 1 stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Multi thread 1", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }
                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Multi thread 2 start");
                    SWX.Reset();
                    SWX.Start();
                    for (int i = 0; i < ThrNum; i++)
                    {
                        int i_ = i;
                        Thr[i] = new Thread(() => ThrProc(i_));
                        Thr[i].Start();
                    }
                    for (int i = 0; i < ThrNum; i++)
                    {
                        Thr[i].Join();
                    }
                    SWX.Stop();
                    Console.WriteLine("Multi thread 2 stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Multi thread 2", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }

                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Multi thread 3 start");
                    SWX.Reset();
                    SWX.Start();
                    for (int i = 0; i < ThrNum; i++)
                    {
                        Thr[i] = new Thread(new ParameterizedThreadStart(ThrProcObj));
                        Thr[i].Start(i);
                    }
                    for (int i = 0; i < ThrNum; i++)
                    {
                        Thr[i].Join();
                    }
                    SWX.Stop();
                    Console.WriteLine("Multi thread 3 stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Multi thread 3", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }
                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Multi thread 4 start");
                    SWX.Reset();
                    SWX.Start();
                    object i_obj = -1;
                    for (int i = 0; i < ThrNum; i++)
                    {
                        i_obj = ((int)i_obj) + 1;
                        Thr[i] = new Thread(new ParameterizedThreadStart(ThrProcObj));
                        Thr[i].Start(i_obj);
                    }
                    for (int i = 0; i < ThrNum; i++)
                    {
                        Thr[i].Join();
                    }
                    SWX.Stop();
                    Console.WriteLine("Multi thread 4 stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Multi thread 4", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }

                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Task 1 start");
                    SWX.Reset();
                    SWX.Start();
                    for (int i = 0; i < ThrNum; i++)
                    {
                        ThrTask[i] = Task.Run(() => ThrProc(i));
                    }
                    Task.WaitAll(ThrTask);
                    SWX.Stop();
                    Console.WriteLine("Task 1 stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Task 1", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }
                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Task 2 start");
                    SWX.Reset();
                    SWX.Start();
                    for (int i = 0; i < ThrNum; i++)
                    {
                        int i_ = i;
                        ThrTask[i] = Task.Run(() => ThrProc(i_));
                    }
                    Task.WaitAll(ThrTask);
                    SWX.Stop();
                    Console.WriteLine("Task 2 stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Task 2", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }
                if (true)
                {
                    ThrIdList.Clear();
                    Console.WriteLine("Task 3 start");
                    SWX.Reset();
                    SWX.Start();
                    for (int i = 0; i < ThrNum; i++)
                    {
                        int i_ = i;
                        ThrTask[i] = Task.Factory.StartNew(() => ThrProc(i_));
                    }
                    Task.WaitAll(ThrTask);
                    SWX.Stop();
                    Console.WriteLine("Task 3 stop (" + ThrIdList.Count + ") " + SWX.ElapsedMilliseconds);
                    TestInfoList.Add(new TestInfo("Task 3", ThrIdList.Count, SWX.ElapsedMilliseconds));
                }

                Console.WriteLine();
                Console.WriteLine();
                foreach (TestInfo TestInfo_ in TestInfoList)
                {
                    Console.Write(TestInfo_.Desc);
                    if (TestInfo_.Threads >= 0)
                    {
                        Console.Write("  ");
                        Console.Write(TestInfo_.Threads);
                        Console.Write("  ");
                        Console.Write(TestInfo_.Time);
                    }
                    Console.WriteLine();
                }

            }
        }
    }

    class MainClass
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("START");
            ObjX X = new ObjX();
            X.Start();
            Console.WriteLine("STOP");
        }

    }
}

1 użytkowników online, w tym zalogowanych: 0, gości: 1