ওহে! আমরা মাল্টিথ্রেডিং নিয়ে আমাদের অধ্যয়ন চালিয়ে যাচ্ছি।
volatile
আজ আমরা কীওয়ার্ড এবং পদ্ধতিটি জানব yield()
। আসুন ডুব দেওয়া যাক :)
উদ্বায়ী কীওয়ার্ড
মাল্টিথ্রেডেড অ্যাপ্লিকেশন তৈরি করার সময়, আমরা দুটি গুরুতর সমস্যায় পড়তে পারি। প্রথমত, যখন একটি মাল্টিথ্রেডেড অ্যাপ্লিকেশন চলছে, তখন বিভিন্ন থ্রেড ভেরিয়েবলের মান ক্যাশে করতে পারে (আমরা ইতিমধ্যেই 'উদ্যোগ ব্যবহার' শিরোনামের পাঠে এটি সম্পর্কে কথা বলেছি )। আপনার এমন পরিস্থিতি হতে পারে যেখানে একটি থ্রেড একটি ভেরিয়েবলের মান পরিবর্তন করে, কিন্তু একটি দ্বিতীয় থ্রেড পরিবর্তনটি দেখতে পায় না, কারণ এটি ভেরিয়েবলের ক্যাশেড কপির সাথে কাজ করছে। স্বাভাবিকভাবেই, এর পরিণতি গুরুতর হতে পারে। ধরুন যে এটি শুধুমাত্র কোন পুরানো পরিবর্তনশীল নয় বরং আপনার ব্যাঙ্ক অ্যাকাউন্টের ব্যালেন্স, যা হঠাৎ করে এলোমেলোভাবে উপরে এবং নিচে লাফানো শুরু করে :) এটি মজার মত শোনাচ্ছে না, তাই না? দ্বিতীয়ত, জাভাতে, সমস্ত আদিম প্রকার পড়তে এবং লিখতে অপারেশন,long
double
, পারমাণবিক। ঠিক আছে, উদাহরণস্বরূপ, আপনি যদি একটি থ্রেডে একটি ভেরিয়েবলের মান পরিবর্তন করেন int
এবং অন্য থ্রেডে আপনি ভেরিয়েবলের মানটি পড়েন, আপনি হয় এর পুরানো মান পাবেন বা নতুনটি পাবেন, অর্থাত্ পরিবর্তনের ফলে যে মানটি এসেছে থ্রেড 1. কোন 'ইন্টারমিডিয়েট মান' নেই। long
যাইহোক, এটি s এবং double
s এর সাথে কাজ করে না । কেন? ক্রস-প্ল্যাটফর্ম সমর্থনের কারণে। শুরুর স্তরে মনে রাখবেন যে আমরা বলেছিলাম যে জাভার গাইডিং নীতি হল 'একবার লিখুন, কোথাও চালান'? এর মানে ক্রস-প্ল্যাটফর্ম সমর্থন। অন্য কথায়, একটি জাভা অ্যাপ্লিকেশন সব ধরণের বিভিন্ন প্ল্যাটফর্মে চলে। উদাহরণস্বরূপ, উইন্ডোজ অপারেটিং সিস্টেমে, Linux বা MacOS এর বিভিন্ন সংস্করণ। এটি তাদের সকলের উপর কোন বাধা ছাড়াই চলবে। একটি 64 বিট ওজন,long
double
জাভাতে 'সবচেয়ে ভারী' আদিম। এবং নির্দিষ্ট 32-বিট প্ল্যাটফর্মগুলি কেবল 64-বিট ভেরিয়েবলের পারমাণবিক পড়া এবং লেখা বাস্তবায়ন করে না। এই ধরনের ভেরিয়েবল দুটি অপারেশনে পড়া এবং লেখা হয়। প্রথমে, প্রথম 32 বিটগুলি ভেরিয়েবলে লেখা হয় এবং তারপরে অন্য 32 বিট লেখা হয়। ফলে সমস্যা দেখা দিতে পারে। একটি থ্রেড একটি ভেরিয়েবলে কিছু 64-বিট মান লিখে X
এবং দুটি অপারেশনে তা করে। একই সময়ে, একটি দ্বিতীয় থ্রেড ভেরিয়েবলের মান পড়ার চেষ্টা করে এবং সেই দুটি ক্রিয়াকলাপের মধ্যে তা করে - যখন প্রথম 32 বিট লেখা হয়েছে, কিন্তু দ্বিতীয় 32 বিট নেই। ফলস্বরূপ, এটি একটি মধ্যবর্তী, ভুল মান পড়ে এবং আমাদের একটি বাগ রয়েছে। উদাহরণস্বরূপ, যদি এমন একটি প্ল্যাটফর্মে আমরা একটি 9223372036854775809 নম্বরে নম্বরটি লেখার চেষ্টা করি একটি ভেরিয়েবলে, এটি 64 বিট দখল করবে। বাইনারি আকারে, এটি এইরকম দেখায়: 100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 প্রথম থ্রেডটি ভেরিয়েবলে সংখ্যা লেখা শুরু করে। প্রথমে, এটি প্রথম 32 বিট (10000000000000000000000000000000000000000) এবং তারপরে দ্বিতীয় 32 বিট (00000000000000000000000000001) লিখে। এবং ভেরিয়েবলের মধ্যবর্তী মান (1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) এই অপারেশনগুলির মধ্যে দ্বিতীয় থ্রেডটি আটকে যেতে পারে যা ইতিমধ্যেই লেখা হয়েছে প্রথম 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
পূর্ণতা পায়। কনসোল আউটপুট এইরকম দেখাবে: থ্রেড-0 অন্যদের কাছে তার জায়গা দেয় থ্রেড-1 অন্যদের কাছে তার জায়গা দেয় থ্রেড-2 অন্যদের কাছে তার জায়গা দেয় থ্রেড-1 কার্যকর করা শেষ হয়েছে। থ্রেড-0 কার্যকর করা শেষ হয়েছে। থ্রেড-2 কার্যকর করা শেষ হয়েছে। অবশ্যই, থ্রেড সময়সূচী একটি ভিন্ন ক্রমে থ্রেডগুলি শুরু করতে পারে (উদাহরণস্বরূপ, 0-1-2 এর পরিবর্তে 2-1-0), কিন্তু নীতিটি একই থাকে।
নিয়মের আগে ঘটে
আমরা আজকে শেষ যে জিনিসটি স্পর্শ করব তা হল ' এর আগে ঘটে ' ধারণাটি । আপনি ইতিমধ্যেই জানেন, জাভাতে থ্রেড শিডিউলার তাদের কাজগুলি সম্পাদন করার জন্য থ্রেডগুলিতে সময় এবং সংস্থান বরাদ্দ করার সাথে জড়িত বেশিরভাগ কাজ সম্পাদন করে। আপনি বারবার দেখেছেন যে কীভাবে থ্রেডগুলি এলোমেলো ক্রমে কার্যকর করা হয় যা সাধারণত অনুমান করা অসম্ভব। এবং সাধারণভাবে, 'অনুক্রমিক' প্রোগ্রামিং এর পরে আমরা আগে করেছি, মাল্টিথ্রেডেড প্রোগ্রামিং কিছু র্যান্ডম মত দেখায়. আপনি ইতিমধ্যেই বিশ্বাস করেছেন যে আপনি একটি মাল্টিথ্রেডেড প্রোগ্রামের প্রবাহ নিয়ন্ত্রণ করতে অনেকগুলি পদ্ধতি ব্যবহার করতে পারেন। কিন্তু জাভাতে মাল্টিথ্রেডিংয়ের আরও একটি স্তম্ভ রয়েছে - 4টি ' হবে-আগে ' নিয়ম। এই নিয়মগুলি বোঝা বেশ সহজ। কল্পনা করুন যে আমাদের দুটি থ্রেড রয়েছে -A
এবংB
. এই থ্রেডগুলির প্রতিটি অপারেশন করতে পারে 1
এবং 2
. প্রতিটি নিয়মে, যখন আমরা বলি ' A ঘটবে-B-এর আগে ', আমরা বলতে চাই যে A
অপারেশনের আগে থ্রেড দ্বারা করা সমস্ত পরিবর্তন এবং এই অপারেশনের ফলে সঞ্চালিত পরিবর্তনগুলি যখন অপারেশন করা হয় এবং তারপরে 1
থ্রেডে দৃশ্যমান হয় । প্রতিটি নিয়ম গ্যারান্টি দেয় যে আপনি যখন একটি মাল্টিথ্রেডেড প্রোগ্রাম লেখেন, তখন নির্দিষ্ট কিছু ঘটনা অন্যদের 100% আগে ঘটবে, এবং অপারেশনের সময় থ্রেড অপারেশন চলাকালীন থ্রেডে করা পরিবর্তনগুলি সম্পর্কে সর্বদা সচেতন থাকবে । আসুন তাদের পর্যালোচনা করি। B
2
2
B
A
1
নিয়ম 1।
একই মনিটর অন্য থ্রেড দ্বারা অর্জিত হওয়ার আগে একটি মিউটেক্স প্রকাশ করা হয়। আমি মনে করি আপনি এখানে সবকিছু বুঝতে পেরেছেন। যদি একটি বস্তু বা শ্রেণীর মিউটেক্স একটি থ্রেড দ্বারা অর্জিত হয়। উদাহরণস্বরূপ, থ্রেড দ্বারাA
, অন্য থ্রেড (থ্রেড B
) একই সময়ে এটি অর্জন করতে পারে না। মিউটেক্স মুক্তি না হওয়া পর্যন্ত এটি অবশ্যই অপেক্ষা করতে হবে।
নিয়ম 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