গেট সেট ডার্ট: অ্যাসিনক্রোনি

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

আমরা কীভাবে কোড করি?

অধিকাংশ প্রোগ্রামিং ল্যাঙ্গুয়েজ, তা সে ফাংশনাল বা অবজেক্ট ওরিয়েন্টেড, শেষমেশ প্রসিডিউরাল (Procedural)। অর্থাৎ আমরা স্টেটমেন্টের পর স্টেটমেন্ট দিয়ে সাজিয়ে একটা কাজ করি। আমরা এক্সপেক্ট করি যে ক্রমানুসারে আমরা স্টেটমেন্টগুলো দিচ্ছি, সেই ক্রমেই এগুলো কাজ করবে। আমাদের চিন্তার, লজিক তৈরীর সাধারণ প্রক্রিয়াটাও আসলে এরকম। স্বাভাবিকভাবেই আমাদের প্রোগ্রামিং ল্যাঙ্গুয়েজে তার গভীর প্রভাব আছে।

1
2
3
4
5
6
void main() {
  for (int i = 0; i < 5; i++) {
    print('hello ${i + 1}');
  }
  print("bye world.");
}

এই কোডে আমরা এক্সপেক্ট করছি যে প্রথমে ফর-লুপ চালিয়ে hello 1, hello 2… hello 5 প্রিন্ট করবে, তারপর “bye world.” প্রিন্ট করবে। আমরা এটাকে বলতে পারি সিনক্রোনাস প্রোগ্রাম। পারফেক্ট, কোনো ঝামেলা নেই, তাই না?

কিন্তু, দুনিয়া পারফেক্ট না…

“কম্পিউটার, প্রোগ্রামের মত পদার্থবিদ্যার নিয়মকে অবজ্ঞা করতে পারে না। যদি কম্পিউটার খুব দ্রুত কাজ করত চায়, ধরুন, প্রতিটা স্টেট চেঞ্জের জন্য মাত্র কয়েক সেকেন্ড, তাহলে তাদের খুব কম দূরত্বের মধ্যে ইলেক্ট্রনের আদান-প্রদান করত হবে (খুব বেশি হলে ১১/২ ফুট)। অল্প জায়গায় রাখা অনেক যন্ত্রপাতির থেকে উৎপাদিত প্রচুর তাপকে বের করার ব্যবস্থা করতে হবে। ব্যবহারযোগ্যতা এবং যন্ত্রপাতির পরিমাণ ঘনত্বের ভারসাম্য বজায় রাখার এক দুর্দান্ত শিল্প গড়ে উঠেছে এটাকেই কেন্দ্র করে। যে কোনো ক্ষেত্রে আমাদের প্রোগ্রামের চেয়ে হার্ডওয়্যার আরো সেকেলে উপায়ে কাজ করে।” — অ্যালান জে. পারলিস (স্ট্রাকচার অ্যান্ড ইন্টারপ্রিটেশন অব কম্পিউটার প্রোগ্রামস্)1

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

অ্যাসিনক্রোনি আমাদের সাধারণ কার্য-কারণের, প্রসিডিওরের নিয়ম ভেঙে আরো ফ্লেক্সিবল প্রোগ্রাম লিখতে সাহায্য করে। যখন আপনি ওভার-দি-নেটওয়ার্ক ডাটা পাস করবেন, স্প্লিট-সেকেন্ড সিঙ্ক করবেন, বড় আকারের কম্প্লিকেটেড ডাটা প্রসেস করবেন, সম্ভবত, অ্যাসিনক্রোনি একটা ভালো পদ্ধতি।

Future

ডার্টের অ্যাসিনক্রোনি ফিচারের মূলে আছে ফিউচার (Future)। Future অবজেক্ট আমাদের একটা ভ্যালু দিবে কম্পিউট করে। কিন্তু কম্পিউটটা সাথে সাথে নাও হতে পারে। যদি আমরা Future.delayed ব্যবহার করি তাহলে ডিলে ডিউরেশনের পর ফিউচারটি কম্পিউট করবে। যদি সাধারণ ফিউচার তৈরী করি তাহলে অন্যান্য সিনক্রোনাস স্টেটমেন্টের পর ফিউচার কম্পিউট করবে। এটা জানা খুবই জরুরী যে অ্যাসিনক্রোনাস স্টেটমেন্টের প্রায়োরিটি কম।

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "dart:async";

String foo(){
  return "foo foo foo!";
}

void main() {
  var fooFuture = new Future(foo);
  fooFuture.then((val) => print(val));
  print("hello world.");
}

// Output:
// hello world.
// foo foo foo!

যদিও প্রসিডিউর হিসেবে fooFuture আগে আসে, কিন্তু পরের সিনক্রোনাস প্রিন্ট স্টেটমেন্টটি আগে এক্সিকিউট করে তারপর এটি কাজ করেছে। কিন্তু কেন? এর পিছনে একটা যুক্তি হচ্ছে, সাধারণত অ্যাসিক্রোনাস কোড এমনসব ক্ষেত্রে ব্যবহৃত হয় যেখানে সিনক্রোনি আপনি এক্সপেক্টই করছেন না। অ্যাসিনক্রোনাস কোডের ওপর নির্ভরশীল কোড এক্সিকিউটের অন্য মেথড থাকে। এক্ষেত্রে সেটি then

Then

Future এর then মেথডের ডেফিনিশন এরকম:

1
2
3
4
5
6
Future<R> then <R>(
	FutureOr<R> onValue(
		T value
	), {
	Function onError
})

