Ada में सुरक्षित concurrency — task और protected object की practical guide

· अद्यतन तिथि: · · Ada, Concurrency, Tasking, Protected Objects, Rendezvous, Real-Time, Parallel Programming, Programming Language, concurrency, high-reliability

1. शुरुआत — language में ही बनी concurrency

Concurrency आधुनिक software development में ऐसा विषय है जिससे बचा नहीं जा सकता। लेकिन ज़्यादातर languages में concurrency «बाद में जोड़ी गई» चीज़ है — library या OS की सुविधा पर depend करती है, और सही इस्तेमाल के लिए गहरा ज्ञान और सावधानी से किया गया design चाहिए।

Ada के पास इस समस्या का अपना जवाब है। Concurrency language specification में ही बनी हुई है।

Ada का concurrency model:
- task — independently चलने वाली concurrent unit
- rendezvous — task के बीच synchronous communication
- protected object — language-managed mutual exclusion
- real-time priority — Annex D की real-time facilities

Task और rendezvous 1983 के Ada 83 से हैं; protected object और Annex D real-time facilities Ada 95 में जुड़े, और Ada 2005 और 2012 के साथ आगे बढ़े। Mutex और semaphore जैसे low-level synchronization primitives की जगह, Ada concurrency की सबसे बड़ी खूबी यह है कि design intent सीधे code में लिख सकते हैं

इस लेख में Ada concurrency को 8 practical code examples से step-by-step समझाया गया है। हर example स्वतंत्र snippet है — compile और run हो सकता है, और अपने मशीन पर आज़मा सकते हैं।

लेख में आए code fragments chapter-wise files में बाँटकर GitHub पर reference collection के रूप में उपलब्ध हैं।

ada-task-concurrency - komurasoft-blog-samples (GitHub)

अपने मशीन पर चलाना — build और run

«Compile और run हो सकता है» लिखा है, तो पहले वही steps दिखा देते हैं।

GNAT तैयार करें

GNAT, GCC का Ada compiler है। Linux पर apt install gnat-13, Windows पर MSYS2 का mingw-w64-x86_64-gcc-ada, या Alire (Ada / SPARK का package manager) से install कर सकते हैं।

Build करके run करें

हर snippet एक file में कई compilation units रखता है (task specification, task body, main procedure), इसलिए पहले gnatchop से split करें, फिर gnatmake करें। gnatchop GNAT के naming convention «unit name = file name» के हिसाब से files बाँटता है।

mkdir work && cd work
gnatchop ../src/snippets/01_hello_task.ada
gnatmake hello_task_demo
./hello_task_demo

Split के बाद main procedure का नाम ही executable का नाम बन जाता है।

Chapter और file का mapping

Chapter संख्या और file संख्या एक से shift हैं (Chapter 3 01_ है)। Mapping यह है:

Chapter File Executable Content
Chapter 3 01_hello_task.ada hello_task_demo Task का basic form
Chapter 4 02_rendezvous_intro.ada rendezvous_demo Rendezvous से bidirectional data transfer
Chapter 5 03_selective_accept.ada selective_accept_demo Selective accept और server task
Chapter 6 04_producer_consumer.ada producer_consumer_demo Producer-consumer
Chapter 7 05_protected_counter.ada protected_counter_demo Protected object से mutual exclusion
Chapter 8 06_bounded_buffer.ada bounded_buffer_demo Barrier वाला protected entry (bounded buffer)
Chapter 9 07_timed_entry.ada timed_entry_demo Timeout वाला select call
Chapter 10 08_task_priorities.ada task_priorities_demo Task priority और real-time scheduling

लेख के code fragments explanation के लिए ज़रूरी हिस्से ही काटे गए हैं। वैसे के वैसे नहीं चलेंगे, इसलिए हाथ से आज़माते समय ऊपर वाली files इस्तेमाल करें।

इस लेख का knowledge map

Ada का task entry और accept वाले rendezvous से बाहर के साथ synchronous communication करता है। Selective accept (select statement) कई entries को guard condition के साथ wait करता है, और or terminate से deadlock का कारण बनने वाले हमेशा wait करते server task से बचा जा सकता है। Protected object language-managed mutual exclusion है: entry की barrier condition true होने तक caller को wait कराके shared data पर exclusive access देता है और data race रोकता है; दूसरी तरफ़ protected operation के अंदर delay जैसी मना processing करने पर bounded error बनता है, और implementation के हिसाब से deadlock तक जा सकता है। Priority ceiling protocol protected object पर ceiling priority सेट करके priority inversion रोकता है, और real-time properties Rate Monotonic Scheduling की theoretical background पर टिकी हैं। Ravenscar profile task model सीमित करके static deadlock analysis संभव बनाता है।

