SD Forum™
Септембар 03, 2010, 05:52:24 *
Добродошли, Гост. Молим вас пријавите се или се региструјте.
Да нисте изгубили свој активациони e-mail?

Пријавите се корисничким именом, лозинком и дужином сесије
Вести: Држите своју адресу еПоште ажурном због слања приватних порука, обавештења уколико пратите неке теме/форуме и сл.
 
   Почетна   Помоћ Пријава Регистрација  
Странице: [1]   Иди доле
  Пошаљи ову тему  |  Штампај  
Аутор Тема: Објашњење: Multithreading  (Прочитано 1305 пута)
toxi
Гост
« послато: Мај 04, 2007, 18:23:16 »

ПС. Ево и другог текста о напредним областима програмирања у С#-у. Cви примери кода су дати у С# језику. Претпоставља се да знате нешто више од основа C#a.


Multithreading( вишенитно програмирање)

Разумеванје појма процеса и нити( thread-a)
Виндовс је један од оперативних система на којима је могуће извршаванје више процеса
"истовремено". Наиме, свака апликација/сервис се третира као процес, а на ваше рачунару је увек покренуто више од једног процеса. На пример можете покренути web browser и notepad и помислићете да та два процеса раде истовремено, али не раде истовремено! Процесор рачунара може да извршава код само једног програма у једном тренутку, али има способност( користећи софистициране алгоритме) да веома брзо прелази са извршавања кода једног програма на извршавање кода другог програма. На овај начин рачунар ствар илузију да сви процеси раде истовремено, иако се то уствари недогађа. Наравно, један рачунар може имати више од једног процеса али то немења начин на који се извршавају процеси, с'тим побољшањем што ће сваки од процесора бити задужен за мањи број процеса и самим тим цео систем ће радити брже.

Нит( thread) је "основна јединица грађе" процеса јер сваки процес мора имати најмање једну нит. Али, по потреби, програм( илити процес) може имати више од једне нити. Оно што је битно уочити јесте да процесор рачунара на потпуно исти начин третира и процесе и нити- он ће "мало извршавати један процес( или нит) па мало други па трећи" и тако у круг. Када ће процесор прећи са извршавања једне нити( или процеса) на другу ми никако неможемо знати, мада ипак можемо делимично утицати на време овог прелажења( објашњење следи касније).
Такође треба знати да свака од нити процеса може заузети све системске ресурсе( нпр. меморију) који су резервисани за тај процес.

Остаје питање зашто би се користило више нити у једном процесу. Коришћенје вишенитности ( multithreading) nemože стварно убрзати рад програма али може створити илузију да program ради брже, али и много више од тога! Razmotrimo primer штампања текста. Написали сте неки текст у notepad-у и желите да га иштампате. Изабраћете опцију "Print" и штампање ће почети али приметићете да ви и даље( док траје штампање) можете мењати садржај текста. Ово је могуће зато што је notepad покренуо штампање у другој нити програма а прва нит( која обрађује унос текста и остале ствари) је наставила своје извршавање, дакле обе нити се извршавају истовремено. Да није урађено на овај начин( користећи додатну нит) ви небисте били у могућности да мењате садржај текста све док штампање не буде завршено. Постоји много примера, ово је само један од њих.



Прављење додатних нити у процесу
Све што вам је потребно да бисте правили и управњали нитима налази се у namespace-у( "оквиру назива") System.Threading . Постоји гомила класа, енумерација и сл. у овом namespace-у али ће овде бити објашњене само неке( за остале погледајте документацију која долази уз visual Studio). Најосновнија класа, помоћу које се и креирају саме нити, је класа Thread. Следећи једноставни пример демонстрира коришћење ове класе.

Код:
using System;
using System.Threading;

namespace NitiOsnova
{
class MojaKlasa
{
//statička funkcija koja ispisuje brojeve od 250 do 0
private static void PisiText()
{
for( int i = 250; i > -1 ; i--)
Console.Write( i.ToString() + " ");
Console.WriteLine("\nKRAJ FUNKCIJE PISITEXT()");
}

static void Main(string[] args)
{
//napravi još jednu nit
Thread t2 = new Thread( new ThreadStart(PisiText));

//pozovi funkciju PisiText() iz niti t2
t2.Start();

//pozovi funkciju PisiText() iz osnovne niti
PisiText();

Console.Read();
}
}
}
Објаснимо код функције main(). Првом линијом кода смо направили објекат класе Thread и назвали га t2. Конструктору смо као параметар предали делегат ThreadStart. Овај делегат захтева да функција коју ће позивати има повратни тип void и да неузима никакве параметре.
Следећа линија кода стартује нит t2. Пошто смо конструктору класе Thread предали функцију PisiText(), стартовање ове нити ће позвати функцију PisiText().
Трећом линијом кода позивамо исту функцију PisiText() али из главне нити програма. На тај начин ћемо у програму имати две "копије" функције PisiText() које ће се извршавати истовремено.

