গেট সেট ডার্ট: আইসোলেট

ডার্টে অ্যাসিনক্রোনাস প্রোগ্রামিঙের জন্য বেশ ভালো ব্যবস্থা আছে। আছে ফিউচারস্ট্রীম। কিন্তু ডার্ট তো সিঙ্গেল-থ্রেডেড। ফলে অ্যাসিনক্রোনাস কোডগুলো প্রায়োরিটিতে পিছনে পড়ে যায়। এখন ভাবুন, আপনি অ্যাসিনক্রোনাসলি গ্রাফিক্স রেন্ডার করছেন বা অন্য কোনোকিছু যেখানে আপনার বেশ মেমরি ও প্রসেসিং পাওয়ার লাগছে। ভালো হত যদি আরেকটি থ্রেডে কাজটি করতে পারতেন, তাই না?

Lo and behold! Isolate!

আইসোলেট (Isolate) হচ্ছে এমন একটি লাইব্রেরি যেটার সাহায্যে আপনি আলাদা একটি থ্রেডে কোনো কাজ করতে পারবেন, এবং প্রয়োজনে দুটি থ্রেডের ভেতর তথ্য আদান-প্রদান করতে পারবেন। তো কীভাবে একটি আইসোলেট তৈরী করা যায়? একদম সোজা। “Hello, World!” প্রোগ্রামের আইসোলেট ভার্সন হবে এরকম:

1
2
3
4
5
6
7
8
import 'dart:isolate';

void main() async {
  var msgIsolate = await Isolate.spawn(print, "Hello, World!");
  msgIsolate.kill(priority: Isolate.immediate);
}

// > "Hello, World!"

আইসোলেট তৈরী করতে আমরা Isolate.spawn ফাংশনটি ব্যবহার করেছি। স্পন ফাংশনটির ডেফিনিশন এইরকম:

1
2
3
4
5
6
7
8
9
10
11
Future<Isolate> spawn <T>(
	void entryPoint(
		T message
	),
	T message, {
	bool paused: false,
	bool errorsAreFatal,
	SendPort onExit,
	SendPort onError,
	String debugName
})

অর্থাৎ, আপনাকে অবশ্যই দিতে হবে একটি ফাংশন যেটি কোনো ভ্যালু রিটার্ন করবে না এবং message প্যারামিটারে একটি মেসেজ নেবে। তাছাড়া, আপনি যে ফাংশনটি দেবেন, হয় (১) সেটি স্ট্যাটিক (static) হবে, নয়ত (২) টপ-লেভেল, অর্থাৎ, কোনো ক্লাসের ভেতরে থাকতে পারবে না। তারপরেই আপনাকে দিতে হবে মেসেজটি, এটি যেকোনো টাইপের হতে পারে। তারপর আছে কিছু কীওয়ার্ড প্যারামিটার যেগুলো নিয়ে আমরা পরে ভাববো।

তো আমরা যখন স্পন ফাংশনটিকে printফাংশন ও “Hello, World!” স্ট্রিং দিয়ে ব্যবহার করেছি তখন আসলে অন্য একটি থ্রেড তৈরী হয়েছে এবং সেই থ্রেডে print("Hello, World!") কোডটি রান করা হয়েছে।

৫ নম্বর লাইনে আমরা আইসোলেটটিকে kill করেছি। ফলে ওটার থ্রেডের রিসোর্স আবার ফ্রী হয়ে যাবে। আইসোলেটের কাজ শেষে সেটাকে কিল করা সবসময়ই একটি ভালো প্রোগ্রামিঙ ডিসিশন।

জীবন তো শুধু হ্যালো ওয়ার্ল্ডে চলে না…

পোস্টের শুরুতে আমি বলেছিলাম আইসোলেট আর মেইন থ্রেড প্রয়োজনে তথ্য আদান-প্রদান করতে পারে। এই আদান-প্রদানের গুরুত্ব অনেক। এরজন্য আছে দুটি ভায়রা-ভাই ক্লাস, RecievePortSendPort। এরা সবজায়গায় গুপি-বাঘার মত একসাথে কাজ করে। রিসিভপোর্ট ইন্সট্যানশিয়েট করলে একটা সেন্ডপোর্টও পাওয়া যায়।