Ada के task और protected object का knowledge mapAda का task rendezvous से synchronous communication करता है और selective accept deadlock बचाते हुए कई entries wait करता है; protected object barrier से mutual exclusion लागू करके data race रोकता है; priority ceiling protocol और Ravenscar profile real-time properties को सहारा देते हैं — दिखाने वाला diagramrequire करता हैrequire करता हैuse करता हैuse करता हैprevent करता हैuse करता हैprevent करता हैcause हो सकताcause हो सकताcause हो सकताprevent करता हैrequire करता हैuse करता हैuse करता हैrecommended उपायmitigate करता हैrequire करता हैcause हो सकतासे configuremitigate करता हैAda task (concurrency)protected objectAda (programming language)rendezvousselective accept (select statement)deadlockbarrier (protected entry का when condition)data raceerroneous executionbounded errorPriority Ceiling Protocolpriority inversiontask priority (pragma Priority)Rate Monotonic Scheduling (RMS)Ravenscar profile

Diagram में solid line हमेशा लागू रहने वाला relation दिखाती है और dashed line conditional relation दिखाती है (शर्तें detail page पर हर relation के explanation में दी गई हैं)। Relations की पूरी list (कुल 20, evidence और certainty सहित) तथा मुख्य concepts की definitions knowledge map की detail page पर संकलित हैं (जापानी में)। Data: JSON-LD / Turtle

2. Concurrency के «खतरे» याद कर लें

Ada की बात शुरू करने से पहले, «safe» concurrency क्यों ज़रूरी है, संक्षेप में देख लेते हैं।

Concurrency के typical bugs ये हैं:

  • Data race: कई threads एक ही memory location पर एक साथ access करें, और कम से कम एक write हो। Result undefined है।
  • Deadlock: कई tasks एक-दूसरे के खत्म होने का wait करते रहें, और कभी आगे न बढ़ें।
  • Priority inversion: High-priority task उस resource का wait करे जो low-priority task hold किए हुए है, और medium-priority task उस low-priority task को preempt कर दे (चल रहे task को रोककर दूसरे task पर switch करना)।
  • Starvation: कोई task कभी resource हासिल न कर पाए।

Ada का concurrency model इन समस्याओं के खिलाफ language-level safeguards देता है।

Data race → protected object exclusive access guarantee करता है
Deadlock → rendezvous model structured synchronization देता है
Priority inversion → Priority Ceiling Protocol language feature के रूप में उपलब्ध
Starvation → entry barrier और queuing policy से control

3. Task की basics — स्वतंत्र execution unit

Ada में concurrency की basic unit task है। Task thread जैसा है, लेकिन OS thread से 1-to-1 map होना ज़रूरी नहीं — scheduling Ada runtime संभालता है।

task Greeter is
   entry Start;
end Greeter;

task body Greeter is
begin
   accept Start;
   Put_Line ("Hello from a task!");
end Greeter;

इस code (01_hello_task.ada) में कुछ महत्वपूर्ण बातें हैं।

Task declaration होते ही अपने आप चलना शुरू करता है। Greeter task उस enclosing procedure के begin पर start होता है, और accept Start; पर caller की rendezvous request का wait करता है।

entry वह interface है जो task बाहर expose करता है। Caller Greeter.Start; call करता है, तो वह task के accept Start; से synchronize होता है। यही rendezvous है।

Task के खत्म होने का wait अपने आप होता है। Main procedure खत्म होते समय अगर कोई task अभी चल रहा हो, तो उसके complete होने का implicit wait होता है। यह C++ में std::thread::join भूल जाने वाले crash के विपरीत है।

Run example

gnatchop ../src/snippets/01_hello_task.ada
gnatmake hello_task_demo
./hello_task_demo
Main: starting task...
Hello from a task!
Main: task has completed.

यहाँ एक बात ध्यान रखने की है। accept Start; में do ... end block नहीं है। मतलब rendezvous होते ही दोनों free हो जाते हैं, और उसके बाद parallel चलते हैं। इसलिए आखिरी दो lines का क्रम guarantee नहीं है। कुछ environments में Main: task has completed. पहले print हो सकता है। क्रम भी fix करना हो तो जिस processing का क्रम रखना है, उसे accept Start do ... end Start; के do ... end के अंदर रखें। पहली line ज़रूर सबसे पहले आएगी, क्योंकि Greeter Greeter.Start; call होने तक accept पर रुका रहता है।

4. Rendezvous — data transfer वाला synchronous communication

Rendezvous सिर्फ़ synchronization नहीं है; दोनों दिशाओं में data भी दे सकता है।

task Worker is
   entry Compute (X, Y : Integer; Result : out Integer);
end Worker;

task body Worker is
   A, B   : Integer;
   Output : Integer;
begin
   accept Compute (X, Y : Integer; Result : out Integer) do
      A := X;
      B := Y;
      Output := A * A + B * B;
      Result := Output;
   end Compute;
end Worker;

Caller इसे ऐसे इस्तेमाल करता है (02_rendezvous_intro.ada)।