Као резултат овај програм би могао да да овако нешто:


Обратите пажнју на редослед бројева- 250, 249...63,62, 250, 249... Ево шта се десило: функција PisiText() је позвана први пут из нити t2 а одмах затим и из основне нити програма. Нит t2 се извршавала неко време( и одбројала до 62) а затим је процесор одлучио да "скочи" на извршавање главне нити програма( која је баш требала да почне са извршавањем кода функције PisiText()) и због тога опет почиње исписивање бројева од 250( и трајаће све док процесор не одлучи да "скочи" на извршавање неке друге нити или процеса, а када се процесор "врати" на ову нит наставиће се са исписивањем преосталих бројева).



Манипулисање начином извршавања нити
Неможе се тачно одредити када ће процесор прећи са извршавања једне нити на извршавање друге нити/процеса, нити колико ће се задржати на тој нити. Ипак, свака нит има свој степен приоритет извршавања а класа Thread, преко својства Priority, омогућава постављање приоритета( удела процесорског времена) за дату нит. Постоји 5 степени приоритета:
Lowest, BelowNormal,Normal, AboveNormal i Highest. Подразумевано, нит има степен приоритета "Normal".
Треба разумети да се приоритети не односе на редослед извршавања нити, већ на удео( већи или мањи) процесорског времена. Због тога, нема гаранција да ће се нит са вишим приоритетом завршити пре нити са нижим приоритетом.

У току извршавања, сваки објекат класе Thread, засебно,  пролази кроз више "фаза извршавања". Све те фазе( и њихово објашњење) се налази у енумерацији ThreadState. Тако нит која још није стартована има вредност за ThreadState "Unstarted", када се покрене та вредност је "Running".



Коришћење системских нити - Thread Pooling
У горњем тексту показано је креирање нити коришћењем класе Thread, међутим то није једини начин добијања вишенитности( multithreading). Наиме, постоје и системске нити. Њих некреирамо ми већ сам оперативни систем. Постоји више предности које пружају ове нити у односу на оне које сами креирате помоћу класе Thread, а можда најбитнија предност је брже извршавање системских нити. У сваком случају препоручљиво је да користите системске нити када год је то потребно. Оне се међусобно не разликују, сем у једној ствари- ако користите системске нити, по завршетку главне нити програм се завршава, без обзира да ли је нека од споредних (системских!) нити завршила своје извршавање.
За коришћење системских нити користи се класа ThreadPool. Најбитнија функција у тој класи је QueueUserWorkItem() која од систем затражује да направи и одмах затим покрене додатну нит. Преко функције GetMaxThreads() добијате максималан број системских нити које су на располагању вашем процесу. Уколико покушате да "заузмете" већи број нити од овог броја, функција ће "блокирати" извршавање кода све док додатна нит не постане доступна. Следи пример.

Код:
using System;
using System.Threading;

namespace SistemskeNiti
{
class MojaKlasa
{
//statička funkcija koja ispisuje brojeve od 250 do 0
// mora da vrati tip void i da kao parametar dobije objekat tipa object
//da bi je mogao pozvati delegatWaitCallback
private static void PisiText(object obj)
{
for( int i = 250; i > -1 ; i--)
Console.Write( i.ToString() + " ");
Console.WriteLine("\nKRAJ FUNKCIJE PISITEXT()");
}

static void Main()
{
//napravi i pokreni sistemsku nit
ThreadPool.QueueUserWorkItem( new WaitCallback( PisiText), null);

//pokreni funkciju PisiText iz glavne niti
PisiText(null);

Console.Read();
}
}
}

Функција QueueUserWorkItem() класе ThreadPool узима два параметра- први је делегат WaitCallback() који захтева да функција враћа тип void(односно да невраћа ништа) и да узима параметар типа Object, други параметар је типа Object и он ће бити предат функцији коју ће позвати WaitCallback(). У овом случају немамо потребу за коришћењем тог објекта па предајемо вредност null која то и сигнализира.



