ہائے! ہم ملٹی تھریڈنگ کا اپنا مطالعہ جاری رکھتے ہیں۔
جب ہم
volatile
آج ہم کلیدی لفظ اور طریقہ جانیں گے yield()
۔ آئیے اس میں غوطہ لگائیں :)
غیر مستحکم مطلوبہ لفظ
ملٹی تھریڈ ایپلی کیشنز بناتے وقت، ہم دو سنگین مسائل کا شکار ہو سکتے ہیں۔ سب سے پہلے، جب ایک ملٹی تھریڈ ایپلیکیشن چل رہی ہوتی ہے، تو مختلف تھریڈز متغیرات کی قدروں کو کیش کر سکتے ہیں (ہم نے پہلے ہی اس کے بارے میں 'استعمال کرنا' کے عنوان سے سبق میں بات کی ہے )۔ آپ کو ایسی صورت حال ہو سکتی ہے جہاں ایک تھریڈ متغیر کی قدر کو تبدیل کرتا ہے، لیکن دوسرا تھریڈ تبدیلی نہیں دیکھتا، کیونکہ یہ متغیر کی اپنی کیش شدہ کاپی کے ساتھ کام کر رہا ہے۔ قدرتی طور پر، نتائج سنگین ہو سکتے ہیں. فرض کریں کہ یہ صرف کوئی پرانا متغیر نہیں ہے بلکہ آپ کے بینک اکاؤنٹ کا بیلنس ہے، جو اچانک تصادفی طور پر اوپر نیچے ہونا شروع ہو جاتا ہے :) یہ مزے کی طرح نہیں لگتا، ٹھیک ہے؟ دوسرا، جاوا میں، تمام قدیم اقسام کو پڑھنے اور لکھنے کے آپریشنز، سوائےlong
اور double
، جوہری ہیں۔ ٹھیک ہے، مثال کے طور پر، اگر آپ ایک تھریڈ پر کسی متغیر کی قدر کو تبدیل کرتے ہیں int
، اور دوسرے تھریڈ پر آپ متغیر کی قدر پڑھتے ہیں، تو آپ کو یا تو اس کی پرانی ویلیو ملے گی یا نئی، یعنی وہ قدر جو تبدیلی کے نتیجے میں ہوئی ہے۔ تھریڈ 1 میں۔ کوئی 'انٹرمیڈیٹ ویلیوز' نہیں ہیں۔ long
تاہم، یہ s اور double
s کے ساتھ کام نہیں کرتا ہے ۔ کیوں؟ کراس پلیٹ فارم سپورٹ کی وجہ سے۔ ابتدائی سطحوں پر یاد رکھیں کہ ہم نے کہا تھا کہ جاوا کا رہنما اصول ہے 'ایک بار لکھیں، کہیں بھی چلائیں'؟ اس کا مطلب ہے کراس پلیٹ فارم سپورٹ۔ دوسرے الفاظ میں، جاوا ایپلیکیشن ہر طرح کے مختلف پلیٹ فارمز پر چلتی ہے۔ مثال کے طور پر، ونڈوز آپریٹنگ سسٹم پر، لینکس یا میک او ایس کے مختلف ورژن۔ یہ ان سب پر بغیر کسی رکاوٹ کے چلے گا۔ 64 بٹس میں وزن، long
اور double
جاوا میں 'سب سے بھاری' قدیم ہیں۔ اور کچھ 32 بٹ پلیٹ فارم صرف 64 بٹ متغیرات کی ایٹم ریڈنگ اور رائٹنگ کو لاگو نہیں کرتے ہیں۔ اس طرح کے متغیرات کو دو آپریشنز میں پڑھا اور لکھا جاتا ہے۔ پہلے، پہلے 32 بٹس کو متغیر پر لکھا جاتا ہے، اور پھر مزید 32 بٹس لکھے جاتے ہیں۔ نتیجے کے طور پر، ایک مسئلہ پیدا ہوسکتا ہے. ایک تھریڈ متغیر پر کچھ 64 بٹ ویلیو لکھتا ہے X
اور ایسا دو آپریشنز میں کرتا ہے۔ ایک ہی وقت میں، دوسرا تھریڈ متغیر کی قدر کو پڑھنے کی کوشش کرتا ہے اور ان دو آپریشنز کے درمیان ایسا کرتا ہے - جب پہلے 32 بٹس لکھے گئے ہوں، لیکن دوسرے 32 بٹس میں نہیں ہے۔ نتیجے کے طور پر، یہ ایک انٹرمیڈیٹ، غلط قدر پڑھتا ہے، اور ہمارے پاس ایک بگ ہے۔ مثال کے طور پر، اگر اس طرح کے پلیٹ فارم پر ہم نمبر کو 9223372036854775809 پر متغیر پر لکھنے کی کوشش کرتے ہیں، تو یہ 64 بٹس پر قبضہ کرے گا۔ بائنری شکل میں، یہ اس طرح نظر آتا ہے: 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 پہلا تھریڈ متغیر پر نمبر لکھنا شروع کرتا ہے۔ سب سے پہلے، یہ پہلے 32 بٹس (1000000000000000000000000000000000000000000) اور پھر دوسرے 32 بٹس (000000000000000000000000000001) لکھتا ہے۔ اور دوسرا دھاگہ ان آپریشنز کے درمیان ویجڈ ہو سکتا ہے، متغیر کی انٹرمیڈیٹ ویلیو (10000000000000000000000000000000000000000000000000000000000000000000000000000) جو کہ پہلے 32 بٹس ہیں جو پہلے ہی لکھے جا چکے ہیں۔ اعشاریہ نظام میں یہ تعداد 2,147,483,648 ہے۔ دوسرے لفظوں میں، ہم صرف ایک متغیر پر نمبر 9223372036854775809 لکھنا چاہتے تھے، لیکن اس حقیقت کی وجہ سے کہ یہ آپریشن کچھ پلیٹ فارمز پر ایٹم نہیں ہے، ہمارے پاس برائی نمبر 2,147,483,648 ہے، جو کہیں سے نہیں نکلا اور اس کا نامعلوم اثر پڑے گا۔ پروگرام دوسرا تھریڈ لکھے جانے سے پہلے صرف متغیر کی قدر کو پڑھتا ہے، یعنی تھریڈ نے پہلے 32 بٹس دیکھے، لیکن دوسرے 32 بٹس کو نہیں۔ یقیناً یہ مسائل کل پیدا نہیں ہوئے۔ جاوا انہیں ایک کلیدی لفظ کے ساتھ حل کرتا ہے: volatile
. اگر ہم volatile
اپنے پروگرام میں کچھ متغیر کا اعلان کرتے وقت کلیدی لفظ استعمال کرتے ہیں…
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…اس کا مطلب ہے کہ:
- یہ ہمیشہ ایٹمی طور پر پڑھا اور لکھا جائے گا۔ یہاں تک کہ اگر یہ 64 بٹ
double
یاlong
. - جاوا مشین اسے کیش نہیں کرے گی۔ لہذا آپ کو ایسی صورتحال نہیں ہوگی جہاں 10 تھریڈز اپنی مقامی کاپیوں کے ساتھ کام کر رہے ہوں۔
پیداوار () طریقہ
ہم پہلے ہیThread
کلاس کے بہت سے طریقوں کا جائزہ لے چکے ہیں، لیکن ایک اہم طریقہ ہے جو آپ کے لیے نیا ہوگا۔ یہ yield()
طریقہ ہے ۔ اور یہ بالکل وہی کرتا ہے جو اس کے نام کا مطلب ہے! 
yield
ایک تھریڈ پر طریقہ کہتے ہیں، تو یہ دراصل دوسرے تھریڈز سے بات کرتا ہے: 'ارے، لوگو۔ مجھے کہیں بھی جانے کی کوئی خاص جلدی نہیں ہے، اس لیے اگر آپ میں سے کسی کے لیے پروسیسر کا وقت حاصل کرنا ضروری ہے، تو اسے لے لیں — میں انتظار کر سکتا ہوں''۔ یہ کیسے کام کرتا ہے اس کی ایک سادہ سی مثال یہ ہے:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
ہم ترتیب وار تین تھریڈز بناتے اور شروع کرتے ہیں: Thread-0
, Thread-1
اور Thread-2
. Thread-0
سب سے پہلے شروع ہوتا ہے اور فوری طور پر دوسروں کو حاصل ہوتا ہے۔ پھر Thread-1
شروع ہوتا ہے اور پیداوار بھی۔ پھر Thread-2
شروع کیا جاتا ہے، جس کا نتیجہ بھی نکلتا ہے۔ ہمارے پاس مزید کوئی دھاگہ نہیں ہے، اور Thread-2
اس کی آخری جگہ حاصل کرنے کے بعد، دھاگے کا شیڈیولر کہتا ہے، 'ہمم، اب مزید نئے تھریڈز نہیں ہیں۔ ہم قطار میں کون ہیں؟ اس سے پہلے اپنی جگہ کس نے دی Thread-2
؟ ایسا لگتا ہے کہ یہ تھا Thread-1
۔ ٹھیک ہے، اس کا مطلب ہے کہ ہم اسے چلنے دیں گے۔ Thread-1
اپنا کام مکمل کرتا ہے اور پھر تھریڈ شیڈولر اپنا ہم آہنگی جاری رکھتا ہے: 'ٹھیک ہے، Thread-1
ختم۔ کیا ہمارے پاس قطار میں کوئی اور ہے؟' تھریڈ-0 قطار میں ہے: اس نے اپنی جگہ ٹھیک سے پہلے حاصل کی Thread-1
۔ اب اس کی باری آتی ہے اور تکمیل کی طرف بھاگتا ہے۔ پھر شیڈیولر تھریڈز کو مربوط کرنا ختم کرتا ہے: 'ٹھیک ہے، Thread-2
آپ نے دوسرے دھاگوں کی طرف رجوع کیا، اور اب وہ سب ہو چکے ہیں۔ آپ سب سے آخر میں تھے، اس لیے اب آپ کی باری ہے۔ پھر Thread-2
تکمیل کی طرف دوڑتا ہے۔ کنسول آؤٹ پٹ اس طرح نظر آئے گا: Thread-0 اپنی جگہ دوسروں کو دیتا ہے Thread-1 اپنی جگہ دوسروں کو دیتا ہے Thread-2 اپنی جگہ دوسروں کو دیتا ہے Thread-1 نے عملدرآمد مکمل کر لیا ہے۔ Thread-0 پر عملدرآمد ختم ہو گیا ہے۔ Thread-2 پر عملدرآمد ختم ہو گیا ہے۔ بلاشبہ، تھریڈ شیڈیولر تھریڈز کو مختلف ترتیب سے شروع کر سکتا ہے (مثال کے طور پر، 0-1-2 کی بجائے 2-1-0)، لیکن اصول وہی رہتا ہے۔
ہوتا ہے - قواعد سے پہلے
آخری چیز جس پر ہم آج چھوئیں گے وہ ہے ' پہلے ہوتا ہے ' کا تصور۔ جیسا کہ آپ پہلے ہی جانتے ہیں، جاوا میں تھریڈ شیڈیولر اپنے کاموں کو انجام دینے کے لیے تھریڈز کو وقت اور وسائل مختص کرنے میں شامل زیادہ تر کام انجام دیتا ہے۔ آپ نے بارہا یہ بھی دیکھا ہے کہ تھریڈز کو بے ترتیب ترتیب میں کیسے عمل میں لایا جاتا ہے جس کا اندازہ لگانا عموماً ناممکن ہوتا ہے۔ اور عام طور پر، 'سیکینشل' پروگرامنگ کے بعد جو ہم نے پہلے کیا تھا، ملٹی تھریڈ پروگرامنگ کچھ بے ترتیب لگتی ہے۔ آپ کو پہلے ہی یقین ہو گیا ہے کہ آپ ملٹی تھریڈ پروگرام کے بہاؤ کو کنٹرول کرنے کے لیے بہت سے طریقے استعمال کر سکتے ہیں۔ لیکن جاوا میں ملٹی تھریڈنگ کا ایک اور ستون ہے - 4 ' ہونے سے پہلے ' کے اصول۔ ان اصولوں کو سمجھنا بہت آسان ہے۔ تصور کریں کہ ہمارے پاس دو دھاگے ہیں -A
اور B
. ان تھریڈز میں سے ہر ایک آپریشن کر سکتا ہے 1
اور 2
. ہر اصول میں، جب ہم کہتے ہیں کہ ' A ہوتا ہے-B سے پہلے '، ہمارا مطلب ہے کہ A
آپریشن سے پہلے دھاگے کے ذریعے کی گئی تمام تبدیلیاں 1
اور اس آپریشن کے نتیجے میں ہونے والی تبدیلیاں B
جب آپریشن 2
کیا جاتا ہے اور اس کے بعد دھاگے میں نظر آتا ہے۔ ہر قاعدہ اس بات کی ضمانت دیتا ہے کہ جب آپ ملٹی تھریڈڈ پروگرام لکھتے ہیں، تو کچھ واقعات دوسروں کے سامنے 100% وقت پر پیش آئیں گے، اور یہ کہ آپریشن کے وقت 2
تھریڈ کے دوران ہونے B
والی تبدیلیوں سے ہمیشہ آگاہ رہیں گے ۔ آئیے ان کا جائزہ لیتے ہیں۔ A
1
اصول 1۔
ایک mutex کو جاری کرنا اس سے پہلے ہوتا ہے جب ایک ہی مانیٹر دوسرے دھاگے کے ذریعہ حاصل کیا جائے۔ مجھے لگتا ہے کہ آپ یہاں سب کچھ سمجھتے ہیں۔ اگر کسی شے یا کلاس کا mutex ایک دھاگے کے ذریعے حاصل کیا جاتا ہے۔، مثال کے طور پر، thread کے ذریعے،A
دوسرا دھاگہ (thread B
) اسے ایک ہی وقت میں حاصل نہیں کر سکتا۔ اسے mutex کے جاری ہونے تک انتظار کرنا ہوگا۔
اصول 2۔
طریقہ پہلے ہوتا ہےThread.start()
۔ ایک بار پھر، یہاں کچھ بھی مشکل نہیں ہے. آپ پہلے ہی جانتے ہیں کہ کوڈ کو طریقہ کے اندر چلانا شروع کرنے کے لیے ، آپ کو تھریڈ پر موجود طریقہ کو کال کرنا ہوگا ۔ خاص طور پر، آغاز کا طریقہ، خود طریقہ نہیں! یہ قاعدہ اس بات کو یقینی بناتا ہے کہ تمام متغیرات کی قدریں جو پہلے کال کی گئی ہیں ، شروع ہونے کے بعد طریقہ کے اندر نظر آئیں گی ۔ Thread.run()
run()
start()
run()
Thread.start()
run()
قاعدہ 3۔
run()
طریقہ کار کا اختتام طریقہ سے واپسی سے پہلے ہوتا ہےjoin()
۔ آئیے اپنے دو تھریڈز پر واپس آتے ہیں: A
اور B
. ہم اس join()
طریقہ کو کہتے ہیں تاکہ تھریڈ اپنا کام کرنے سے پہلے B
دھاگے کے مکمل ہونے کا انتظار کرے ۔ A
اس کا مطلب یہ ہے کہ A آبجیکٹ کے run()
طریقہ کار کے اختتام تک چلنے کی ضمانت ہے۔ اور ڈیٹا میں ہونے والی تمام تبدیلیاں جو run()
تھریڈ کے طریقہ کار میں ہوتی A
ہیں سو فیصد اس بات کی گارنٹی ہیں کہ دھاگے میں نظر آنے B
کے بعد دھاگے A
کے کام ختم ہونے کا انتظار کیا جائے تاکہ وہ اپنا کام خود شروع کر سکے۔
قاعدہ 4۔
volatile
متغیر پر لکھنا اسی متغیر سے پڑھنے سے پہلے ہوتا ہے ۔ جب ہم volatile
کلیدی لفظ استعمال کرتے ہیں، تو ہمیں درحقیقت ہمیشہ موجودہ قدر ملتی ہے۔ یہاں تک کہ ایک long
یا کے ساتھ double
(ہم نے پہلے ان مسائل کے بارے میں بات کی تھی جو یہاں ہوسکتے ہیں)۔ جیسا کہ آپ پہلے ہی سمجھ چکے ہیں، کچھ تھریڈز پر کی گئی تبدیلیاں ہمیشہ دوسرے تھریڈز پر نظر نہیں آتیں۔ لیکن، بلاشبہ، ایسے حالات اکثر ہوتے ہیں جہاں اس طرح کا رویہ ہمارے لیے مناسب نہیں ہوتا۔ فرض کریں کہ ہم تھریڈ پر متغیر کو ایک قدر تفویض کرتے ہیں A
:
int z;
….
z = 555;
اگر ہمارے تھریڈ کو کنسول پر متغیر B
کی قدر ظاہر کرنی چاہیے ، تو یہ آسانی سے 0 ظاہر کر سکتا ہے، کیونکہ یہ تفویض کردہ قدر کے بارے میں نہیں جانتا ہے۔ z
لیکن رول 4 اس بات کی ضمانت دیتا ہے کہ اگر ہم z
متغیر کو بطور ڈیکلیئر کرتے ہیں volatile
، تو ایک تھریڈ پر اس کی قدر میں تبدیلیاں ہمیشہ دوسرے تھریڈ پر نظر آئیں گی۔ اگر ہم لفظ کو volatile
پچھلے کوڈ میں شامل کرتے ہیں...
volatile int z;
….
z = 555;
...پھر ہم اس صورتحال کو روکتے ہیں جہاں تھریڈ B
0 ظاہر کر سکتا ہے۔ volatile
متغیرات کو لکھنا ان سے پڑھنے سے پہلے ہوتا ہے۔
GO TO FULL VERSION