Worker.Compute (3, 4, Answer);
Put_Line ("Main: result = " & Integer'Image (Answer));

यहाँ design की महत्वपूर्ण बात यह है कि parameter modes साफ़ लिखे गए हैं

  • in mode: caller से task को value देना
  • out mode: task से caller को result लौटाना
  • in out mode: दोनों दिशाएँ

accept body के अंदर का do ... end block critical section है। इस दौरान caller block रहता है, और task कोई और entry accept नहीं करता। काम खत्म होते ही दोनों फिर चलते हैं।

Timeline पर wait कुछ ऐसा दिखता है।

Caller और Worker task के बीच rendezvousCaller Worker.Compute को call करता है और accept तक block रहता है; accept body critical section के रूप में चलती है; end Compute पर दोनों साथ resume होते हैं।Worker taskcallerWorker taskcalleraccept तक पहुँचने तक blockrendezvous established / accept body चलती हैend Compute पर दोनों साथ resume होते हैंWorker.Compute को call करता हैaccept Compute ... do तक पहुँचता हैout parameter Result में result लिखता हैआगे का processingआगे का processing

जो पहले पहुँचे, वही wait करता है। Caller पहले हो तो accept तक रुकेगा; task पहले हो तो कोई call आने तक रुकेगा। दोनों में से कोई भी पहले आए, do ... end का काम हमेशा तब चलता है जब दोनों वहाँ मौजूद हों।

Rendezvous की खूबियाँ संक्षेप में:

Feature Explanation
Synchronization Caller और task दोनों rendezvous point पर पहुँचने तक wait करते हैं
Data transfer in / out / in out parameters से value दोनों दिशाओं में जा सकती है
Mutual exclusion accept body चलते समय उसी task की दूसरी entries block रहती हैं
Structured कौन-सी entry कब accept होगी, यह task body में साफ़ लिखा होता है

Run example

gnatchop ../src/snippets/02_rendezvous_intro.ada
gnatmake rendezvous_demo
./rendezvous_demo
Main: calling Worker.Compute...
Main: result = 25

3 * 3 + 4 * 4 = 25 out parameter से लौट आया है। = के दाएँ एक space इसलिए है कि integer type का 'Image non-negative value के पहले एक blank character रखता है। Chapter 3 से फ़र्क यह है कि यहाँ दो lines का क्रम guarantee है, क्योंकि Worker.Compute की call end Compute; तक लौटती नहीं।

5. Selective accept — कई services का wait

असली server task को कई तरह की requests का wait करना पड़ता है। Ada का select statement यही language level पर देता है।

task Server is
   entry Deposit  (Amount : Integer);
   entry Withdraw (Amount : Integer; Success : out Boolean);
   entry Balance  (Value : out Integer);
end Server;

task body Server is
   Current : Integer := 0;
begin
   loop
      select
         accept Deposit (Amount : Integer) do
            Current := Current + Amount;
         end Deposit;
      or
         accept Withdraw (Amount : Integer; Success : out Boolean) do
            if Current >= Amount then
               Current := Current - Amount;
               Success := True;
            else
               Success := False;
            end if;
         end Withdraw;
      or
         accept Balance (Value : out Integer) do
            Value := Current;
         end Balance;
      or
         terminate;
      end select;
   end loop;
end Server;

इस code (03_selective_accept.ada) के select में कई or branches हैं, और जिन entries पर call पड़ी है उनमें से एक चुनी जाती है (कौन-सी — implementation-defined)। कोई entry call न हुई हो तो किसी एक के आने तक wait रहता है।

or terminate; खास branch है: «main procedure खत्म हो चुकी है, और इस task पर अब कोई entry call आने वाली नहीं» — तब task सुरक्षित तरीके से खत्म हो जाता है। «हमेशा wait करता रहने वाला server task» वाली deadlock समस्या का यह Ada-specific हल है।

Selective accept की ताकत यह भी है कि guard conditions लिख सकते हैं।

नीचे वाला task अंदर ring buffer रखता है। सिर्फ़ select का टुकड़ा काटें तो Count और Head कहाँ से आए, यह साफ़ नहीं रहता, इसलिए declaration से पूरा दिखा रहे हैं। वही idea protected object के रूप में Chapter 8 में आएगी।

task Buffer_Task is
   entry Put_Item (Item : Integer);
   entry Get_Item (Item : out Integer);
end Buffer_Task;

task body Buffer_Task is
   Max   : constant := 8;
   Data  : array (0 .. Max - 1) of Integer;
   Head  : Integer := 0;   -- अगली read की position
   Tail  : Integer := 0;   -- अगली write की position
   Count : Integer := 0;   -- अभी कितने elements हैं
begin
   loop
      select
         when Count > 0 =>
            accept Get_Item (Item : out Integer) do
               Item := Data (Head);
               Head := (Head + 1) mod Max;
               Count := Count - 1;
            end Get_Item;
      or
         when Count < Max =>
            accept Put_Item (Item : Integer) do
               Data (Tail) := Item;
               Tail := (Tail + 1) mod Max;
               Count := Count + 1;
            end Put_Item;
      or
         terminate;
      end select;
   end loop;
end Buffer_Task;

जिस branch की guard false हो, वह उस पल selection से बाहर हो जाती है। इससे «buffer खाली हो तो Get wait करे, भरा हो तो Put wait करे» जैसी control declarative लिखी जाती है। Head और Tail को mod Max से बढ़ाना ring buffer का असल काम है; guard यह भी ensure करती है कि ये indexes valid range में रहें।

6. Producer-consumer — rendezvous से synchronization

Rendezvous का typical pattern देखें: producer-consumer।

task Consumer is
   entry Deliver (Item : Integer);
end Consumer;

task Producer;

task body Consumer is
   Sum : Integer := 0;
begin
   for I in 1 .. 5 loop
      accept Deliver (Item : Integer) do
         Sum := Sum + Item;
      end Deliver;
   end loop;
end Consumer;

task body Producer is
begin
   for I in 1 .. 5 loop
      Consumer.Deliver (I);
   end loop;
end Producer;

इस pattern (04_producer_consumer.ada) में producer हर Deliver पर consumer से synchronize होता है। Producer तेज़ हो तो consumer के accept तक wait करता है; consumer तेज़ हो तो producer की अगली call तक wait करता है। प्राकृतिक backpressure लगती है (receiver संभाल न पाए तो sender अपने आप धीमा पड़ जाता है)। Queue न रखने वाले rendezvous में यह buffer overflow की चिंता के बिना चलता है।

7. Protected object — बिना lock के mutual exclusion

Task «active actor» है; protected object «passive shared data» के लिए है।

protected Counter is
   procedure Increment;
   function Value return Integer;
private
   Count : Integer := 0;
end Counter;

protected body Counter is
   procedure Increment is
   begin
      Count := Count + 1;
   end Increment;

   function Value return Integer is
   begin
      return Count;
   end Value;
end Counter;

Protected object के महत्वपूर्ण नियम ये हैं।

  • function read-only है। कई tasks एक साथ function call कर सकते हैं।
  • procedure read-write है। Procedure चलते समय दूसरी procedures और functions दोनों block रहती हैं।
  • entry के साथ barrier है। Barrier true होने तक caller queue में wait करता है।

इस code (05_protected_counter.ada) में तीन worker tasks में से हर एक Increment 1,000 बार call करता है। Protected object mutual exclusion guarantee करता है, इसलिए अंतिम counter हमेशा 3,000 रहता है। Mutex का lock/unlock हाथ से लिखने की ज़रूरत नहीं।

task type Worker (Id : Integer; Rounds : Integer);

task body Worker is
begin
   for I in 1 .. Rounds loop
      Counter.Increment;  -- protected object mutual exclusion guarantee करता है
   end loop;
end Worker;

W1 : Worker (1, 1_000);
W2 : Worker (2, 1_000);
W3 : Worker (3, 1_000);

Run example

यहाँ एक समस्या है। Main procedure सीधे Counter.Value पढ़े तो workers अभी loop में हो सकते हैं, और बीच का value मिल जाएगा। इसलिए पूरी file (05_protected_counter.ada) में completion गिनने वाली procedure और सभी के खत्म होने का wait करने वाली entry जोड़ी गई है।

protected Counter is
   procedure Increment;
   procedure Mark_Done;
   entry All_Done;
   function Value return Integer;
private
   Count      : Integer := 0;
   Done_Count : Integer := 0;
end Counter;

entry All_Done when Done_Count = Num_Workers जैसा barrier रखें, और हर worker loop से निकलते ही Counter.Mark_Done; call करे। Main procedure Counter.All_Done; पर सभी के complete होने का wait करके फिर value पढ़ती है। Wait के लिए अलग flag variable या sleep की ज़रूरत नहीं।

gnatchop ../src/snippets/05_protected_counter.ada
gnatmake protected_counter_demo
./protected_counter_demo
Final counter value = 3000

जितनी बार चलाएँ, 3000 ही आता है। तीन tasks कुल 3,000 बार Increment call करते हैं, और protected object हर call को exclusive चलाता है।

Protected object न हो तो क्या होता है

Protected object की कीमत समझने के लिए बिना protection वाला खतरनाक code देखें।

-- ⚠ खतरनाक: shared variable को सीधे modify कर रहे हैं
Shared_Counter : Integer := 0;

task body Bad_Worker is
begin
   for I in 1 .. 10_000 loop
      Shared_Counter := Shared_Counter + 1;  -- data race!
   end loop;
end Bad_Worker;

Shared_Counter := Shared_Counter + 1 CPU पर तीन steps है: read → add → write back। कई tasks इसे एक साथ चलाएँ तो एक task के increment का result दूसरे task के read तक पहुँच नहीं पाता, और increment drop हो जाता है। इससे आगे, यह Ada RM 9.10 का erroneous execution है। «Erroneous execution» «value थोड़ा गलत निकला» से कहीं मज़बूत standard शब्द है — उस program के behaviour पर standard कुछ भी guarantee नहीं करता। Unsynchronized shared variable पर एक साथ read/write सिर्फ़ अंतिम count को गलत नहीं बनाता; पूरे program का behaviour arbitrary हो सकता है। दो tasks 10,000-10,000 बार चलाएँ, तब भी अंतिम value 20,000 होने की कोई गारंटी नहीं।

यह «कोई गारंटी नहीं» अपनी आँखों से देखना हो तो ऊपर वाला Bad_Worker version बनाकर बार-बार चलाएँ और हर बार का अंतिम value लिख लें। इस लेख में measured numbers नहीं दिए गए हैं। Data race का नतीजा CPU, optimization options, और runtime timing से बदलता है। किसी एक environment का एक अंक «ऐसा होता है» कहकर दिखाना उलटा यह गलत पैमाना दे सकता है कि «इतना ही बिगड़ता है»। जाँच यह नहीं कि «20,000 से छोटा कोई खास अंक निकला», बल्कि यह कि हर run पर result बदलता है, और एक बार सही value आ जाना बेमानी है

Protected object इस समस्या को «syntax से रोकता» है। Counter.Increment; लिखना काफी है — compiler और runtime mutual exclusion guarantee करते हैं।

8. Protected entry और barrier — bounded buffer

Protected object में entry जोड़ें तो conditional synchronization मिलती है। Classic bounded buffer से देखें।

type Buffer_Array is array (0 .. Buffer_Size - 1) of Integer;

protected Buf is
   entry Put (Item : Integer);
   entry Get (Item : out Integer);
private
   Data    : Buffer_Array;
   Head    : Integer := 0;
   Tail    : Integer := 0;
   Count   : Integer := 0;
end Buf;

protected body Buf is
   entry Put (Item : Integer) when Count < Buffer_Size is
   begin
      Data (Tail) := Item;
      Tail := (Tail + 1) mod Buffer_Size;
      Count := Count + 1;
   end Put;

   entry Get (Item : out Integer) when Count > 0 is
   begin
      Item := Data (Head);
      Head := (Head + 1) mod Buffer_Size;
      Count := Count - 1;
   end Get;
end Buf;

when Count < Buffer_Size barrier है। Barrier हर entry call पर evaluate होता है: true हो तो चलता है, false हो तो caller task queue में wait करता है। Buffer की state बदलते ही (कोई और task Put या Get चलाए) waiting tasks के barrier फिर evaluate होते हैं।

Re-evaluation कब होता है, सिर्फ़ prose से follow करना मुश्किल है। खाली buffer पर Get पहले आए तो timeline कुछ ऐसी है।

Bounded buffer के protected object में barrier का re-evaluationखाली buffer पर Get आता है और entry queue में wait करता है; Put के बाद protected operation के अंत में barrier re-evaluate होता है और Get चलता है।Producer taskprotected object BufConsumer taskProducer taskprotected object BufConsumer taskGet की entry queue में waitprotected operation खत्म होते समय waiting entries के barrier re-evaluateGet को call करता हैbarrier evaluate: Count > 0? → falsePut को call करता हैbarrier evaluate: Count < Buffer_Size? → truePut body चलाता है / Count 1 हो जाता हैGet का barrier true हो गयाGet body चलाकर Consumer को release करता है

मुख्य बात: barrier का re-evaluation protected operation के अंत में एक साथ होता है। Put की body खत्म होने और Buf का lock छूटने के बीच waiting entries के barrier evaluate होते हैं, और जो true हो जाए वही तुरंत चलता है। C की condition variable वाली failure यहाँ नहीं है: «किसी ने signal call करना भूल दिया, तो कोई कभी नहीं जागता»।

यह pattern (06_bounded_buffer.ada) Ada के protected object के सबसे चमकदार moments में से एक है। C में pthread mutex + condition variable से लिखने से तुलना करें।

// C + pthread (Ada से तुलना के लिए)
pthread_mutex_lock(&mutex);
while (count >= BUFFER_SIZE) {           // Ada के when के बराबर
    pthread_cond_wait(&not_full, &mutex); // barrier wait
}
data[tail] = item;
tail = (tail + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(&not_empty);         // waiting task को notify
pthread_mutex_unlock(&mutex);

Ada में यह सब when Count < Buffer_Size की एक line में सिमट जाता है। while loop की condition, signal भेजना, lock release के timing की गलतियाँ — ये सारे bug के मौके गायब हो जाते हैं।

9. Timeout वाली call — forever wait न करें

Real-time system में «forever wait» मना है। Ada select ... or delay से timeout support करती है।

select
   Slow_Worker.Do_Work (Result);
   Put_Line ("Main: work completed");
or
   delay until Ada.Real_Time.Clock + Milliseconds (500);
   Put_Line ("Main: timeout after 500ms!");
end select;

इस code (07_timed_entry.ada) में Slow_Worker delay 2.0 चला रहा है और अभी accept तक नहीं पहुँचा, इसलिए queue में पड़ी entry call 500ms पर timeout हो जाती है। (Timeout rendezvous accept होने से पहले की queue wait पर लगता है; rendezvous body को बीच में interrupt नहीं करता।) delay until absolute time है — cumulative drift रोकने की real-time programming की basic तकनीक।

Ada conditional entry call भी support करती है।

select
   Server.Process (Item);
else
   Put_Line ("Server is busy, will retry later");
end select;

else से: तुरंत rendezvous न हो तो उसी पल वैकल्पिक काम पर चले जाते हैं। Polling हाथ से लिखने की ज़रूरत नहीं।

Timeout के बाद का design न भूलें

Timeout सुविधाजनक है, लेकिन design का असल सवाल यह है: «wait न कर पाने के बाद क्या करें»। Value सच में discard कर दें, retry करें, या error ऊपर भेजें — यह vague रहा तो production में data loss या service बंद होने में बदल जाता है। Timeout लिखते समय, timeout के बाद की ज़िम्मेदारी भी उसी जगह design करें।

Periodic task और delay until

delay until सिर्फ़ timeout के लिए नहीं; periodic execution के लिए भी है। साधारण delay 0.1 period को «processing time + 0.1 सेकंड» बना देता है; delay until अगला wake time absolute time से तय करता है, इसलिए period processing time पर depend नहीं करती।

loop
   Next := Next + Period;
   Do_Work;
   delay until Next;
end loop;

यह pattern sensor monitoring, control loop — जहाँ fixed-period processing चाहिए — हर जगह काम आता है।

10. Task priority और real-time scheduling

Ada की real-time facilities Annex D (Real-Time Systems) में परिभाषित हैं। Implementation Annex D support करे तो task priority और scheduling policy बता सकते हैं।

अपने environment में चलता है या नहीं, जाँचें

Annex D Specialized Needs Annex (खास domain का annex) है; support implementation और runtime environment पर depend करता है। अपने पास उपलब्ध है या नहीं, तीन चरणों में काटें।

1. Priority की range देखें

with Ada.Text_IO; use Ada.Text_IO;
with System;

procedure Check_Priority is
begin
   Put_Line ("Priority range   :"
             & Integer'Image (System.Priority'First)
             & " .."
             & Integer'Image (System.Priority'Last));
   Put_Line ("Default_Priority :"
             & Integer'Image (System.Default_Priority));
end Check_Priority;

System.Priority की range और default implementation-defined हैं, इसलिए यहाँ कोई खास संख्या नहीं लिख रहे। Range काफ़ी चौड़ी दिखे तो pragma Priority (System.Default_Priority + 5) जैसी specification उस environment में मतलब रखती है।

2. Policy compile होती है या नहीं, देखें

pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);

