কনকারেন্সি এবং প্যারাাললিজম আজ Ruby ডেভেলপারদের জন্য চাইতে চাইতে বেশি গুরুত্বপূর্ণ। এগুলো আমাদের অ্যাপ্লিকেশনকে দ্রুত করতে পারে, সেগুলোকে চালানো হার্ডওয়্যারের পূর্ণ পোটেনশিয়াল ব্যবহার করে। এই আর্টিকেলে, আমরা প্রতিটি Rubyist-কে উপলব্ধ বর্তমান টুলগুলো এবং Ruby ভবিষ্যতে এই দিক থেকে কী দিতে обещает করে সেটি এক্সপ্লোর করব।
সবাই কনকারেন্সি ডিরেক্টলি ব্যবহার করে না, কিন্তু আমরা সবাই পরোক্ষভাবে সাইডকিউ (Sidekiq) এর মতো টুলস দিয়ে ব্যবহার করি। Ruby কনকারেন্সি বুঝলে আপনি শুধু নিজের সমাধান বানাতে পারবেন না, বরং বিদ্যমান সমাধানগুলো বুঝতে এবং ট্রাবলশুট করতে পারবেন।
তবে প্রথমে একটু পিছিয়ে যাই এবং বড় চিত্রটি দেখি।
কনকারেন্সি বনাম প্যারাাললিজম
এই টার্মগুলো লুজলি ব্যবহৃত হয়, কিন্তু তাদের স্বতন্ত্র মানে আছে।
- কনকারেন্সি (Concurrency): অনেক টাস্ক একটা একটা করে করার কল্পনা। দ্রুত স্যুইচ করলে ব্যবহারকারীর কাছে সেগুলো সিমালটেনিয়াসলে হচ্ছে বলে মনে হয়।
- প্যারাাললিজম (Parallelism): অনেক টাস্ক শाब্দিক অর্থে একই সময়ে করা। সিমালটেনিয়াসলে মনে হওয়ার বদলে, সেগুলো আসলে সিমালটেনিয়াস।
কনকারেন্সি সবচেয়ে বেশি IO-হেভি অ্যাপ্লিকেশনে ব্যবহার হয়। উদাহরণস্বরূপ, একটি ওয়েব অ্যাপ নিয়মিত ডাটাবেসের সাথে ইন্টারঅ্যাক্ট করতে পারে বা অনেক নেটওয়ার্ক রিকোয়েস্ট করতে পারে। কনকারেন্সি ব্যবহার করে, ডাটাবেস আমাদের কোয়েরিতে রেসপন্স দিচ্ছে এমন সময়েও আমরা আমাদের অ্যাপ্লিকেশনকে রেসপনসিভ রাখতে পারি।
এটি সম্ভব কারণ Ruby VM অন্য থ্রেডগুলো রান করতে দেয় যখন একটি IO-র জন্য অপেক্ষা করছে। এমনকি যদি একটি প্রোগ্রামে দर्जনো রিকোয়েস্ট করতে হয়, কনকারেন্সি ব্যবহার করলে রিকোয়েস্টগুলো ভার্চুয়ালি একই সময়ে হয়।
দूसरी দিকে, প্যারাাললিজম বর্তমানে Ruby-এ সাপোর্টেড নয়।
Ruby-এ প্যারাাললিজম কেন নেই?
আজ, ডিফল্ট Ruby ইমপ্লিমেন্টেশন (সাধারণত MRI বা CRuby বলা হয়) ব্যবহার করে একটি সিঙ্গল Ruby প্রসেসের ভিতরে প্যারাাললিজম অর্জন করার কোনো উপায় নেই। Ruby VM একটি লক এনফোর্স করে (GVM, বা Global VM Lock) যা একাধিক থ্রেড একই সময়ে Ruby কোড রান করতে বাধা দেয়। এই লক VM-এর ইন্টারন্যাল স্টেট প্রোটেক্ট করতে এবং VM ক্র্যাশ হতে পারে এমন সিনেরিও রোকতে থাকে। এটি কোনো ভালো পজিশন নয়, কিন্তু সব আশা হারানো নয়: Ruby 3 শীঘ্রই আসছে এবং এটি Guild নামে একটি কনসেপ্ট আনার মাধ্যমে এই 핸ডিক্যাপ দূর করবে বলে обещает করে (লাস্ট সেকশনে ব্যাখ্যা করা হয়েছে)।
থ্রেড (Threads)
থ্রেড হচ্ছে Ruby-এর কনকারেন্সির ওয়ার্কহর্স। এগুলো কীভাবে ব্যবহার করতে হয় এবং কী পিটফলগুলো থেকে সতর্ক থাকতে হয় সেটি ভালোভাবে বুঝতে আমরা একটি উদাহরণ দেব। আমরা একটি ছোট প্রোগ্রাম বানাব যা একটি API কনসিউম করে এবং এর ফলাফল একটি ডেটাস্টোরে কনকারেন্টলি স্টোর করে।
API ক্লায়েন্ট বানানোর আগে আমাদের একটি API দরকার। নিচে একটি টাইনি API-র ইমপ্লিমেন্টেশন দেওয়া হলো যা একটি নম্বর এক্সেপ্ট করে এবং প্লেইন টেক্সটে রেসপন্স দেয় নম্বরটি ইভেন নাকি অড্ড। সিনট্যাক্স অদ্ভুত লাগলে চিন্তা করবেন না। এর কনকারেন্সি সাথে কোনো সম্পর্ক নেই। এটি শুধু আমাদের ব্যবহার করার একটি টুল।
app = Proc.new do |env|
sleep 0.05
qs = env['QUERY_STRING']
number = Integer(qs.match(/number=(\d+)/)[1])
[
'200',
{ 'Content-Type' => 'text/plain' },
[number.even? ? 'even' : 'odd']
]
end
run app
এই ওয়েব অ্যাপ রান করতে rack gem ইনস্টলড থাকতে হবে, তারপর rackup config.ru এক্সিকিউট করুন।
আমাদের একটি মক ডেটাস্টোরও দরকার। এখানে একটি ক্লাস যা কি-ভ্যালু ডাটাবেস সিমুলেট করে:
class Datastore
# ... এক্সেসর এবং ইনিশিয়ালাইজেশন опуচ্ছন্ন ...
def read(key)
data[key]
end
def write(key, value)
data[key] = value
end
end
এবার, আমাদের কনকারেন্ট সলিউশনের ইমপ্লিমেন্টেশন দিয়ে যাই। আমাদের একটি মেথড আছে, run, যা কনকারেন্টলি ১,০০০ রিকর্ড ফেচ করে এবং আমাদের ডেটাস্টোরে সেগুলো স্টোর করে।
class ThreadPoweredIntegration
# ... এক্সেসর এবং ইনিশিয়ালাইজেশন ...
def run
threads = []
(1..1000).each_slice(250) do |subset|
threads << Thread.new do
subset.each do |number|
uri = 'https://localhost:9292/' \
"even_or_odd?number=#{number}"
status, body = AdHocHTTP.new(uri).blocking_get
handle_response(status, body)
rescue Errno::ETIMEDOUT
retry # সার্ভার টাইমআউট দিলে আবার চেষ্টা করুন।
end
end
end
threads.each(&:join)
end
# ...
end
আমরা চারটি থ্রেড তৈরি করি, প্রতিটি ২৫০ রিকর্ড প্রসেস করে। আমরা এই কৌশলটি ব্যবহার করি থার্ড-পার্টি API বা আমাদের নিজস্ব সিস্টেমকে অভিভূত না করার জন্য।
মাল্টিপল থ্রেড ব্যবহার করে রিকোয়েস্টগুলো কনকারেন্টলি করা হলে, টোটাল এক্সিকিউশন সিকুয়েনশিয়াল ইমপ্লিমেন্টেশনের অপেক্ষা অনেক কম সময় নেয়। যেহেতু প্রতিটি থ্রেড HTTP রিকোয়েস্ট স্থাপন এবং কমিউনিকেশনের সমস্ত স্টেপের সময় একটিভ থাকে না, Ruby VM অন্য একটি থ্রেডকে রান করতে দেয়। এই কারণেই এই ইমপ্লিমেন্টেশন সিকুয়েনশিয়াল থেকে অনেক দ্রুত।
AdHocHTTPক্লাস হলো একটি স্ট্রেইটফরওয়ার্ড HTTP ক্লায়েন্ট যা এই আর্টিকেলের জন্য স্পেশালি ইমপ্লিমেন্ট করা হয়েছে যাতে আমরা শুধু থ্রেড এবং ফাইবার-চালিত কোডের পার্থক্যে ফোকাস করতে পারি। এর ইমপ্লিমেন্টেশন এই আর্টিকেলের স্কোপের বাইরে, কিন্তু আপনি যদি কৌতূহলী হন তবে এখানে চেক আউট করুন।
শেষে, আমরা সার্ভারের রেসপন্স হ্যান্ডেল করি ইনার লুপের শেষে। handle_response মেথডটি কীভাবে দেখાય:
# ... ThreadPoweredIntegration ক্লাসের ভিতরে ...
attr_reader :ds
def initialize
@ds = Datastore.new(even: 0, odd: 0)
end
# ...
def handle_response(status, body)
return if status != '200'
key = body.to_sym
curr_count = ds.read(key)
ds.write(key, curr_count + 1)
end
এই মেথডটি ঠিকই দেখাচ্ছে, ঠিক না? চলে দেখি ডেটাস্টোরে কী থাকে:
{ even: 497, odd: 489 }
এটা খুব স্ট্রেঞ্জ, কারণ ১ থেকে ১০০০ এর মধ্যে ৫০০টি ইভেন এবং ৫০০টি অড্ড নম্বর আছে বলে আমি নিশ্চিত। পরবর্তী সেকশনে বুঝি কী হচ্ছে এবং একটু এক্সপ্লোর করি এটিকে ফিক্স করার একটি উপায়।
থ্রেড এবং ডেটা রেস: শয়তানটি ডিটেইলসে আছে
থ্রেড ব্যবহার করে আমাদের IO-হেভি প্রোগ্রামগুলো অনেক দ্রুত চলে, কিন্তু সেগুলো ঠিক করা কঠিন। উপরের রেজাল্টে এররটি handle_response মেথডে একটি রেস কন্ডিশন (race condition) দিয়ে হয়েছে। রেস কন্ডিশন হয় যখন দুটি থ্রেড একই ডেটা ম্যানিপুলেট করে।
যেহেতু আমরা একটি শেয়ার্ড রিসোর্স ( ds ডেটাস্টোর অবজেক্ট) ওপর অপারেট করছি, আমাদের নন-অ্যাটমিক অপারেশনের সাথে খুব সতর্ক থাকতে হবে। লক্ষ্য করুন, আমরা প্রথমে ডেটাস্টোর থেকে রিড করি এবং — দ্বিতীয় স্টেটমেন্টে — কাউন্ট ১ বাড়িয়ে সেটি রাইট করি। এটি সমস্যা কারণ আমাদের থ্রেড রিডের পর কিন্তু রাইটের আগে রুকতে পারে। তখন, যদি অন্য একটি থ্রেড রান করে এবং আমরা যেই কি-তে আগ্রহী সেটির মান ইনক্রিমেন্ট করে, তবে অরিজিনাল থ্রেড রিজিউম হলে আমরা একটি আউট-অফ-ডেট কাউন্ট লিখব।
থ্রেড ব্যবহারের খতরনাক দিক কম করার একটি উপায় হলো হাইয়ার-লেভেল অ্যাবস্ট্রাকশন ব্যবহার করে কনকারেন্ট ইমপ্লিমেন্টেশন স্ট্রাকচার করা।
concurrent-rubygem দেখুন বিভিন্ন প্যাটার্ন এবং সेफার থ্রেড-চালিত প্রোগ্রামের জন্য।
ডেটা রেস ফিক্স করার অনেক উপায় আছে। একটি সিম্পল সমাধান হলো mutex ব্যবহার করা। এই সিঙ্ক্রোনাইজেশন মেকানিজম একটি নির্দিষ্ট কোড সেগমেন্টে একবারে একটিকেই অ্যাক্সেস দেয়। এখানে আমাদের আগের ইমপ্লিমেন্টেশন mutex দিয়ে ফিক্স করা হয়েছে:
# ... ThreadPoweredIntegration ক্লাসের ভিতরে ...
def initialize
# ...
@semaphore = Mutex.new
end
# ...
def handle_response(status, body)
return if status != '200'
key = body.to_sym
semaphore.synchronize do
curr_count = ds.read(key)
ds.write(key, curr_count + 1)
end
end
আপনি যদি Rails অ্যাপ্লিকেশনে থ্রেড ব্যবহার করার পরিকল্পনা করেন, অফিসিয়াল গাইড Threading and Code Execution in Rails অবশ্যই পড়ুন। এই গাইডলাইন ফলো না করলে খুব অপ্রিয় ফলাফল আসতে পারে, যেমন ডাটাবেস কানেকশন লিক।
আমাদের করেক্টেড ইমপ্লিমেন্টেশন রান করলে আমরা এক্সপেক্টেড রেজাল্ট পাই:
{ even: 500, odd: 500 }
Mutex ব্যবহার করার বদলে, আমরা থ্রেড একদম ছাড়া দিয়ে Ruby-এ উপলব্ধ আরেকটি কনকারেন্সি টুলের দিকে যেতে পারি। পরবর্তী সেকশনে, আমরা Fiber-কে IO-হেভি অ্যাপের পারফরম্যান্স উন্নত করার মেকানিজম হিসেবে দেখব।
Fiber: কনকারেন্সির জন্য একটি স্লেন্ডার টুল
Ruby Fibers আপনাকে একটি সিঙ্গল থ্রেডের ভিতরে কো-অপারেটিভ কনকারেন্সি অর্জন করতে দেয়। এর মানে ফাইবার প্রি-এম্পটেড নয় এবং প্রোগ্রামটিই শিডিউলিং করতে হবে। কারণ প্রোগ্রামার কন্ট্রোল করে ফাইবার কবে শুরু এবং বন্ধ হবে, তাই রেস কন্ডিশন এড়ানো অনেক সহজ।
থ্রেডের বিপরীতে, ফাইবার IO হলে আমাদের बेटার পারফরম্যান্স দেয় না। সৌভাগ্যবশত, Ruby তার IO ক্লাস দিয়ে অ্যাসিনক্রোনাস রিড এবং রাইট প্রদান করে। এই অ্যাসিন্ক মেথডগুলো ব্যবহার করে আমরা IO অপারেশন আমাদের ফাইবার-বেসড কোডকে ব্লক করানো থেকে রক্ষা করতে পারি।
একই সিনেরিও, এবার ফাইবার দিয়ে
আমরা একই উদাহরণটি ফাইবার এবং Ruby-এর IO ক্লাসের অ্যাসিন্ক ক্যাপাবিলিটি দিয়ে যাব। Ruby-এ অ্যাসিন্ক IO-এর সব ডিটেইল ব্যাখ্যা করা এই আর্টিকেলের স্কোপের বাইরে। তবে, আমরা এর কাজের অপরিহার্য অংশগুলো স্পর্শ করব এবং আপনি যদি কৌতূহলী হন তবে AdHocHTTP-এর প্রাসঙ্গিক মেথডের ইমপ্লিমেন্টেশন দেখতে পারেন (যেটি থ্রেডেড সলিউশনে দেখেছিলাম)।
আমাদের ফাইবার-চালিত ইমপ্লিমেন্টেশনের run মেথডটি দেখি:
class FiberPoweredIntegration
# ... এক্সেসর এবং ইনিশিয়ালাইজেশন ...
def run
(1..1000).each_slice(250) do |subset|
Fiber.new do
subset.each do |number|
uri = 'https://127.0.0.1:9292/' \
"even_or_odd?number=#{number}"
client = AdHocHTTP.new(uri)
socket = client.init_non_blocking_get
yield_if_waiting(client, socket, :connect_non_blocking_get)
yield_if_waiting(client, socket, :write_non_blocking_get)
status, body = yield_if_waiting(client, socket, :read_non_blocking_get)
handle_response(status, body)
ensure client&.close_non_blocking_get
end
end.resume
end
wait_all_requests
end
# ...
end
আমরা প্রথমে চেক করার নম্বর들의 প্রতিটি সাবসেটের জন্য একটি ফাইবার তৈরি করি।
তারপর নম্বরগুলো লুপ করি, yield_if_waiting কল করি। এই মেথডটি করেন্ট ফাইবার বন্ধ করে অন্যটিকে রিজিউম করার দায়িত্বে।
লক্ষ্য করুন ফাইবার তৈরি করার পর আমরা resume কল করি। এটি ফাইবারকে রান করতে শুরু করে। ক্রিয়েশন直後 resume কল করলে, ১ থেকে ১০০০ পর্যন্ত মেইন লুপ ফিনিশ হওয়ার আগেই আমরা HTTP রিকোয়েস্ট তৈরি করা শুরু করি।
run মেথডের শেষে wait_all_requests কল আছে। এই মেথড রান করার জন্য রেডি ফাইবার সিলেক্ট করে এবং আমরা সব ইচ্ছিত রিকোয়েস্ট করেছি সেটি গ্যারান্টি দেয়। আমরা এটিকে এই সেকশনের লাস্ট সেগমেন্টে দেখব।
এবার yield_if_waiting ডিটেইলে দেখি:
# ... FiberPoweredIntegration-এ ...
def initialize
@ds = Datastore.new(even: 0, odd: 0)
@waiting = { wait_readable: {}, wait_writable: {} }
end
# ...
def yield_if_waiting(client, socket, operation)
res_or_status = client.send(operation)
is_waiting = [:wait_readable, :wait_writable].include?(res_or_status)
return res_or_status unless is_waiting
waiting[res_or_status][socket] = Fiber.current
Fiber.yield
waiting[res_or_status].delete(socket)
yield_if_waiting(client, socket, operation)
rescue Errno::ETIMEDOUT
retry # সার্ভার টাইমআউট দিলে আবার চেষ্টা করুন।
end
আমরা প্রথমে ক্লায়েন্ট ব্যবহার করে একটি অপারেশন (কানেক্ট, রিড, বা রাইট) পারফর্ম করার চেষ্টা করি। দুটি প্রাইমারি আউটকাম সম্ভব:
- সাক্সেস: তখন আমরা রিটার্ন করি।
- একটি সিম্বল পেতে পারি: এর মানে অপেক্ষা করতে হবে।
কীভাবে কেউ "অপেক্ষা" করে?
- আমাদের সকেট করেন্ট ফাইবারসহ ইনস্ট্যান্স ভেরিয়েবল
waiting(যা একটিHash) তে যোগ করে একটি চেকপয়েন্ট তৈরি করি। - এই জোড়াটি একটি কালেকশনে স্টোর করি যা IO রিড বা রাইটের জন্য অপেক্ষা করছে (কেন তা গুরুত্বপূর্ণ সেটি একটু পর দেখব), ক্লায়েন্ট থেকে পাওয়া রেজাল্টের উপর নির্ভর করে।
- করেন্ট ফাইবার এক্সিকিউশন বন্ধ করি, অন্যটিকে রান করতে দেই। পজড ফাইবার কিছু সময় পর সংশ্লিষ্ট নেটওয়ার্ক সকেট রেডি হলে রিজিউম করার সুযোগ পাবে। তারপর IO অপারেশন রিট্রাই হবে (এবার সাক্সেস হবে)।
প্রতিটি Ruby প্রোগ্রাম একটি ফাইবারের ভিতরে চলে যা নিজে একটি থ্রেডের অংশ (সবকিছু একটি প্রসেসের ভিতরে)। ফলস্বরূপ, যখন আমরা প্রথম ফাইবার তৈরি করি, রান করি, এবং কিছু সময় পর yield করি, তবে আমরা প্রোগ্রামের সেন্ট্রাল পার্টের এক্সিকিউশন রিজিউম করছি।
এখন আমরা বুঝতে পারলাম ফাইবার IO-র জন্য অপেক্ষা করলে এক্সিকিউশন yield করার মেকানিজম, চলে এই ফাইবার-চালিত ইমপ্লিমেন্টেশন বুঝতে বাকি লাস্ট বিটটি এক্সপ্লোর করি।
def wait_all_requests
while(waiting[:wait_readable].any? || waiting[:wait_writable].any?)
ready_to_read, ready_to_write = IO.select(waiting[:wait_readable].keys, waiting[:wait_writable].keys)
ready_to_read.each do |socket|
waiting[:wait_readable][socket].resume
end
ready_to_write.each do |socket|
waiting[:wait_writable][socket].resume
end
end
end
মূল আইডিয়া হচ্ছে অপেক্ষা করা (অর্থাৎ লুপ করা) যতক্ষণ না সকল পেন্ডিং IO অপারেশন কমপ্লিট হয়।
এটি করার জন্য আমরা IO.select ব্যবহার করি। এটি পেন্ডিং IO অবজেক্টের দুটি কালেকশন এক্সেপ্ট করে: একটি রিডের জন্য এবং একটি রাইটের জন্য। এটি সেগুলো রিটার্ন করে যাদের কাজ ফিনিশ হয়েছে। কারণ আমরা এই IO অবজেক্টগুলোকে সেগুলো রান করার দায়িত্বশীল ফাইবারসহ অ্যাসোসিয়েট করেছি, সেগুলো রিজিউম করা সিম্পল।
আমরা এই স্টেপগুলো রিপিট করি যতক্ষণ না সকল রিকোয়েস্ট ফায়ার এবং কমপ্লিট হয়।
দ্যা গ্র্যান্ড ফিনালে: কমপেরেবল পারফরম্যান্স, লOCKের প্রয়োজন নেই
আমাদের handle_response মেথডটি ঠিক সেই যে সেম যা থ্রেড ব্যবহার করা কোডে 초기에 ব্যবহার করা হয়েছিল (mutex ছাড়া ভার্সন)। যেহেতু আমাদের সব ফাইবার একই থ্রেডের ভিতরে চলে, আমাদের কোনো ডেটা রেস হবে না। আমরা কোড রান করলে এক্সপেক্টেড রেজাল্ট পাই:
{ even: 500, odd: 500 }
আপনি সম্ভবত প্রতিবার async IO লিভারেজ করার সময় সেই ফাইবার সুইচিং বिजনেসে পড়তে চান না। সৌভাগ্যবশত, কিছু জেম এই সকল কাজ অ্যাবস্ট্রাক্ট করে এবং ফাইবার ব্যবহারকে এমন কিছু করে দেয় যা ডেভেলপারকে চিন্তা করতে হয় না।
asyncপ্রজেক্টটি চেক আউট করুন একটি গ্রেট স্টার্ট হিসেবে।
ফাইবার চমকে উঠে যখন হাই স্কেলেবিলিটি বাধ্যতামূলক
যদিও আমরা স্মল স্কেল সিনেরিওতে ডেটা রেস এর রিস্ক ভার্চুয়ালি এলিমিনেট করার সুবিধা পাই, ফাইবার একটি চমৎকার টুল যখন হাই স্কেলেবিলিটি দরকার। ফাইবার থ্রেডের চেয়ে অনেক লাইটওয়েট। একই উপলব্ধ রিসোর্সে, থ্রেড তৈরি করলে সিস্টেম অনেক আগে অভিভূত হবে ফাইবারের তুলনায়। এই টপিকে চমৎকার এক্সপ্লোরেশনের জন্য Ruby Core Team-এর স্যামুয়েল 윌리엄স-এর প্রেজেন্টেশন The Journey to One Million দেখুন।
Guild - Ruby-এ প্যারাালেল প্রোগ্রামিং
এতক্ষণে আমরা Ruby-এ কনকারেন্সির দুটি উপযোগী টুল দেখেছি। কিন্তু এদের কোনোটিই পিউর কম্পুটেশনের পারফরম্যান্স উন্নত করতে পারে না। उसके জন্য আপনাকে ট্রু প্যারাাললিজম দরকার, যা বর্তমানে Ruby-তে নেই (এখানে আমরা MRI, ডিফল্ট ইমপ্লিমেন্টেশন কনসিডার করছি)।
এটি Ruby 3-এ "Guilds" নামে একটি নতুন ফিচারের আগমনের সাথে বদলতে পারে। ডিটেইলস এখনো হেজি, কিন্তু নিচের সেকশনে আমরা দেখব এই ওয়ার্ক-ইন-প্রোগ্রেস ফিচার কীভাবে Ruby-এ প্যারাাললিজম আনার обещает দিচ্ছে।
Guilds কীভাবে কাজ করতে পারে
কনকারেন্ট/প্যারাালেল সলিউশন ইমপ্লিমেন্ট করার সময় শেয়ার্ড মেমরি একটি বড় পেইন পয়েন্ট। থ্রেড সেকশনে আমরা ইতিমধ্যে দেখেছি কীভাবে সহজে একটি স্লিপ হতে পারে এবং কোড লিখা যায় যা প্রথম দৃষ্টিতে অনিন্নোকেন্স লাগে কিন্তু আসলে সাবটেল বাগ ধারণ করে।
কোয়িচি সাসাদা — Ruby Core Team-এর সদস্য যিনি নতুন Guild ফিচার ডেভেলপমেন্ট হেড করছেন — শেয়ার্ড মেমরি একাধিক থ্রেডের মধ্যে যেসব ডেঞ্জার আছে সেগুলো সরাসরি ট্যাকল করার একটি সমাধান ডিজাইন করার জন্য কঠোর পরিশ্রম করছেন। ২০১৮ RubyConf-এ তাঁর প্রেজেন্টেশনে তিনি ব্যাখ্যা করেছেন যে গিল্ড ব্যবহার করলে কেউ সিম্পলি মিউটেবল অবজেক্ট শেয়ার করতে পারবে না। মূল আইডিয়া হচ্ছে শুধু ইমমিউটেবল অবজেক্ট শেয়ার করার অনুমতি দিয়ে ডেটা রেস প্রিভেন্ট করা।
স্পেশালাইজড ডেটা স্ট্রাকচার Ruby-এ যোগ করা হবে যাতে গিল্ডের মধ্যে কিছু মাপের শেয়ার্ড মেমরি থাকতে পারে, কিন্তু কীভাবে একদম কাজ করবে সেটি এখনো ফুলি ফ্লেশড আউট নয়। এছাড়া একটি API থাকবে যা অবজেক্টগুলোকে গিল্ডের মধ্যে কপি বা মুভ করতে দেবে, প্লাস একটি সेफগার্ড যাতে একটি অবজেক্ট অন্য গিল্ডে মুভ হওয়ার পর রেফারেন্স করা যাবে না।
Guilds ব্যবহার করে একটি কমন সিনেরিও এক্সপ্লোর করা
অনেক সিনেরিও আছে যেখানে আপনি কামনা করবেন কম্পুটেশন প্যারাালেলে রান করে স্পিড আপ করতে। কল্পনা করুন আমাদের একই ডেটাসেটের এভারেজ এবং মিন ক্যালকুলেট করতে হচ্ছে।
নিচের উদাহরণ দেখায় আমরা গিল্ড দিয়ে কীভাবে এটি করতে পারি। মনে রাখবেন এই কোড বর্তমানে কাজ করে না এবং হয়তো গিল্ড রিলিজ হওয়ার পরেও কখনো কাজ করবে না।
# একটি ফ্রোজেন অ্যারে নমেরিক ভ্যালুর একটি ইমমিউটেবল অবজেক্ট।
dataset = [88, 43, 37, 85, 84, 38, 13, 84, 17, 87].freeze
# গিল্ড ব্যবহার করার ওভারহেড সম্ভবত ভारी হবে,
# তাই বড় ডেটাসেট / অনেক অপারেশন করার সময়ই
# প্যারাালেলাইজ করা অর্থবহ হবে।
g1 = Guild.new do
mean = dataset.reduce(:+).fdiv(dataset.length)
Guild.send_to(:mean, Guild.parent)
end
g2 = Guild.new do
median = Median.calculate(dataset.sort)
Guild.send_to(:median, Guild.parent)
end
results = {}
# প্রতিটি Ruby প্রোগ্রাম একটি মেইন গিল্ডের ভিতরে রান হবে;
# তাই আমরা মেইন সেকশনে মেসেজ রিসিভও করতে পারি।
Guild.receive(:mean, :median) do |tag, result|
results[tag] = result
end
সারমর্ম
কনকারেন্সি এবং প্যারাাললিজম Ruby-এর মূল শক্তি নয়, কিন্তু এই ডিপার্টমেন্টেও ভাষাটি কিছু টুল অফার করে যা প্রায় সব ইউজ কেসে হ্যান্ডেল করার জন্য যথেষ্ট ভালো। Ruby 3 আসছে এবং Guild প্রিমিটিভের সফরে জিনিসগুলো নিখুঁতভাবে बेटার হয়ে যাচ্ছে বলে মনে হয়। আমার মতামতে, Ruby অনেক সিনেরিওতে এখনো খুব উপযুক্ত পছন্দ, এবং এর কমিউনিটি স্পষ্টভাবে ভাষাকে আরও ভালো করার জন্য কঠোর পরিশ্রম করছে। আগামী কী আসছে সেটির জন্য লেজ না দিয়ে থাকুন!