ஒரேநேர ஸ்ட்ரீம் செயலாக்கத்திற்கான மேம்பட்ட ஜாவாஸ்கிரிப்ட் நுட்பங்களை ஆராயுங்கள். உயர்-செயல்திறன் API அழைப்புகள் மற்றும் கோப்பு செயலாக்கத்திற்காக இணை இட்டரேட்டர் ஹெல்பர்களை உருவாக்க கற்றுக்கொள்ளுங்கள்.
உயர்-செயல்திறன் ஜாவாஸ்கிரிப்டைத் திறத்தல்: இட்டரேட்டர் ஹெல்பர் இணை செயலாக்கம் மற்றும் ஒரேநேர ஸ்ட்ரீம்களில் ஒரு ஆழமான பார்வை
நவீன மென்பொருள் மேம்பாட்டு உலகில், டேட்டாதான் ராஜா. APIகள், தரவுத்தளங்கள், அல்லது கோப்பு முறைமைகள் என எதுவாக இருந்தாலும், பரந்த டேட்டா ஸ்ட்ரீம்களைச் செயலாக்கும் சவாலை நாம் தொடர்ந்து எதிர்கொள்கிறோம். ஜாவாஸ்கிரிப்ட் டெவலப்பர்களுக்கு, மொழியின் ஒற்றை-திரெட் தன்மை ஒரு குறிப்பிடத்தக்க தடையாக இருக்கலாம். ஒரு பெரிய டேட்டாசெட்டை செயலாக்கும் நீண்ட நேரம் இயங்கும், ஒத்திசைவான லூப், ஒரு பிரவுசரில் பயனர் இடைமுகத்தை முடக்கலாம் அல்லது நோட்.ஜேஎஸ்-இல் ஒரு சேவையகத்தை ஸ்தம்பிக்கச் செய்யலாம். இந்த தீவிரமான பணிச்சுமைகளைத் திறமையாகக் கையாளக்கூடிய, பதிலளிக்கக்கூடிய, உயர்-செயல்திறன் கொண்ட பயன்பாடுகளை நாம் எவ்வாறு உருவாக்குவது?
இதற்கான பதில் ஒத்திசைவற்ற முறைகளில் தேர்ச்சி பெறுவதிலும், ஒரேநேர செயல்பாடுகளை ஏற்றுக்கொள்வதிலும் உள்ளது. ஜாவாஸ்கிரிப்ட்டிற்கான வரவிருக்கும் இட்டரேட்டர் ஹெல்பர்ஸ் முன்மொழிவு, ஒத்திசைவான சேகரிப்புகளுடன் நாம் பணிபுரியும் விதத்தில் புரட்சியை ஏற்படுத்தும் என்று உறுதியளித்தாலும், அதன் உண்மையான ஆற்றலை அதன் கொள்கைகளை ஒத்திசைவற்ற உலகிற்கு விரிவுபடுத்தும்போதுதான் திறக்க முடியும். இந்தக் கட்டுரை இட்டரேட்டர் போன்ற ஸ்ட்ரீம்களுக்கான இணை செயலாக்கம் என்ற கருத்தைப் பற்றிய ஒரு ஆழமான பார்வை. அதிக செயல்திறன் கொண்ட API அழைப்புகள் மற்றும் இணை தரவு மாற்றங்கள் போன்ற பணிகளைச் செய்ய நமது சொந்த ஒரேநேர ஸ்ட்ரீம் ஆபரேட்டர்களை எவ்வாறு உருவாக்குவது என்பதை நாம் ஆராய்வோம், செயல்திறன் தடைகளைத் திறமையான, தடுக்காத பைப்லைன்களாக மாற்றுவோம்.
அடிப்படை: இட்டரேட்டர்கள் மற்றும் இட்டரேட்டர் ஹெல்பர்களைப் புரிந்துகொள்ளுதல்
நாம் ஓடுவதற்கு முன், நடக்க கற்றுக்கொள்ள வேண்டும். நமது மேம்பட்ட முறைகளுக்கான அடித்தளத்தை உருவாக்கும் ஜாவாஸ்கிரிப்டில் உள்ள இட்டரேஷனின் முக்கியக் கருத்துக்களைச் சுருக்கமாக மீண்டும் பார்ப்போம்.
இட்டரேட்டர் புரோட்டோகால் என்றால் என்ன?
இட்டரேட்டர் புரோட்டோகால் என்பது மதிப்புகளின் வரிசையை உருவாக்குவதற்கான ஒரு நிலையான வழியாகும். ஒரு ஆப்ஜெக்ட், இரண்டு பண்புகளைக் கொண்ட ஒரு ஆப்ஜெக்ட்டைத் திருப்பியளிக்கும் next() முறையைக் கொண்டிருக்கும்போது அது ஒரு இட்டரேட்டராகும்:
value: வரிசையில் உள்ள அடுத்த மதிப்பு.done: இட்டரேட்டர் தீர்ந்துவிட்டால்trueஆகவும், இல்லையெனில்falseஆகவும் இருக்கும் ஒரு பூலியன்.
ஒரு குறிப்பிட்ட எண் வரை எண்ணும் ஒரு தனிப்பயன் இட்டரேட்டரின் எளிய உதாரணம் இங்கே:
function createCounter(limit) {
let count = 0;
return {
next: function() {
if (count < limit) {
return { value: count++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
const counter = createCounter(3);
console.log(counter.next()); // { value: 0, done: false }
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: undefined, done: true }
அரேக்கள், மேப்கள் மற்றும் ஸ்டிரிங்குகள் போன்ற ஆப்ஜெக்ட்கள் "iterable" ஆகும், ஏனெனில் அவை ஒரு இட்டரேட்டரைத் திருப்பியளிக்கும் [Symbol.iterator] முறையைக் கொண்டுள்ளன. இதுவே அவற்றை for...of லூப்களில் பயன்படுத்த நமக்கு உதவுகிறது.
இட்டரேட்டர் ஹெல்பர்களின் வாக்குறுதி
TC39 இட்டரேட்டர் ஹெல்பர்ஸ் முன்மொழிவு, Iterator.prototype மீது நேரடியாகப் பயன்பாட்டு முறைகளின் தொகுப்பைச் சேர்ப்பதை நோக்கமாகக் கொண்டுள்ளது. இது நாம் ஏற்கனவே Array.prototype மீது கொண்டுள்ள map, filter, மற்றும் reduce போன்ற சக்திவாய்ந்த முறைகளைப் போன்றது, ஆனால் எந்தவொரு iterable ஆப்ஜெக்ட்டிற்கும் பொருந்தும். இது வரிசைகளைச் செயலாக்க ஒரு மேலும் அறிவிப்புரீதியான மற்றும் நினைவக-திறனுள்ள வழியை அனுமதிக்கிறது.
இட்டரேட்டர் ஹெல்பர்களுக்கு முன் (பழைய முறை):
const numbers = [1, 2, 3, 4, 5, 6];
// To get the sum of squares of even numbers, we create intermediate arrays.
const evenNumbers = numbers.filter(n => n % 2 === 0);
const squares = evenNumbers.map(n => n * n);
const sum = squares.reduce((acc, n) => acc + n, 0);
console.log(sum); // 56 (2*2 + 4*4 + 6*6)
இட்டரேட்டர் ஹெல்பர்களுடன் (முன்மொழியப்பட்ட எதிர்காலம்):
const numbersIterator = [1, 2, 3, 4, 5, 6].values();
// No intermediate arrays are created. Operations are lazy and pulled one by one.
const sum = numbersIterator
.filter(n => n % 2 === 0) // returns a new iterator
.map(n => n * n) // returns another new iterator
.reduce((acc, n) => acc + n, 0); // consumes the final iterator
console.log(sum); // 56
முக்கியமான விஷயம் என்னவென்றால், இந்த முன்மொழியப்பட்ட ஹெல்பர்கள் வரிசையாக மற்றும் ஒத்திசைவாக செயல்படுகின்றன. அவை ஒரு உருப்படியை இழுத்து, சங்கிலி வழியாகச் செயலாக்கி, பின்னர் அடுத்ததை இழுக்கின்றன. இது நினைவகத் திறனுக்குச் சிறந்தது, ஆனால் நேரத்தைச் செலவழிக்கும், I/O-சார்ந்த செயல்பாடுகளுடனான நமது செயல்திறன் சிக்கலைத் தீர்க்காது.
ஒற்றை-திரெட் ஜாவாஸ்கிரிப்டில் உள்ள ஒரேநேர சவால்
ஜாவாஸ்கிரிப்டின் இயக்க மாதிரி ஒரு நிகழ்வு வளையத்தை (event loop) சுற்றி, பிரபலமாக ஒற்றை-திரெட் கொண்டது. அதாவது, அதன் முக்கிய அழைப்பு அடுக்கில் (call stack) ஒரு நேரத்தில் ஒரு குறியீட்டை மட்டுமே இயக்க முடியும். ஒரு ஒத்திசைவான, CPU-தீவிரமான பணி (ஒரு பெரிய லூப் போன்றவை) இயங்கும்போது, அது அழைப்பு அடுக்கைத் தடுக்கிறது. ஒரு பிரவுசரில், இது உறைந்த UI-க்கு வழிவகுக்கிறது. ஒரு சேவையகத்தில், இது சேவையகம் வேறு எந்த உள்வரும் கோரிக்கைகளுக்கும் பதிலளிக்க முடியாது என்பதைக் குறிக்கிறது.
இங்குதான் நாம் ஒரேநேரம் (concurrency) மற்றும் இணை (parallelism) ஆகியவற்றுக்கு இடையேயான வேறுபாட்டை அறிய வேண்டும்:
- ஒரேநேரம் (Concurrency) என்பது ஒரு குறிப்பிட்ட காலப்பகுதியில் பல பணிகளை நிர்வகிப்பதாகும். நிகழ்வு வளையம் ஜாவாஸ்கிரிப்டை அதிக ஒரேநேர செயல்பாடு கொண்டதாக இருக்க அனுமதிக்கிறது. அது ஒரு நெட்வொர்க் கோரிக்கையை (ஒரு I/O செயல்பாடு) தொடங்கலாம், மற்றும் பதிலுக்காகக் காத்திருக்கும்போது, பயனர் கிளிக்குகள் அல்லது பிற நிகழ்வுகளைக் கையாளலாம். பணிகள் ஒரே நேரத்தில் இயங்காமல், ஒன்றோடொன்று பிணைந்து செயல்படுகின்றன.
- இணை (Parallelism) என்பது ஒரே நேரத்தில் பல பணிகளை இயக்குவதாகும். ஜாவாஸ்கிரிப்டில் உண்மையான இணை செயல்பாடு பொதுவாக பிரவுசரில் வெப் வொர்க்கர்ஸ் அல்லது நோட்.ஜேஎஸ்-இல் வொர்க்கர் திரெட்கள்/குழந்தை செயல்முறைகள் போன்ற தொழில்நுட்பங்களைப் பயன்படுத்தி அடையப்படுகிறது, அவை அவற்றின் சொந்த நிகழ்வு வளையங்களுடன் தனித்தனி திரெட்களை வழங்குகின்றன.
நமது நோக்கங்களுக்காக, I/O-சார்ந்த செயல்பாடுகளுக்கு (API அழைப்புகள் போன்றவை) அதிக ஒரேநேர செயல்பாட்டை அடைவதில் கவனம் செலுத்துவோம், இங்குதான் மிக முக்கியமான நிஜ-உலக செயல்திறன் ஆதாயங்கள் பெரும்பாலும் காணப்படுகின்றன.
முன்மாதிரி மாற்றம்: ஒத்திசைவற்ற இட்டரேட்டர்கள் (Asynchronous Iterators)
காலப்போக்கில் வந்து சேரும் டேட்டா ஸ்ட்ரீம்களை (நெட்வொர்க் கோரிக்கை அல்லது ஒரு பெரிய கோப்பிலிருந்து வருவது போன்றவை) கையாள, ஜாவாஸ்கிரிப்ட் அசிங்க் இட்டரேட்டர் புரோட்டோகாலை அறிமுகப்படுத்தியது. இது அதன் ஒத்திசைவான உறவினரைப் போன்றதுதான், ஆனால் ஒரு முக்கிய வேறுபாட்டுடன்: next() முறை, { value, done } ஆப்ஜெக்ட்டாகத் தீர்க்கப்படும் ஒரு Promise-ஐத் திருப்பியளிக்கிறது.
இது எல்லா டேட்டாவும் ஒரே நேரத்தில் கிடைக்காத டேட்டா மூலங்களுடன் பணிபுரிய நம்மை அனுமதிக்கிறது. இந்த அசிங்க் ஸ்ட்ரீம்களை மென்மையாகப் பயன்படுத்த, நாம் for await...of லூப்பைப் பயன்படுத்துகிறோம்.
ஒரு API-யிலிருந்து பக்கங்களாக டேட்டாவைப் பெறுவதை உருவகப்படுத்தும் ஒரு அசிங்க் இட்டரேட்டரை உருவாக்குவோம்:
async function* fetchPaginatedData(url) {
let nextPageUrl = url;
while (nextPageUrl) {
console.log(`Fetching from ${nextPageUrl}...`);
const response = await fetch(nextPageUrl);
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
// Yield each item from the current page's results
for (const item of data.results) {
yield item;
}
// Move to the next page, or stop if there isn't one
nextPageUrl = data.nextPage;
}
}
// Usage:
async function processUsers() {
const userStream = fetchPaginatedData('https://api.example.com/users');
for await (const user of userStream) {
console.log(`Processing user: ${user.name}`);
// This is still sequential processing. We wait for one user to be logged
// before the next one is even requested from the stream.
}
}
இது ஒரு சக்திவாய்ந்த முறை, ஆனால் லூப்பில் உள்ள கருத்தைக் கவனியுங்கள். செயலாக்கம் வரிசையாக உள்ளது. `process user` என்பது மற்றொரு மெதுவான, அசிங்க் செயல்பாட்டை (ஒரு தரவுத்தளத்தில் சேமிப்பது போன்றவை) உள்ளடக்கியிருந்தால், அடுத்ததைத் தொடங்குவதற்கு முன் ஒவ்வொன்றும் முடிவடையும் வரை நாம் காத்திருப்போம். இதுதான் நாம் அகற்ற விரும்பும் தடை.
இட்டரேட்டர் ஹெல்பர்களுடன் ஒரேநேர ஸ்ட்ரீம் செயல்பாடுகளை வடிவமைத்தல்
இப்போது நாம் நமது விவாதத்தின் மையத்திற்கு வருகிறோம். முந்தைய உருப்படி முடிவடையும் வரை காத்திருக்காமல், ஒரு ஒத்திசைவற்ற ஸ்ட்ரீமிலிருந்து உருப்படிகளை ஒரே நேரத்தில் எவ்வாறு செயலாக்க முடியும்? நாம் ஒரு தனிப்பயன் அசிங்க் இட்டரேட்டர் ஹெல்பரை உருவாக்குவோம், அதை asyncMapConcurrent என்று அழைப்போம்.
இந்தச் செயல்பாடு மூன்று வாதங்களை எடுக்கும்:
sourceIterator: நாம் உருப்படிகளை இழுக்க விரும்பும் அசிங்க் இட்டரேட்டர்.mapperFn: ஒவ்வொரு உருப்படிக்கும் பயன்படுத்தப்படும் ஒரு அசிங்க் செயல்பாடு.concurrency: ஒரே நேரத்தில் எத்தனை `mapperFn` செயல்பாடுகள் இயங்கலாம் என்பதை வரையறுக்கும் ஒரு எண்.
முக்கியக் கருத்து: ப்ராமிஸ்களின் ஒரு வொர்க்கர் பூல்
இந்த உத்தியானது, செயலில் உள்ள ப்ராமிஸ்களின் ஒரு "குளம்" அல்லது தொகுப்பை பராமரிப்பதாகும். இந்தக் குளத்தின் அளவு நமது concurrency அளவுருவால் வரையறுக்கப்படும்.
- நாம் மூல இட்டரேட்டரிலிருந்து உருப்படிகளை இழுத்து, அவற்றுக்கான அசிங்க் `mapperFn`-ஐத் தொடங்குவதன் மூலம் ஆரம்பிக்கிறோம்.
mapperFnமூலம் திருப்பியளிக்கப்பட்ட ப்ராமிஸை நமது செயலில் உள்ள குளத்தில் சேர்க்கிறோம்.- குளம் நிரம்பும் வரை (அதன் அளவு நமது
concurrencyநிலைக்குச் சமமாகும் வரை) இதைத் தொடர்கிறோம். - குளம் நிரம்பியதும், *அனைத்து* ப்ராமிஸ்களுக்கும் காத்திருப்பதற்குப் பதிலாக, அவற்றில் *ஒன்று* மட்டும் முடிவடையும் வரை காத்திருக்க
Promise.race()-ஐப் பயன்படுத்துகிறோம். - ஒரு ப்ராமிஸ் முடிந்ததும், அதன் முடிவை நாம் ஈல்டு (yield) செய்து, அதை குளத்திலிருந்து அகற்றிவிடுகிறோம், இப்போது ஒரு புதியதைச் சேர்க்க இடம் உள்ளது.
- மூலத்திலிருந்து அடுத்த உருப்படியை இழுத்து, அதன் செயலாக்கத்தைத் தொடங்கி, புதிய ப்ராமிஸை குளத்தில் சேர்த்து, சுழற்சியை மீண்டும் செய்கிறோம்.
இது ஒரு தொடர்ச்சியான ஓட்டத்தை உருவாக்குகிறது, அங்கு வரையறுக்கப்பட்ட ஒரேநேர வரம்பு வரை வேலை எப்போதும் செய்யப்பட்டுக்கொண்டே இருக்கும், டேட்டா செயலாக்கப்பட இருக்கும் வரை நமது செயலாக்க பைப்லைன் ஒருபோதும் சும்மா இருக்காது என்பதை உறுதி செய்கிறது.
`asyncMapConcurrent`-இன் படிப்படியான செயல்படுத்தல்
இந்த யூட்டிலிட்டியை உருவாக்குவோம். இது ஒரு அசிங்க் ஜெனரேட்டர் செயல்பாடாக இருக்கும், இது அசிங்க் இட்டரேட்டர் புரோட்டோகாலைச் செயல்படுத்துவதை எளிதாக்குகிறது.
async function* asyncMapConcurrent(sourceIterator, mapperFn, concurrency = 5) {
const activePromises = new Set();
const source = sourceIterator[Symbol.asyncIterator]();
while (true) {
// 1. Fill the pool up to the concurrency limit
while (activePromises.size < concurrency) {
const { value, done } = await source.next();
if (done) {
// Source iterator is exhausted, break the inner loop
break;
}
const promise = (async () => {
try {
return { result: await mapperFn(value), error: null };
} catch (e) {
return { result: null, error: e };
}
})();
activePromises.add(promise);
// Also, attach a cleanup function to the promise to remove it from the set upon completion.
promise.finally(() => activePromises.delete(promise));
}
// 2. Check if we are done
if (activePromises.size === 0) {
// The source is exhausted and all active promises have finished.
return; // End the generator
}
// 3. Wait for any promise in the pool to finish
const completed = await Promise.race(activePromises);
// 4. Handle the result
if (completed.error) {
// We can decide on an error handling strategy. Here, we re-throw.
throw completed.error;
}
// 5. Yield the successful result
yield completed.result;
}
}
செயல்படுத்தலை விரிவாகப் பார்ப்போம்:
- நாம்
activePromises-க்கு ஒருSet-ஐப் பயன்படுத்துகிறோம். செட்கள் தனித்துவமான ஆப்ஜெக்ட்களை (ப்ராமிஸ்கள் போன்றவை) சேமிக்க வசதியானவை மற்றும் விரைவான சேர்த்தல் மற்றும் நீக்குதலை வழங்குகின்றன. - வெளியிலுள்ள
while (true)லூப், நாம் வெளிப்படையாக வெளியேறும் வரை செயல்முறையைத் தொடர்கிறது. - உள்ளிருக்கும்
while (activePromises.size < concurrency)லூப் நமது வொர்க்கர் குளத்தை நிரப்புவதற்குப் பொறுப்பாகும். அது தொடர்ந்துsourceஇட்டரேட்டரிலிருந்து இழுக்கிறது. - மூல இட்டரேட்டர்
doneஆகும்போது, புதிய ப்ராமிஸ்களைச் சேர்ப்பதை நிறுத்திவிடுகிறோம். - ஒவ்வொரு புதிய உருப்படிக்கும், நாம் உடனடியாக ஒரு அசிங்க் IIFE (Immediately Invoked Function Expression) ஐ அழைக்கிறோம். இது
mapperFn-ஐ உடனடியாக இயக்கத் தொடங்குகிறது. மேப்பரிலிருந்து ஏற்படக்கூடிய பிழைகளை மென்மையாகக் கையாளவும்,{ result, error }என்ற சீரான ஆப்ஜெக்ட் வடிவத்தைத் திருப்பியளிக்கவும் அதை ஒரு `try...catch` பிளாக்கில் போர்த்துகிறோம். - முக்கியமாக, நாம்
promise.finally(() => activePromises.delete(promise))-ஐப் பயன்படுத்துகிறோம். இது ப்ராமிஸ் தீர்க்கப்பட்டாலும் அல்லது நிராகரிக்கப்பட்டாலும், அது நமது செயலில் உள்ள தொகுப்பிலிருந்து அகற்றப்படுவதை உறுதிசெய்கிறது, புதிய வேலைக்கு இடம் கொடுக்கிறது. `Promise.race`-க்குப் பிறகு ப்ராமிஸைக் கைமுறையாகக் கண்டுபிடித்து அகற்ற முயற்சிப்பதை விட இது ஒரு சுத்தமான அணுகுமுறை. Promise.race(activePromises)என்பது ஒரேநேர செயல்பாட்டின் இதயமாகும். இது தொகுப்பிலுள்ள *முதல்* ப்ராமிஸ் தீர்க்கப்பட்டவுடன் அல்லது நிராகரிக்கப்பட்டவுடன் தீர்க்கப்படும் அல்லது நிராகரிக்கப்படும் ஒரு புதிய ப்ராமிஸைத் திருப்பியளிக்கிறது.- ஒரு ப்ராமிஸ் முடிந்ததும், நாம் நமது போர்த்தப்பட்ட முடிவை ஆய்வு செய்கிறோம். ஒரு பிழை இருந்தால், அதை நாம் வீசுகிறோம், ஜெனரேட்டரை நிறுத்துகிறோம் (ஒரு fail-fast உத்தி). அது வெற்றிகரமாக இருந்தால், நமது
asyncMapConcurrentஜெனரேட்டரின் நுகர்வோருக்கு முடிவைyieldசெய்கிறோம். - இறுதி வெளியேறும் நிபந்தனை, மூலம் தீர்ந்து,
activePromisesதொகுப்பு காலியாகும்போது ஏற்படுகிறது. இந்த கட்டத்தில், வெளியிலுள்ள லூப் நிபந்தனைactivePromises.size === 0பூர்த்தி செய்யப்படுகிறது, மற்றும் நாம்returnசெய்கிறோம், இது நமது அசிங்க் ஜெனரேட்டரின் முடிவைக் குறிக்கிறது.
நடைமுறைப் பயன்பாட்டு வழக்குகள் மற்றும் உலகளாவிய எடுத்துக்காட்டுகள்
இந்த முறை ஒரு கல்விப் பயிற்சி மட்டுமல்ல. இது நிஜ-உலகப் பயன்பாடுகளுக்கு ஆழமான தாக்கங்களைக் கொண்டுள்ளது. சில காட்சிகளை ஆராய்வோம்.
பயன்பாட்டு வழக்கு 1: அதிக செயல்திறன் கொண்ட API தொடர்புகள்
சூழல்: நீங்கள் ஒரு உலகளாவிய இ-காமர்ஸ் தளத்திற்கான ஒரு சேவையை உருவாக்குகிறீர்கள் என்று கற்பனை செய்து கொள்ளுங்கள். உங்களிடம் 50,000 தயாரிப்பு ஐடிகளின் பட்டியல் உள்ளது, ஒவ்வொன்றிற்கும், ஒரு குறிப்பிட்ட பகுதிக்கான சமீபத்திய விலையைப் பெற நீங்கள் ஒரு விலை நிர்ணய API-ஐ அழைக்க வேண்டும்.
வரிசைமுறைத் தடை:
async function updateAllPrices(productIds) {
const startTime = Date.now();
for (const id of productIds) {
await fetchPrice(id); // Assume this takes ~200ms
}
console.log(`Total time: ${(Date.now() - startTime) / 1000}s`);
}
// Estimated time for 50,000 products: 50,000 * 0.2s = 10,000 seconds (~2.7 hours!)
ஒரேநேர தீர்வு:
// Helper function to simulate a network request
function fetchPrice(productId) {
return new Promise(resolve => {
setTimeout(() => {
const price = (Math.random() * 100).toFixed(2);
console.log(`Fetched price for ${productId}: $${price}`);
resolve({ productId, price });
}, 200 + Math.random() * 100); // Simulate variable network latency
});
}
async function updateAllPricesConcurrently() {
const productIds = Array.from({ length: 50 }, (_, i) => `product-${i + 1}`);
const idIterator = productIds.values(); // Create a simple iterator
// Use our concurrent mapper with a concurrency of 10
const priceStream = asyncMapConcurrent(idIterator, fetchPrice, 10);
const startTime = Date.now();
for await (const priceData of priceStream) {
// Here you would save the priceData to your database
// console.log(`Processed: ${priceData.productId}`);
}
console.log(`Concurrent total time: ${(Date.now() - startTime) / 1000}s`);
}
updateAllPricesConcurrently();
// Expected output: A flurry of "Fetched price..." logs, and a total time
// that is roughly (Total Items / Concurrency) * Avg Time per Item.
// For 50 items at 200ms with concurrency 10: (50/10) * 0.2s = ~1 second (plus latency variance)
// For 50,000 items: (50000/10) * 0.2s = 1000 seconds (~16.7 minutes). A huge improvement!
உலகளாவிய கவனம்: API விகித வரம்புகளை மனதில் கொள்ளுங்கள். ஒரேநேர அளவை மிகவும் அதிகமாக அமைப்பது உங்கள் IP முகவரியைத் தடுக்கலாம். பல பொது API-களுக்கு 5-10 என்ற ஒரேநேர அளவு ஒரு பாதுகாப்பான தொடக்கப் புள்ளியாகும்.
பயன்பாட்டு வழக்கு 2: நோட்.ஜேஎஸ்-இல் இணை கோப்பு செயலாக்கம்
சூழல்: நீங்கள் மொத்த படப் பதிவேற்றங்களை ஏற்கும் ஒரு உள்ளடக்க மேலாண்மை அமைப்பை (CMS) உருவாக்குகிறீர்கள். ஒவ்வொரு பதிவேற்றப்பட்ட படத்திற்கும், நீங்கள் மூன்று வெவ்வேறு சிறுபட அளவுகளை உருவாக்கி, அவற்றை AWS S3 அல்லது Google Cloud Storage போன்ற ஒரு கிளவுட் சேமிப்பக வழங்குநருக்குப் பதிவேற்ற வேண்டும்.
வரிசைமுறைத் தடை: ஒரு படத்தை முழுமையாகச் செயலாக்குவது (படித்தல், மூன்று முறை அளவை மாற்றுதல், மூன்று முறை பதிவேற்றுதல்) அடுத்ததைத் தொடங்குவதற்கு முன் மிகவும் திறமையற்றது. இது CPU (பதிவேற்றங்களுக்கான I/O காத்திருப்புகளின் போது) மற்றும் நெட்வொர்க் (CPU-சார்ந்த அளவு மாற்றத்தின் போது) இரண்டையும் குறைவாகப் பயன்படுத்துகிறது.
ஒரேநேர தீர்வு:
const fs = require('fs/promises');
const path = require('path');
// Assume 'sharp' for resizing and 'aws-sdk' for uploading are available
async function processImage(filePath) {
console.log(`Processing ${path.basename(filePath)}...`);
const imageBuffer = await fs.readFile(filePath);
const sizes = [{w: 100, h: 100}, {w: 300, h: 300}, {w: 600, h: 600}];
const uploadTasks = sizes.map(async (size) => {
const thumbnailBuffer = await sharp(imageBuffer).resize(size.w, size.h).toBuffer();
return uploadToCloud(thumbnailBuffer, `thumb_${size.w}_${path.basename(filePath)}`);
});
await Promise.all(uploadTasks);
console.log(`Finished ${path.basename(filePath)}`);
return { source: filePath, status: 'processed' };
}
async function run() {
const imageDir = './uploads';
const files = await fs.readdir(imageDir);
const filePaths = files.map(f => path.join(imageDir, f));
// Get the number of CPU cores to set a sensible concurrency level
const concurrency = require('os').cpus().length;
const processingStream = asyncMapConcurrent(filePaths.values(), processImage, concurrency);
for await (const result of processingStream) {
console.log(result);
}
}
இந்த எடுத்துக்காட்டில், நாம் ஒரேநேர அளவை கிடைக்கக்கூடிய CPU கோர்களின் எண்ணிக்கைக்கு அமைக்கிறோம். இது CPU-சார்ந்த பணிகளுக்கான ஒரு பொதுவான உத்தியாகும், இது அமைப்பு இணையாகக் கையாளக்கூடியதை விட அதிக வேலையால் நிரம்பி வழியாமல் இருப்பதை உறுதி செய்கிறது.
செயல்திறன் பரிசீலனைகள் மற்றும் சிறந்த நடைமுறைகள்
ஒரேநேர செயல்பாட்டைச் செயல்படுத்துவது சக்தி வாய்ந்தது, ஆனால் அது ஒரு வெள்ளித் தோட்டா அல்ல. இது சிக்கலை அறிமுகப்படுத்துகிறது மற்றும் கவனமான பரிசீலனை தேவைப்படுகிறது.
சரியான ஒரேநேர அளவைத் தேர்ந்தெடுத்தல்
உகந்த ஒரேநேர அளவு எப்போதும் "முடிந்தவரை உயர்வானது" அல்ல. இது பணியின் தன்மையைப் பொறுத்தது:
- I/O-சார்ந்த பணிகள் (எ.கா., API அழைப்புகள், தரவுத்தள வினவல்கள்): உங்கள் குறியீடு அதன் பெரும்பாலான நேரத்தை வெளிப்புற வளங்களுக்காகக் காத்திருப்பதில் செலவிடுகிறது. நீங்கள் பெரும்பாலும் ஒரு உயர் ஒரேநேர அளவைப் பயன்படுத்தலாம் (எ.கா., 10, 50, அல்லது 100 கூட), இது முதன்மையாக வெளிப்புறச் சேவையின் விகித வரம்புகள் மற்றும் உங்கள் சொந்த நெட்வொர்க் அலைவரிசையால் வரையறுக்கப்படுகிறது.
- CPU-சார்ந்த பணிகள் (எ.கா., பட செயலாக்கம், சிக்கலான கணக்கீடுகள், குறியாக்கம்): உங்கள் குறியீடு உங்கள் இயந்திரத்தின் செயலாக்க சக்தியால் வரையறுக்கப்படுகிறது. ஒரு நல்ல தொடக்கப் புள்ளி, ஒரேநேர அளவை கிடைக்கக்கூடிய CPU கோர்களின் எண்ணிக்கைக்கு அமைப்பதாகும் (பிரவுசர்களில்
navigator.hardwareConcurrency, நோட்.ஜேஎஸ்-இல்os.cpus().length). அதை மிகவும் அதிகமாக அமைப்பது அதிகப்படியான சூழல் மாற்றத்திற்கு வழிவகுக்கும், இது உண்மையில் செயல்திறனைக் குறைக்கலாம்.
ஒரேநேர ஸ்ட்ரீம்களில் பிழை கையாளுதல்
நமது தற்போதைய செயல்படுத்தல் ஒரு "fail-fast" உத்தியைக் கொண்டுள்ளது. ஏதேனும் `mapperFn` ஒரு பிழையை வீசினால், முழு ஸ்ட்ரீமும் நிறுத்தப்படும். இது விரும்பத்தக்கதாக இருக்கலாம், ஆனால் பெரும்பாலும் நீங்கள் மற்ற உருப்படிகளைச் செயலாக்கத் தொடர விரும்புவீர்கள். நீங்கள் தோல்விகளைச் சேகரித்து அவற்றைத் தனியாக ஈல்டு செய்ய ஹெல்பரை மாற்றியமைக்கலாம், அல்லது வெறுமனே அவற்றைப் பதிவு செய்துவிட்டுத் தொடரலாம்.
ஒரு மேலும் வலுவான பதிப்பு இப்படி இருக்கலாம்:
// Modified part of the generator
const completed = await Promise.race(activePromises);
if (completed.error) {
console.error("An error occurred in a concurrent task:", completed.error);
// We don't throw, we just continue the loop to wait for the next promise.
// We could also yield the error for the consumer to handle.
// yield { error: completed.error };
} else {
yield completed.result;
}
பின்னழுத்த மேலாண்மை (Backpressure Management)
பின்னழுத்தம் என்பது ஸ்ட்ரீம் செயலாக்கத்தில் ஒரு முக்கியமான கருத்தாகும். ஒரு வேகமாக உற்பத்தி செய்யும் டேட்டா மூலம் ஒரு மெதுவான நுகர்வோரை மூழ்கடிக்கும்போது இது நிகழ்கிறது. நமது இழுத்தல்-அடிப்படையிலான இட்டரேட்டர் அணுகுமுறையின் அழகு என்னவென்றால், அது பின்னழுத்தத்தைத் தானாகவே கையாளுகிறது. நமது asyncMapConcurrent செயல்பாடு, activePromises குளத்தில் ஒரு இலவச இடம் இருக்கும்போது மட்டுமே sourceIterator-இலிருந்து ஒரு புதிய உருப்படியை இழுக்கும். நமது ஸ்ட்ரீமின் நுகர்வோர் ஈல்டு செய்யப்பட்ட முடிவுகளைச் செயலாக்க மெதுவாக இருந்தால், நமது ஜெனரேட்டர் இடைநிறுத்தப்படும், மேலும் மூலத்திலிருந்து இழுப்பதை நிறுத்தும். இது ஒரு பெரிய எண்ணிக்கையிலான செயலாக்கப்படாத உருப்படிகளை இடையகப்படுத்துவதன் மூலம் நினைவகம் தீர்ந்து போவதைத் தடுக்கிறது.
முடிவுகளின் வரிசை
ஒரேநேர செயலாக்கத்தின் ஒரு முக்கியமான விளைவு என்னவென்றால், முடிவுகள் மூல டேட்டாவின் அசல் வரிசையில் அல்லாமல், முடிவடையும் வரிசையில் ஈல்டு செய்யப்படுகின்றன. உங்கள் மூலப் பட்டியலில் உள்ள மூன்றாவது உருப்படி செயலாக்க மிகவும் வேகமாகவும், முதலாவது மிகவும் மெதுவாகவும் இருந்தால், நீங்கள் மூன்றாவது உருப்படிக்கான முடிவை முதலில் பெறுவீர்கள். அசல் வரிசையைப் பராமரிப்பது ஒரு தேவையாக இருந்தால், நீங்கள் இடையகப்படுத்தல் மற்றும் முடிவுகளை மீண்டும் வரிசைப்படுத்துதல் உள்ளிட்ட ஒரு சிக்கலான தீர்வை உருவாக்க வேண்டும், இது குறிப்பிடத்தக்க நினைவகச் சுமையைச் சேர்க்கிறது.
எதிர்காலம்: நேட்டிவ் செயல்படுத்தல்கள் மற்றும் சுற்றுச்சூழல் அமைப்பு
நமது சொந்த ஒரேநேர ஹெல்பரை உருவாக்குவது ஒரு அருமையான கற்றல் அனுபவமாக இருந்தாலும், ஜாவாஸ்கிரிப்ட் சுற்றுச்சூழல் அமைப்பு இந்த பணிகளுக்காக வலுவான, போர்க்களத்தில் சோதிக்கப்பட்ட நூலகங்களை வழங்குகிறது.
- p-map: நமது
asyncMapConcurrentசெய்வதை சரியாகச் செய்யும் ஒரு பிரபலமான மற்றும் இலகுரக நூலகம், ஆனால் அதிக அம்சங்கள் மற்றும் மேம்படுத்தல்களுடன். - RxJS: அப்சர்வேபிள்களுடன் கூடிய எதிர்வினை நிரலாக்கத்திற்கான ஒரு சக்திவாய்ந்த நூலகம், அவை சூப்பர்-பவர் ஸ்ட்ரீம்கள் போன்றவை. இது
mergeMapபோன்ற ஆபரேட்டர்களைக் கொண்டுள்ளது, அவற்றை ஒரேநேர செயலாக்கத்திற்காக உள்ளமைக்க முடியும். - Node.js Streams API: சேவையகப் பக்க பயன்பாடுகளுக்கு, நோட்.ஜேஎஸ் ஸ்ட்ரீம்கள் சக்திவாய்ந்த, பின்னழுத்தம்-அறிந்த பைப்லைன்களை வழங்குகின்றன, இருப்பினும் அவற்றின் API தேர்ச்சி பெறுவதற்கு மிகவும் சிக்கலானதாக இருக்கலாம்.
ஜாவாஸ்கிரிப்ட் மொழி வளர்ச்சியடையும்போது, ஒரு நாள் நாம் ஒரு நேட்டிவ் Iterator.prototype.mapConcurrent அல்லது அதுபோன்ற ஒரு யூட்டிலிட்டியைப் பார்க்க வாய்ப்புள்ளது. TC39 குழுவில் நடக்கும் விவாதங்கள், டெவலப்பர்களுக்கு டேட்டா ஸ்ட்ரீம்களைக் கையாள அதிக சக்திவாய்ந்த மற்றும் எளிமையான கருவிகளை வழங்கும் ஒரு தெளிவான போக்கைக் காட்டுகின்றன. இந்தக் கட்டுரையில் நாம் செய்தது போல, அடிப்படைக் கொள்கைகளைப் புரிந்துகொள்வது, இந்தக் கருவிகள் வரும்போது அவற்றை திறம்படப் பயன்படுத்த நீங்கள் தயாராக இருப்பதை உறுதி செய்யும்.
முடிவுரை
நாம் ஜாவாஸ்கிரிப்ட் இட்டரேட்டர்களின் அடிப்படைகளிலிருந்து ஒரு ஒரேநேர ஸ்ட்ரீம் செயலாக்க யூட்டிலிட்டிக்கான சிக்கலான கட்டமைப்பு வரை பயணம் செய்துள்ளோம். இந்தப் பயணம் நவீன ஜாவாஸ்கிரிப்ட் மேம்பாட்டைப் பற்றிய ஒரு சக்திவாய்ந்த உண்மையை வெளிப்படுத்துகிறது: செயல்திறன் என்பது ஒரு ஒற்றைச் செயல்பாட்டை மேம்படுத்துவது மட்டுமல்ல, திறமையான டேட்டா ஓட்டங்களை வடிவமைப்பதாகும்.
முக்கியமான கற்றல்கள்:
- நிலையான இட்டரேட்டர் ஹெல்பர்கள் ஒத்திசைவானவை மற்றும் வரிசையானவை.
- ஒத்திசைவற்ற இட்டரேட்டர்கள் மற்றும்
for await...ofடேட்டா ஸ்ட்ரீம்களைச் செயலாக்க ஒரு சுத்தமான தொடரியலை வழங்குகின்றன, ஆனால் இயல்பாக வரிசையாகவே இருக்கின்றன. - I/O-சார்ந்த பணிகளுக்கான உண்மையான செயல்திறன் ஆதாயங்கள் ஒரேநேர செயல்பாட்டிலிருந்து வருகின்றன—ஒரே நேரத்தில் பல உருப்படிகளைச் செயலாக்குவது.
Promise.raceஉடன் நிர்வகிக்கப்படும் ப்ராமிஸ்களின் ஒரு "வொர்க்கர் பூல்", ஒரேநேர மேப்பர்களை உருவாக்குவதற்கான ஒரு பயனுள்ள முறையாகும்.- இந்த முறை உள்ளார்ந்த பின்னழுத்த மேலாண்மையை வழங்குகிறது, நினைவகப் பெருக்கத்தைத் தடுக்கிறது.
- இணை செயலாக்கத்தைச் செயல்படுத்தும்போது எப்போதும் ஒரேநேர வரம்புகள், பிழை கையாளுதல், மற்றும் முடிவு வரிசைப்படுத்துதல் ஆகியவற்றைக் கவனத்தில் கொள்ளுங்கள்.
எளிய லூப்களைத் தாண்டி, இந்த மேம்பட்ட, ஒரேநேர ஸ்ட்ரீமிங் முறைகளை ஏற்றுக்கொள்வதன் மூலம், நீங்கள் செயல்திறன் மற்றும் அளவிடக்கூடிய தன்மை கொண்ட ஜாவாஸ்கிரிப்ட் பயன்பாடுகளை உருவாக்க முடியும், மேலும் கனமான டேட்டா செயலாக்கச் சவால்களை எதிர்கொள்ளும்போதும் மேலும் மீள்தன்மையுடன் இருக்க முடியும். டேட்டா தடைகளை அதிவேக பைப்லைன்களாக மாற்றுவதற்கான அறிவுடன் நீங்கள் இப்போது ஆயத்தமாக உள்ளீர்கள், இது இன்றைய டேட்டா-சார்ந்த உலகில் எந்தவொரு டெவலப்பருக்கும் ஒரு முக்கியமான திறமையாகும்.