इन्हें लगाकर build पास हो जाए तो कम से कम syntax स्वीकार हो चुकी है।

3. Priority सच में उसी हिसाब से चलती है या नहीं, देखें

यहीं सबसे बड़ा pitfall है। Compile हो जाना और OS scheduler का priority के हिसाब से चलाना दो अलग बातें हैं। Linux या Windows जैसे general-purpose OS पर real-time priority को scheduler पर actually apply करने के लिए OS-side privileges चाहिए हो सकते हैं। इस लेख के sample collection के README में भी नोट है: जहाँ Annex D पूरा support नहीं, वहाँ 08_task_priorities.ada साधारण task की तरह चलता है।

मतलब priority काम न करे, तब भी program चल जाता है। Hard real-time चाहिए तो on-paper priority design काफी नहीं; target environment पर actual order नापकर जाँचना ज़रूरी है।

task High_Task is
   pragma Priority (System.Default_Priority + 5);
end High_Task;

task Low_Task is
   pragma Priority (System.Default_Priority);
end Low_Task;

इससे आगे, scheduling policy और Priority Ceiling Protocol भी बता सकते हैं।

pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
pragma Locking_Policy (Ceiling_Locking);

Priority Ceiling Protocol priority inversion रोकने वाला protocol है। हर protected object पर pragma Priority (या Priority aspect) से ceiling priority साफ़ सेट करें। Caller task की active priority उस ceiling से ऊपर हो तो Program_Error raise होता है। जब object lock में हो, execution ceiling priority पर चलती है, इसलिए medium-priority task उसे preempt नहीं कर पाता।