Извршавање кода у једнаким временским интервалима - Класа Timer
Видели сте како се користе нити и све њихове предности и мане. Ипак, често ће вам бити потребно да неку функцију позивате у једнаким временским интервалима. Приметно,класу која би ово радила је релативно лако направити, међутим .NET framework садржи и Класу Timer која је врло флексибилна тако да нема потребе да "ручно" пишете код. Поред тога класа  Timer је врло штедљива на ресурсима тако да је свакако требате користити ако имате потребу да неки код( функцију) покрећете више пута у једнаким временским интервалима.
конструктор класе Timer је преоптерећен а најчешће коришћена верзија је она која узима три параметра:
-Први је делегат TimerCallback који пружа методу коју ће Timer периодично позивати.
-Други параметап је објекат који ће бити предат одређеној функцији( која је то функција одређује се првим параметром)
-Трећи параметар означава колико времена треба да протекне пре него што ће Timer периодично почети да позива одређену функцију. Дакле време које треба да протекне до првог позива.
-четврти параметар означава у ком временском интервалу ће се одређена функција позивати.

Време је изражено у милисекундама, а један секунд има 1000 милисекунди. Демострација:

Код:
using System;
using System.Threading;

namespace KlasaTimer
{
class MojaKlasa
{
static int second = 0; //proteklo vreme

//funkcija koju će pozivati Timer
static void NekiText(object obj)
{
++second;
}

static void Main(string[] args)
{
int pocetnoVreme = 1000, interval = 1000; //vremenski intervali

//pravljenje objekta klase Timer
Timer tmr = new Timer( new TimerCallback(NekiText), null, pocetnoVreme, interval);

//ovo bi trebalo da je jasno :)
Console.Write("Unesite neki tekst-> ");
Console.ReadLine();
tmr.Dispose(); // uništavanje objekta tmr. Na taj način se njegovo izvršavanje zaustavlja!!
Console.WriteLine( "Za unos teksta vam je bilo potrebno {0} sekundi!",
second);

Console.Read();
}
}
}
Program će računati koliko vam je vremena bilo potrebno da unesete proizvoljnu rečenicu. Za detaljno pojašnjenje pogledajte komentare u samom kodu.