প্রথম প্যারামিটারটি একটি ফাংশন যেটি আসলে একটি ভ্যালু নেবে, দ্বিতীয় কীওয়ার্ড প্যারামিটারটি আরেকটি ফাংশন নেবে যেটি এররের ক্ষেত্রে কাজ করবে।

এক্ষেত্রে যা হয়, তা হলো, ফিউচারটি যখনই আসলে এক্সিকিউট হয় তখন then এর ভেতরের ফাংশনটিকে সে এক্সিকিউশনের পরে পাওয়া ভ্যালুটা দিয়ে দেয় কিছু করার জন্য। আর যদি এরর হয় তাহলে onError ফাংশনটিকে এরর দেয়।

অতএব আমরা আগের প্রোগ্রামে যদি চাই “hello world” “foo foo foo!” এর পরে প্রিন্ট হবে তাহলে প্রিন্ট স্টেটমেন্টটি then এর মেথডে দিতে হবে:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "dart:async";

String foo() {
  return "foo foo foo!";
}

void main() {
  var fooFuture = new Future(foo);
  fooFuture.then((val) {
    print(val);
    print("hello world.");
  }, onError: (e) => print(e));
}

// Output:
// foo foo foo!
// hello world.

Async/Await

Future খুবই ভালো, কিন্তু অনেক এবং নেস্টেড ফিউচার কল খুব দ্রুতই জগাখিচুড়ির মত লাগতে পারে। ডিবাগ করা তখন বেশ কঠিন হয়। অ্যাসিনক্রোনাস কোডকে প্রসিডিউরাল সিনক্রোনাস কোডের মত লেখার জন্য মানিকজোড় সিনট্যাক্টিক সুগার হচ্ছে asyncawait কীওয়ার্ড দুটি। Async/Await ব্যবহার করে আমরা আগের কোডটি এভাবে লিখতে পারি:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "dart:async";

String foo() {
  return "foo foo foo!";
}

void main() async {
  var fooFuture = new Future(foo);
  var data = await fooFuture;
  print(data);
  print("hello world.");
}
// Output:
// foo foo foo!
// hello world.

await কীওয়ার্ডটি দিয়ে আমরা একটি Future এর এক্সিকিউশনের পর ভ্যারিয়েবল পাওয়া পর্যন্ত অপেক্ষা করতে পারি। then কীওয়ার্ডে যে ভ্যালু ফিউচার দেয়, await স্টেটমেন্টের ভ্যালু হিসেবেও সেটি পাওয়া যায়, এবং এটার প্রায়োরিটি অন্যান্য স্টেটমেন্টের মতই থাকে। ফলে, প্রসিডিউরাল কোডের মত এটি ব্যবহার করা যায়।

কিন্তু, যে ফাংশনের মধ্যে **await** আছে সেই ফাংশনের ডেফিনেশনে অবশ্যই আপনাকে **async** ব্যবহার করতে হবে। অর্থাৎ, ওই ফাংশনের মধ্যে await দিয়ে প্রসিডিউরাল করলেও ওই ফাংশনটি অ্যাসিনক্রোনাস হয়ে যাচ্ছে।

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "dart:async";

String foo() {
  return "foo foo foo!";
}

Future<String> printFoo() async{
  var fooVal = await foo();
  print(fooVal);
}

void main() {
  printFoo();
  print("hello world.");
}
// Output:
// hello world.
// foo foo foo!

আমরা ‍foo ফাংশন থেকে পাওয়া ভ্যালু প্রিন্ট করার ফাংশনালিটিকে আরেকটি অ্যাসিনক্রোনাস ফাংশন printFoo-তে সরিয়ে নিলাম। একটা জিনিস জানা দরকার, async ফাংশন Future অবজেক্ট রিটার্ন করে। এই অবজেক্টের ভেতরে যেকোনো টাইপের ভ্যালু থাকতে পারে। এই টাইপটাই ‘আসল‍’ রিটার্ন ভ্যালু।

আমাদের main ফাংশনে যখন আমরা printFoo কল করছি তখন এটি আসলে ফিউচার রিটার্ন করছে, এবং ফিউচারের প্রায়োরিটি কম বলে শেষে এক্সিকিউট হচ্ছে। আমরা যদি আগে প্রিন্ট করতে চাই তাহলে main-কেও async করে তার ভেতর await ব্যবহার করতে হবে।

কখন কোনটা ব্যবহার করবো?

Futureasync/await একটা আরেকটার পরিবর্তে ব্যবহারের জন্য না, বরং পরিপূরক। খুব বেশি নেস্টেড কল না হলে আমি সাধারণত async/await এড়িয়ে চলি। অ্যাসিনক্রোনাস প্রসেস হ্যান্ডেল করতে প্যারেন্ট ফাংশনকে অ্যাসিনক্রোনাস হিসেবে ডিফাইন করা আমার ব্যক্তিগত অপছন্দ।

পরের পোস্টে কথা হবে স্ট্রীম (Stream) নিয়ে।

  1. সংক্ষেপে SICP প্রোগ্রামিঙের বাইবেল বলে খ্যাত। উদ্ধৃত অংশটুকু এই বইটির মুখবন্ধের তৃতীয় অনুচ্ছেদের কিছুটা অংশের ভাবানুবাদ। 

  2. “প্রয়োজনীয় মাত্রায়” কেননা সম্পূর্ণ নিঁখুত প্রায় অসম্ভব একটা বিষয়। কেন? কার্লো রোভেলির ‘দি অর্ডার অব টাইম’ বইটা পড়ে দেখতে পারেন।