protected Shared_Data is
   pragma Priority (15);  -- ceiling priority
   procedure Update (Val : Integer);
   function Read return Integer;
private
   Data : Integer := 0;
end Shared_Data;

ये features Rate Monotonic Scheduling (RMS) के theoretical background पर टिके हैं, और aircraft flight control या medical devices जैसे hard real-time systems में इस्तेमाल हो चुके हैं। RMS fixed-priority तरीका है: «छोटी period वाले task को ऊँची priority दो»। Periodic tasks का set deadlines पूरा करेगा या नहीं, यह execute करने से पहले analyse हो सकता है — hard real-time में यही बात मायने रखती है।

11. Practice के लिए design guidelines

अब तक task और protected object की basic syntax देखी। आखिर में, Ada concurrency को production में इस्तेमाल करते समय ध्यान रखने वाली design guidelines।

Protected object के अंदर क्या नहीं करना चाहिए

Protected object के अंदर सिर्फ़ छोटा state update करें; भारी काम बाहर चलाएँ — यही नियम है। Protected operations internally exclusive होती हैं, इसलिए उनके अंदर देर तक block करें तो उसी protected object का इस्तेमाल करने वाले बाकी सभी tasks रुक जाते हैं।

खास तौर पर बचें:

  • delay या समय लेने वाला I/O
  • दूसरे protected object की जटिल calls
  • External library की भारी calls