তো, আমরা এবার কালো-জাদুর মন্ত্র বানানোর জন্য প্রোগ্রাম লিখবো। অন্য একটি ম্যাজিক্যাল ডাইমেনশনে (আহেম! আইসোলেটে) আমরা কোনো কথা পাঠালে আমাদের কালো জাদুর মন্ত্র পাঠাবে। এখন, মেসেজ প্যারামিটারের জন্য তো একটাই আর্গুমেন্ট দেওয়া যায়। কিন্তু আমাদের দুটো তথ্য পাঠাতে হবে। একটা হচ্ছে আমাদের মেসেজটি, এবং আরেকটি হচ্ছে সেই পোর্টটি যেটা দিয়ে আমাদের কাছে ডাটা আবার ফেরত পাঠানো হবে। এজন্য আমরা Scroll নামের একটি ক্লাস লিখছি:

1
2
3
4
5
6
7
8
9
class SpellScroll {
  /// A class to hold data for [spellGen].
  /// 'message' is the original message which is to be transformed.
  /// 'portal' is the sendPort to send back the transformed message.
  String message;
  SendPort portal;

  SpellScroll(this.portal, this.message);
}

এই ক্লাসে আমরা একটি SendPort ও একটি message পাঠাবো।

এবার আমরা আমাদের কালোজাদু তৈরীর ফাংশন বানাবো:

1
2
3
4
5
6
7
8
void spellGen(SpellScroll scroll) {
  /// A function to generate black magic spell from scroll. :p
  if (scroll.message != null) {
    print("Recieved: ${scroll.message}");
    var spell = scroll.message.split("").reversed.join("");
    scroll.portal.send(spell);
  }
}

খুবই সহজ ফাংশন। স্ক্রল থেকে মেসেজটা নিয়ে দেখে null কিনা। না হলে রিভার্স করে, তারপর portal এ পাওয়া SendPort অবজেক্ট এর send মেথড দিয়ে ডাটা পাঠিয়ে দেয়। আমরা যদি এই portal এর সহযোগী ReceivePort অবজেক্টে লিসেন্ করি তাহলে আমরা ডাটাটি পেয়ে যাবো। সব মিলিয়ে প্রোগ্রামটা হচ্ছে এরকম:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import 'dart:isolate';

class SpellScroll {
  /// A class to hold data for [spellGen].
  /// 'message' is the original message which is to be transformed.
  /// 'portal' is the sendPort to send back the transformed message.
  String message;
  SendPort portal;

  SpellScroll(this.portal, this.message);
}

void spellGen(SpellScroll scroll) {
  /// A function to generate black magic spell from scroll. :p
  if (scroll.message != null) {
    print("Recieved: ${scroll.message}");
    var spell = scroll.message.split("").reversed.join("");
    scroll.portal.send(spell);
  }
}

void main() async {
  var bridge =
      new ReceivePort(); // Instantiating a port to communicate between isolates.
  var scroll = new SpellScroll(bridge.sendPort, "Motivational speakers suck");
  var magicDimension = await Isolate.spawn(spellGen, scroll);
  bridge.listen((msg) =>
      print(msg)); // Listening to the port for incoming data and printing it.
  magicDimension.kill(priority: Isolate.immediate);
}
// > Recieved: Motivational speakers suck
// > skcus srekaeps lanoitavitoM

মেইন ফাংশনে আমরা প্রথমে একটা ReceivePort ইন্সট্যানশিয়েট করছি bridge নামে। সাথে সাথে আমরা আসলে একটি SendPort অবজেক্টও পাচ্ছি bridge.sendPort প্রোপার্টিতে। তারপর আমরা scroll নামে একটি SpellScroll অবজেক্ট ইন্সট্যানশিয়েট করছি এবং magicDimension আইসোলেট স্পন করার সময় ওই scroll টি মেসেজ হিসেবে দিচ্ছি।

আইসোলেটটি রান করার পর আমরা যে ডাটা পাবো সেটা আমরা bridge.listen মেথড থেকে পেয়ে প্রিন্ট করছি। কোডটি রান করলে 31-32 লাইনের কমেন্টের মত প্রিন্ট করবে।

এভাবেই আমরা পেয়ে যাই দারুণ দারুণ জাদুমন্ত্র!

কখন ব্যবহার করবো আইসোলেট?

শুরুতেই বলেছি, পারফর্মেন্স-হেভি অ্যাসিনক্রোনাস কাজের জন্য আইসোলেট খুবই ভালো সিদ্ধান্ত। একসাথে একাধিক এপিআই থেকে ডাটা টানা, পিরিয়ডিক টাস্ক রান করা ইত্যাদি অনেকক্ষেত্রেই আইসোলেট হতে পারে প্রথম চয়েজ।