Опубликован: 05.03.2013 | Уровень: для всех | Доступ: платный
Лекция 3:

Работа с потоками

Передача параметров

Общение с потоком (передача параметров, возвращение результатов) можно реализовать с помощью глобальных переменных.

 class Program 
 { 
   static long Factorial(long n) 
   { 
     long res = 1; 
     do 
     { 
       res = res * n; 
     } while(--n > 0); 
     return res; 
   } 
   static void Main() 
   {   
     long res1, res2; 
     long n1 = 5000, n2 = 10000; 
     Thread t1 = new Thread(() => 
       { 
         res1 = Factorial(n1)) 
       }); 
     Thread t2 = new Thread(() => { res2=Factorial(n2); }); 
     // Запускаем потоки 
     t1.Start();  t2.Start(); 
     // Ожидаем завершения потоков 
     t1.Join();  t2.Join(); 
     Console.WriteLine("Factorial of {0} equals {1}", 
                  n1, res1); 
     Console.WriteLine("Factorial of {0} equals {1}",  
                 n2, res2); 
   } 
 } 
  

Существует возможность передать параметры в рабочий метод потока с помощью перегрузки метода Start. Сигнатура рабочего метода строго фиксирована – либо без аргументов, либо только один аргумент типа object. Поэтому при необходимости передачи нескольких параметров в рабочем методе необходимо выполнить правильные преобразования.

       class Program 
       { 
         static double res; 
         static void ThreadWork(object state) 
         { 
           string sTitle = ((object[])state)[0] as string; 
           double d = (double)(((object[])state)[1]);  
           Console.WriteLine(sTitle); 
           res = SomeMathOperation(d); 
         } 
         static void Main() 
         { 
           Thread thr1 = new Thread(ThreadWork); 
           thr1.Start(new object[] {"Thread #1", 3.14}); 
           thr1.Join(); 
           Console.WriteLine("Result: {0}", res); 
     
         }   
       } 
  

Работа в лямбда-выражениях и анонимных делегатах с общими переменными может приводить к непредсказуемым результатам.

       for(int i=0; i<10; i++) 
       { 
         Thread t = new Thread(() => 
             Console.Write("ABCDEFGHIJK"[i])) 
         t.Start();  
       } 
        
  

Ожидаем получить все буквы в случайном порядке, а получаем

       BDDDEEJJKK 
  

Если в строковой константе оставить только 10 букв, полагая, что индекс i может быть от 0 до 9, получаем ошибку "Индекс вышел за границы массива".

Проблема связана с тем, что при объявлении потока делегат метода или лямбда-выражение содержит только ссылку на индекс i. Когда созданный поток начинает свою работу фактическое значение индекса уже давно убежало вперед. Последнее значение индекса равно 10, что и приводит к возникновению исключения. Исправить данный фрагмент можно с помощью дополнительной переменной, которая на каждой итерации сохраняет текущее значение индекса.

       for(int i=0; i<10; i++) 
       { 
         int i_copy = i; 
         Thread t = new Thread(new delegate( 
             Console.Write("ABCDEFGHIJK"[i_copy])) 
         t.Start();  
       } 
  

Теперь у каждого потока своя независимая переменная i_copy c уникальным значением. В результате получаем:

    ABCDEFGHIJK
    

или в произвольном порядке, но все буквы по одному разу

    BDACFGHEIJK
    

Приостановление потока

Метод Sleep() позволяет приостановить выполнение текущего потока на заданное число миллисекунд:

       // Приостанавливаем поток на 100 мс 
       Thread.Sleep(100); 
       // Приостанавливаем поток на 5 мин 
       Thread.Sleep(TimeSpan.FromMinute(5)); 
  

Если в качестве аргумента указывается ноль Thread.Sleep(0), то выполняющийся поток отдает выделенный квант времени и без ожидания включается в конкуренцию за процессорное время. Такой прием может быть полезен в отладочных целях для обеспечения параллельности выполнения определенных фрагментов кода.

Например, следующий фрагмент

     static void ThreadFunc(object o) 
     { 
       for(int i=0; i<20; i++) 
         Console.Write(o); 
     } 
     static void Main() 
     { 
       Thread[] t = new Thread[4]; 
       for(int i=0; i<4; i++) 
         t[i] = new Thread(ThreadFunc); 
        
       t[0].Start("A"); t[1].Start("B"); 
       t[2].Start("C"); t[3].Start("D"); 
        
       for(int i=0; i<4; i++) 
         t[i].Join(); 
     } 
  

Выводит на консоль:

     BBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAACCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDD 
  

Параллельность не наблюдается, так как каждый поток за выделенный квант процессорного времени успевает обработать все 20 итераций. Изменим тело цикла рабочей функции:

       static void ThreadFunc(object o) 
       { 
         for(int i=0; i<20; i++) 
         { 
           Console.Write(o); 
           Thread.Sleep(0); 
         } 
       } 
  

Вывод стал более разнообразный:

    AAAACACACACACACACACACACACACACACACAACCBBDDDBDBDBDBDBDBDBDBDBDBDBDBDBCCCDBBBDDBBDD
    

Существует аналог метода Thread.Sleep(0), который позволяет вернуть выделенный квант – Thread.Yield(). При этом возврат осуществляется только в том случае, если для ядра, на котором выполняется данный поток, есть другой готовый к выполнению поток. Неосторожное применение методов Thread.Sleep(0) и Thread.Yield() может привести к ухудшению быстродействия из-за не оптимального использования кэш-памяти.

Светлана Папшева
Светлана Папшева
Россия
Валерий Субботин
Валерий Субботин
Украина