ध्यान दें: protected operation के अंदर delay या certain I/O सिर्फ़ performance की बात नहीं; Ada standard का bounded error है। Bounded error का मतलब: «संभव नतीजों की range standard तय करता है, लेकिन उस range में कौन-सा नतीजा आएगा, यह तय नहीं»। Erroneous execution जितना खुला नहीं, फिर भी सही चलने की गारंटी नहीं। कुछ implementations पर Program_Error या deadlock हो सकता है, इसलिए «कम करें» नहीं — पूरी तरह हटाएँ।

अच्छा design यह pattern है: «protected object से ज़रूरी values जल्दी निकालें → बाहर भारी computation या I/O करें → सिर्फ़ result को protected object में जल्दी लिख दें»।

Barrier conditions सादी रखें

entry ... when <condition> का barrier ताकतवर है, लेकिन जटिल हो जाए तो पढ़ना मुश्किल हो जाता है, और कोई task क्यों release नहीं हो रहा, यह जाँचना कठिन पड़ जाता है।

when Count < Buffer_Size या when Used > 0 जैसा — state का मतलब एक नज़र में दिखे — आदर्श है। कई conditions सच में चाहिएँ तो enumeration से state लिखें, और barrier को when State = Running जैसा state-name से पढ़ने लायक बनाने पर विचार करें।