Синхронизација - спречавање "сукоба" при дељењу ресурса између више нити
Главни проблем са нитима долази до изражаја када више нити користи исте ресурсе. Проблеми нису уопште занемарљиви а покад-кад их је врло тешко решити јер, њих компајлер неће сам увидети( јер су то лигичке грешке) већ ћете као излаз програма добити, очигледно или неочигледно, чудне резултате. За решавање ових проблема .NET платформа обезбеђује неколико класа, које у суштини раде готово исти посао али свака од њих има својих предности и мана. Те класе су: Lock( ово је у исто време и кључна реч С#-а, то је уведено јер се lock врло често користи), Monitor i Mutex. Следи пример проблематичног кода( из мени непознатог разлога овај код се најчешће среће приликом објашањавања синхронизације па ћу га зато и ја овде користити):

Код:
using System;
using System.Threading;

namespace Lock_Monitor
{
class LockClass
{
//staticka promenljiva koja ce sluziti kao brojac u funkciju WriteText
static int broj = 0;

//f-ja koja ce ispisivati prvih 10 brojeva
public static void WriteText()
{
while( broj < 10 )
{
int temp = broj;
++temp;
Console.WriteLine(Thread.CurrentThread.Name + " " + temp);
Thread.Sleep(1000);
broj = temp;
}
}

static void Main(string[] args)
{
//napravi dva thread-a koji ce pozivati funkciju WriteText()
Thread th1 = new Thread( new ThreadStart(LockClass.WriteText));
th1.Name = "thread_1";
Thread th2 = new Thread( new ThreadStart(LockClass.WriteText));
th2.Name = "thread_2";

//pokreni oba thread-a
th1.Start();   th2.Start();
Console.ReadLine();
}
}
}

На први поглед неочекивано, излаз овог програма ће бити:
Цитат
thread_1 1
thread_2 1
thread_1 2
thread_2 2
thread_1 3
thread_2 3
thread_1 4
thread_2 4
thread_1 5
thread_2 5
thread_1 6
thread_2 6
thread_1 7
thread_2 7
thread_1 8
thread_2 8
thread_1 9
thread_2 9
thread_1 10
thread_2 10

Зар није требало да буде само 11 редова? размотримо како програм ради... Најпре се у main()-у покрећу, један за другим, thread-ови th1 i th2 - оба позивају ф-ју WriteText(). Проблематичан део се налази унутар ове функције. Наиме, th1 ће први "ући" у петљу и temp променљивој ће додати вредност broj + 1 и приказаће ту вредност на екран( конзолу). После тога ће th1 "спавати"(зауставити своје извршавање) за 1 секунд( 1000 милисекунди), да би после тога broj добио вредност коју има temp. За то време "спавања", нит th2 улази у петљу( broj има вредност нула јер th1 још увек није извршио наредбу broj=temp;) и ради исту ствар као и th1, с'тим што ће th2, када "одспава" једну секунду унети нову вредност у променљиву broj- игром случаја унеће исту ону вредност коју прменљива broj већ садржи па последице неће бити велике. Thread.Sleep() смо користили само да би поједноставили објашњење.
Остаје питање како разрешити овај проблем. Можемо то извести користећи било коју од три класе које смо раније поменули. Класа( али и keyword!) Lock је најједноставнија и зато починјемо од ње.
-Решење помоћу Lock-a
Lock омогућава да одређени део кода буде "закључан" на тај начин да га само једна нит може користи у исто време. Можда је боље рећи "Lock каже: Не дозволи да било која нит користи овај део кода док га ја користим". У наредном примеру је измењен пређашњи код тако да користи lock.

Код:
using System;
using System.Threading;

namespace Lock_Monitor
{
class LockClass
{
//staticka promenljiva koja ce sluziti kao brojac u funkciju WriteText
static int broj = 0;

//f-ja koja ce ispisivati prvih 10 brojeva
public void WriteText()
{
while( broj < 10 )
{
lock(this)
{
int temp = broj + 1;
Console.WriteLine(Thread.CurrentThread.Name + " " + temp);
Thread.Sleep(1000);
broj = temp;
}
}
}

static void Main(string[] args)
{
LockClass cls = new LockClass();
//napravi dva thread-a koji ce pozivati funkciju WriteText()
Thread th1 = new Thread( new ThreadStart(cls.WriteText));
th1.Name = "thread_1";
Thread th2 = new Thread( new ThreadStart(cls.WriteText));
th2.Name = "thread_2";

//pokreni oba thread-a
th1.Start();   th2.Start();
Console.ReadLine();
}
}
}

Излаз из овог програма је:
Цитат
thread_1 1
thread_2 2
thread_1 3
thread_2 4
thread_1 5
thread_2 6
thread_1 7
thread_2 8
thread_1 9
thread_2 10
thread_1 11
Све изгледа добро сем овог једанаестог реда. Зашто се и он исписује? Петља се извршавала неколико пута...th1 је почео са новим "кругом" кроз петљу и у том тренутку broj има вредност 9.  Thread th2 такође почиње нови круг кроз петљу али се зауставља на почетку јер је, у том тренутку, код блокиран( lock-ован). th1 завршава пролаз кроз тело петље, доељивајући променљивој broj вредност 10 и код бива одблокиран тако да th2 може да настави извршавање. th2 променљивој temp додељује вредност broj + 1( а то је 11) и ето га разлог постојања тог једанаестог реда.

-Решење помоћу класе Monitor
Ако вас флексибилност коју пружа lock не задовољава, на располагању вам је класа Monitor која у суштини ради исти посао( и на исти начин) као и lock али даје нешто више опција за манипулисање. Да би претходни пример уместо lock-a користио Monitor класу потребно је да уместо:
Код:
lock(this)
{
 /* ... ostatak koda */
}
ставите ово:
Код:
Monitor.Enter( this);
 /* ... ostatak koda */
Monitor.Exit( this);
И то је све! Класа Monitor садржи пар корисних метода, а све оне су статичке. Те методе су: Enter(), Exit(), Pulse(), PulseAll(), TryEnter() i Wait(). Било би глупаво објашњавати њихово коришћење обзиром да се опис тога шта оне раде налази у MSDN-у.


Толико о multithreading-у. У следећем објашњењу( које ће говорити о GDI+-у) ћу комбиновати GDI+ и вишенитност( уз Windows Forms, наравно) да бих објаснио појам "main game loop"-а и зашто се он неможе остварити коришћењем само једног, оног основног, thread-а а самим тим изашто је multithreading чврсто "везан" са програмирањем игара.

Enjoy Patriota
Сачувана
Странице: [1]   Иди горе
  Пошаљи ову тему  |  Штампај  
 
Пребаци се на:  

Покреће MySQL Покреће PHP Powered by SMF 1.1.11 | SMF © 2006, Simple Machines LLC Исправан XHTML 1.0! Исправан CSS!
Страница је направљена за 0.111 секунди са 18 упита.

Google последњи пут посетио данас у 03:52:56