Task में exception और shutdown

Task के अंदर exception आए तो नीति पहले से तय रखें। कम से कम task body के सबसे ऊपर exception catch करें, और क्या हुआ वह रिकॉर्ड करें।

उससे ज़्यादा ज़रूरी exception के बाद का design है। वह task रुक जाए तो system चल सकता है? Restart ठीक है? दूसरे tasks को कैसे बताएँ? Shared state को सुरक्षित हालत में कैसे लौटाएँ? Ada के पास exception mechanism language feature के रूप में है, लेकिन exception के बाद की safety application design की ज़िम्मेदारी है।

छोटी design checklist

पहलू क्या जाँचें
Shared state Protected object में बंद है? बाहर से सीधे छू तो नहीं रहे
Protected operations छोटी हैं? अंदर block तो नहीं कर रही
Entries Barrier सादी है? forever wait रहने का खतरा तो नहीं? Timeout की नीति है?
Task lifetime खत्म होने की condition साफ़ है? Exception की नीति है?
Periodic work delay की जगह delay until सोचा?

Concurrency में «शायद ठीक है» सबसे खतरनाक वाक्य है। Shared state, wait condition, termination condition, और exception नीति को code पर साफ़ लिखना — सुरक्षित concurrency का पहला कदम है।

12. Summary — concurrency को «grammar» बनाने वाली language

Ada के concurrency model को दूसरी languages से अलग यह बनाता है कि सुरक्षित concurrency «बाद में जोड़ी गई best practice» नहीं, «grammar» के रूप में बनी हुई है

क्या करना है Ada की syntax
स्वतंत्र execution unit task / task body
Synchronous communication entry / accept
कई requests का wait select / or / else
Mutual exclusion protected / function / procedure
Conditional synchronization entry ... when <barrier>
Timeout or delay until <time>
Priority control pragma Priority

ये constructs compiler verification के दायरे में आते हैं। उदाहरण: protected object की function के अंदर उसी object के private component को लिखने की कोशिश compile error है। Protected operation खत्म होते ही waiting entries के barrier अपने आप re-evaluate होते हैं — हाथ से signal भेजने की ज़रूरत नहीं।

«जैसे type system memory safety guarantee करता है,
  Ada के concurrency constructs synchronization safety guarantee करते हैं»

इस लेख के 8 code examples task, rendezvous, protected object, और real-time facilities का practical परिचय हैं। इन्हें अपने मशीन पर चलाते हुए इन आगे के विषयों पर भी हाथ आजमाएँ।

  • Ravenscar profile: High-integrity real-time systems के लिए tasking restriction profile। सीमित task model से static deadlock analysis संभव होती है।
  • Ada 2022 parallel blocks: parallel ... do से data-parallel processing।
  • SPARK के साथ integration: Concurrent program के behaviour का formal verification (Ravenscar profile के तहत GNATprove support करता है)।

फिर भी «Ada इस्तेमाल किया तो सुरक्षित» नहीं

आखिरी ज़रूरी चेतावनी। Ada के concurrency constructs ताकतवर हैं, लेकिन Ada इस्तेमाल करने से program अपने आप सुरक्षित नहीं हो जाता। Shared data को protected object में डाले बिना सीधे छूना, protected object के अंदर देर तक block करना, कई protected objects को उलझाकर call करना — ये design गलतियाँ Ada में भी हो सकती हैं।

Language features इस हिसाब से बनी हैं कि «खतरनाक लिखने के लिए जान-बूझकर मेहनत करनी पड़े», लेकिन सही design की जगह वे नहीं लेते। Ada की असल कीमत यह है कि safety की चर्चा code के पास आ जाती है — «यह state protected है?», «यह task कब खत्म होता है?», «यह entry किस condition पर wait करता है?» जैसे सवाल syntax के रूप में code पर रह सकते हैं।

Type से design बोलने वाला Ada का विचार concurrency में भी consistent है। सुरक्षित concurrency lock को सावधानी से संभालने से नहीं शुरू होती; खतरनाक shared state को बिना protection के छोड़ने से इनकार करने से शुरू होती है।

«Concurrency मुश्किल है» वाली धारणा के जवाब में Ada कहती है: «सही syntax चुनें, safety compiler guarantee करेगा»। यह design philosophy आज के Rust और Pony से भी मिलती है — फ़र्क यह है कि Ada इसे 40 साल से language specification में रखी हुई है।

निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।

ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।

अक्सर पूछे जाने वाले प्रश्न

इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।

Ada में task क्या है?
task Ada में concurrency की basic unit है। thread जैसा है, लेकिन OS thread से 1-to-1 map होना ज़रूरी नहीं — scheduling Ada runtime संभालता है। Declaration होते ही task अपने आप चलना शुरू करता है, और main procedure खत्म होते समय अभी भी चल रहे tasks के complete होने का implicit wait होता है। बाहर के साथ entry के ज़रिए rendezvous में synchronous communication होती है। task और rendezvous 1983 के Ada 83 से language specification में हैं।
Ada का protected object mutex से कैसे अलग है?
protected object language-managed mutual exclusion है; lock/unlock हाथ से लिखने की ज़रूरत नहीं। function read-only है, कई tasks एक साथ call कर सकते हैं; procedure read-write है, और जब वह चल रही हो तो बाकी calls block हो जाती हैं; entry barrier condition true होने तक caller को queue में wait कराती है। C में pthread mutex और condition variable मिलाकर लिखा जाने वाला bounded buffer का control, Ada में when Count < Buffer_Size जैसी एक barrier line में सिमट जाता है।
Ada में rendezvous कैसे काम करता है?
rendezvous tasks के बीच synchronous communication है: caller की entry call और task की accept statement दोनों rendezvous point पर पहुँचने तक एक-दूसरे का wait करते हैं। in/out/in out parameter modes से data दोनों दिशाओं में जा सकता है। accept body का do...end block critical section है — जब वह चलता है caller block रहता है, और task कोई और entry accept नहीं करता। select के साथ मिलाकर कई entries का wait, timeout, और guard conditions भी declarative लिख सकते हैं।
protected object के अंदर क्या नहीं करना चाहिए?
जो देर तक block करे: delay, समय लेने वाला I/O, या external library की भारी calls। Protected operation के अंदर delay या certain I/O Ada standard में bounded error हैं; implementation के हिसाब से Program_Error या deadlock तक जा सकते हैं, इसलिए इन्हें पूरी तरह हटाना ज़रूरी है। नियम यह है: state update छोटा रखें, भारी computation या I/O protected object के बाहर करें, और सिर्फ़ result को जल्दी वापस लिखें।

लेखक की प्रोफ़ाइल

लेख के लेखक का परिचय पृष्ठ।

Go Komura

KomuraSoft LLC के प्रतिनिधि

Windows सॉफ़्टवेयर विकास, तकनीकी परामर्श और बग जाँच में विशेषज्ञ, विशेष रूप से मौजूदा सिस्टम वाली परियोजनाओं और पुनरुत्पादन में कठिन बग में।

सार्वजनिक लिंक

ब्लॉग पर लौटें