원글은 Notion에서 작성되었습니다.
📌 원글 보러가기
해당 글은 본인의 개념 정리 용으로 작성하였으므로 불친절한 전달이라도 이해 부탁드립니다!
process는 Heap을 사용하며, thread는 process안에서 stack을 사용한다.
동시성과 병렬성의 개념을 명확히!
Concurrency ( 동시성 )
interleaving, 다수의 task들에 대해서 각각을 쪼개어서 조금씩 빠르게 실행하여
전체로 보았을때는 동시에 실행되고 있는 것처럼 보이도록 실행하는 것.
Parallelism ( 병렬성 )
parallelizing, 다수의 task들이 한번에 수행되는 것
두 개념 모두 Concurrency를 보장하기 위한 기술이다.
Thread - Thread의 효율성은 OS의 몫이다.
Coroutine - Coroutine의 효율성은 프로그래머의 몫이다.
출처 | Coroutine, Thread 와의 차이와 그 특징
js에서는 generator와 yield를 이용해서 coroutine을 수행한다.
yield - generator
function* call() {
console.log('first call');
yield 10;
console.log('second call');
yield 20;
console.log('third call');
yield 30;
}
let gen = call(); // iterator를 지정한다. standby상태
console.log(gen.next()); //첫번째 yield까지 수행한다.
console.log(gen.next());
console.log(gen.next()); // 아직 call함수의 return이 시행되지 않았기에 done은 false
console.log(gen.next()); // yield값은 없지만 함수는 return되었으므로, done은 true
first call
{ value: 10, done: false }
second call
{ value: 20, done: false }
third call
{ value: 30, done: false }
{ value: undefined, done: true }
yield* - generator
iterator가 iterator를 호출하는 경우에는 yield*사용해준다.
yield값이 generator인 경우에 **generator의 의미로서,
해당 generator의 yield를 수행한다.
function* func(){
yield 42;
}
function* option1(){
yield func(); // func의 iterator를 yield한다.
}
function* option2(){
yield func; // func함수 자체를 yield한다.
}
function* option3(){
yield* func(); // func함수의 iterator를 수행하여 func의 yield까지 수행한다.
}
console.log('option 1: ', option1().next());
console.log('option 2: ', option2().next());
console.log('option 3: ', option3().next());
option 1: { value: Object [Generator] {}, done: false }
option 2: { value: [GeneratorFunction: func1], done: false }
option 3: { value: 42, done: false }
yield - 취급값 없음
...
출처 | [JavaScript-16]Generator와 Yield
보통, huge process들을 처리하기 위해서 worker thread가 사용된다. worker thread를 이용해서 데이터가 이동하는 것은 결함을 야기할 수 있다.
Collaborative multitasking를 통해 main thread를 sharing한다. (coroutine 개념, thread쪼개기)
⇒ Coroutine에 우선 순위를 두는 것! huge process는 후순위가 됨
⇒ Context switching은 일어나지 않음! main Thread만 사용
*imperative; 어떻게 동작하는지에 관한 statements에 초점을 둔다. (C, C++, JAVA ...)
*declarative; 내부 동작 원리 보다는 논리 자체에 초점을 둔다. (HTML, CSS, ...)
css declarative 언어이기 때문에(loop나 이런거 못씀)
requestAnimationFrame을 사용해서 multiple frame을 사용할 수는 있지만, 점점 코드는 난해해져간다.
generator function을 이용해서 복잡한 애니메이션 논리가 구성되는 다음 frame을 기다려야할 때, 그 시간을 채울 수 있도록 할 수 있다!
js-coroutines를 사용하면, generator function을 이용해서 collaborative multitasking을 수행가능하다.
어쨌든 generator를 사용해서 iterator를 만드는 것은 coroutine을 이용하겠다는 의미!
js-coroutines의 controlling time check ( 특정 frame이 처리되기 위한 시간을 조절하는 법 )
js-coroutine은 어쨌든 generator.next()를 내부적으로 idle타임에서 수행해준다!
사용자가 할 일은, 그냥 load가 큰 함수를 넣어주고, yield할 위치만 정하면 됨!
coroutine은 main thread의 logical processing state machine이다.
👩💻 코드 분석
skeleton code
Promise와 requestIdleCallback이 main concept
export async function run(coroutine, loopWhileMsRemains=1, timeout){};
coroutine
generator | Iterator
coroutine으로서 수행될 task를 의미한다.
loopWhileMsRemains
current frame의 idle time이 해당 시간보다 적으면 next idle frame에서 coroutine이 수행됨
timeout
system이 Idle time에 있지 않다면, task를 수행할 시간.
let terminated = false; let resolver = null;
terminated
task의 종료 여부를 결정하는 변수
resolver
task의 반환 함수(resolve)를 담는 변수
const result = new Promise(function(resolve, reject){});
promise
promise를 통해 비동기로 수행되는 작업의 마지막 end point를 알 수 있게 해준다.
result.terminate = function(result) { terminated = true; if(resolver){ resolver(result) } }
수행중이던 coroutine을 멈추고, 수행 중이던 결과까지를 반환함!
coroutine in idle time 작동 원리 (Promise 함수 분석)
generator/yield와 resolve를 통해서 Idle Time coroutine을 실행함
resolver = resolve; const iterator = coroutine.next ? coroutine : coroutine();
iterator
coroutine의 Iterator가 생성되었는지 확인 후, 생성되지 않았으면 생성. 아니면(이미 coroutine은 iterator임.) 그대로 사용함.
request(run) // request request = typeof window === 'undefined' ? getNodeCallback() // requestIdleCallback 대체함수. 거의 사용 안함 : window.requestIdleCallback;
window.requestIdleCallback(callback)
전달받은 callback을 browser의 idle period에서 호출할 수 있도록 queue해놓는 method
background 작업 수행과, main event loop에서 priority에 맞게 작업을 수행할 수 있도록 함.
해당 함수가 호출되면, callback으로
IdleDeadline
object를 넘려준다.
IdleDeadline
IdleDeadline.timeRemaining
current idle period에서 남은 ms를 반환한다.
let id = 0; let parameter = undefined; // coroutine안에 coroutine(Promiese)의 경우, 그 값을 담는 변수 let running = false; // background에서 수행중인 task의 유무. true이면 pending async function run(api){ if(running) return; try{ running = true; // 현재 끝나지 않은 coroutine이 있음을 의미 clearTimeout(id) // 이전에 timeout 변수에 맞게 scheduling되어있던 setTimeout 제거 ~ 지금 수행할거니까! if(terminated){ // 외부 종료 명령에 따라서 coroutine을 수행하지 않음 iterator.return() return } let minTime = Math.max(minRemainingTime = 1.75, loopWhileMsRemains); try{ // 프로그램 상 남은 idle time이 설정치보다 커야 계속 작업 수행함. while(api.timeRemaining() > minTime) { const {value, done} = iterator.next(await parameter) parameter = undefined; // 보통은 다음 coroutine을 위한 parameter는 필요하지 않으므로 undefined로 정의 if(done){ // coroutine 작업이 다 끝났으므로 결과값을 반환하기! resolve(value); return; } if(value === true){ // yield done의 경우 coroutine을 중지하고, next idle에서 수행할 수 있도록 함 break; } else if (typeof value === 'number'){ // value만큼의 idleTime이 남았는지! minTime = +value; // value가 number인지 한번 더 확인 후, minTime을 value로 if(isNaN(minTime)) minTime = minRemainingTime } else if (value && value.then) { // value가 Promise인 경우! parameter = value; } } } catch (e) { console.log('error: ', e); reject(e); return; } // 끝날때까지 계속 수행 request(run); if(timeout){ id = setTimeout(runFromTimeout, timeout); } } finally { // coroutine의 작업이 모두 종료되었음을 의미함 running = false; } }
minTime
programmer가 지정한 최소 Idle time과 권장되는 최소 Idle Time중 더 큰 것으로 default를 가진다.
iterator.next(await parameter)
yield가 가지는 값이 또다른 coroutine(Promise)인 경우를 위해서 await parameter로 전달됨
react-native ios에서 js-coroutine의 run함수가 아예 실행이 안됨
그거는 window.requestIdleCallback
의 문제!!!
아래 글을 보고 requestIdleCallback을 바꿔줬더니 잘~돌아감
https://github.com/facebook/react-native/issues/28602
window.requestIdleCallback = function (cb) {
var start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
import React, {useRef, useState} from 'react';
import {useEffect} from 'react';
import {memo} from 'react';
import {Dimensions, Image, View, Animated} from 'react-native';
// import {
// append,
// forEach,
// map,
// reduce,
// run,
// singleton,
// update,
// yielding,
// } from 'js-coroutines';
import run from '../utils/run';
import Typography from '../components/atoms/Typography';
import {append, forEach, map, reduce, update, yielding} from 'js-coroutines';
const image = require('../assets/images/app_logo.png');
const width = 100;
function* coroutine1() {
console.log('in coroutine!');
let results;
results = new Array(2000000);
for (let i = 0; i < 2000000; i++) {
if ((i & 127) === 0) yield;
results[i] = (Math.random() * 10000) | 0;
}
}
async function createAsync() {
console.log('createAsync!');
await run(coroutine1);
}
const CoroutineTest: React.FC<{}> = ({}) => {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const [text, setText] = useState('first');
const {format} = new Intl.NumberFormat();
function animate() {
console.log('-animate-');
let multiplier = Dimensions.get('window').width / 300;
return update(function* () {
while (true) {
//Move left to right
//Move top to bottom
for (let y = 0; y < 200; y++) {
setY(y * multiplier);
yield;
}
for (let y = 200; y >= 0; y--) {
setY(y * multiplier);
yield;
}
}
});
}
async function calculateAsync() {
return await run(function* () {
let results;
//Create 2 million rows of random values
results = new Array(2000000);
for (let i = 0; i < 2000000; i++) {
if ((i & 127) === 0) {
yield;
}
results[i] = (Math.random() * 10000) | 0;
}
setText(`CO: Created ${format(results.length)} items`);
//Double all the values
yield* forEach(
results,
yielding((r, i) => (results[i] = r * 2)),
);
setText(`CO: Doubled the value of ${format(results.length)} items`);
//Get the square roots
const sqrRoot = yield* map(
results,
yielding((r) => Math.sqrt(r)),
);
setText(
`CO: Created a new array with the square roots of ${format(
sqrRoot.length,
)} items`,
);
//Sum all of the items
setText(
`CO: Sum of ${format(results.length)} items is ${format(
yield* reduce(
results,
yielding((c, a) => c + a, 64),
0,
),
)}`,
);
//Join the arrays
yield* append(results, sqrRoot);
setText(
`CO: Appended the square roots to the normal values making ${format(
results.length,
)} items in the array`,
);
// Sort the results
yield* sort(results, (a, b) => a - b);
setText(`CO: Sorted ${format(results.length)} items`);
return results;
});
}
useEffect(() => {
animate();
calculateAsync().then((r) => {
console.log('calculation done: ', r);
});
createAsync().then((v) => {
console.log('done!');
});
}, []);
return (
<View
style={{
width: '100%',
height: '100%',
alignItems: 'center',
paddingTop: 100,
borderWidth: 10,
}}>
<Animated.View
style={{
height: width * 0.8,
width: width * 0.8,
backgroundColor: 'green',
borderRadius: width * 0.8,
borderWidth: 10,
borderColor: 'blue',
transform: [
{
translateY: y,
},
],
}}
/>
<Typography>{text}</Typography>
</View>
);
};
export default memo(CoroutineTest);
function* sort(results: any, arg1: (a: any, b: any) => number) {
throw new Error('Function not implemented.');
}
JS에서 실현할 수 있는 coroutine(generator & yield)은 프로그래머의 선택에 따른다.
그 말인 즉, thread를 얼마나 사용하고 어떤 thread에서 어떤 task를 수행할지도 다 정해야한다!
또한, thread가 어떤 time period에서 사용되는지도 선택해야한다는 것!
js-coroutine은 로드가 큰 작업들은 완전히 background에서 수행될 수 있도록,
idle time을 내부적으로 체크해서 해당 period에서 수행되도록 기능을 제공한다.
또한, main Thread만 사용하므로 context switching도 발생하지 않는다.
아래는 JS-coroutine에서 핵심적으로 사용된 background Tasks API에 관한 포스트이다.
= cooperative Scheduling of Background Tasks API는 해당 작업을 수행할 free time이 존재하면 자동으로 수행될 수 있도록 queuing tasks를 제공한다.
JS-coroutine은 background라는 개념을 사용했고, requestIdleCallback을 통해서 해당 개념을 수행한다.
requestIdleCallback을 이용해서 system lag 없이 event loop를 수행할 수 있는 시간을 파악하고, 수행한다.
window.requestAnimationFrame()
을 사용하도록! ~ JS-coroutine의 update도 이 개념을 그대로 사용하고 있다.이 부분이 아래 RN에서 requestIdleCallback을 지원하지 않는 문제 해결 가능!!
window.requestIdleCallback = window.requestIdleCallback || function(handler) {
let startTime = Date.now();
return setTimeout(function() {
handler({
didTimeout: false,
timeRemaining: function() {
return Math.max(0, 50.0 - (Date.now() - startTime));
}
});
}, 1);
}
Background Tasks API - Example - code sample
페이지 소스 보기로 예시 코드 볼 수 있음!
Background Tasks API - Web APIs | MDN
출처 | requestIdleCallback 코드 설명
requestIdleCallback이 호출되면, 호출되는 순서대로 callback이 queue되어서 시스템 내부적으로 FIFO 순서에 맞게 각 callback이 idleTime에 맞게 알아서들 호출되지만,
개발자의 입장에서 시스템의 내부에 맡기는 것이 아니라 직접 task 수행 실행/완료를 control하는 것이 더 맞기 때문에, taskList
변수를 이용해서 queue를 명시적으로 수행해준다.
위의 예시를 조금만 뜯어서 살펴보면
requestIdleCallback과 queue개념을 이용해서 load가 큰 task를 background에서 효율적으로 사용했음을 알 수 있다.
수행중인 task가 끝나기 전에 새로운 task에 대한 사용자 요청이 생기더라도
task는 queue에 accumulate되기 때문에 FIFO 순서대로 실행된다.
사용방식에 따라서 효율적으로 background task를 수행할 수 있을 것 같다.
답을 하기전, 지금까지 소개했던 개념들의 차이를 정확하게 알아야할 것 같다.
task와 thread를 1:1로 대응하는 방식이 아닌, N:1로 대응하는 방식이다.
선택에 따라서 1개 이상의 thread를 사용할 수 있으며, thrad에서 수행될 task들도 지정할 수 있다.
JS에서는 coroutine을 generator와 yield를 통해 수행할 수 있다.
함수 자체는, mainThread의 idle time에서 task(callback)를 수행할 수 있도록 해준다.
callback으로 받은 task는 idle period에 실행될 수 있도록 시스템 자체에서 queue되며, 언젠가 앞에서 먼저 queue되어있던 task가 끝나면, 다음 task가 실행되는 구조를 갖고 있다.
위의 Background Tasks API 사용해보기
의 코드를 살펴보면,
queue 개념을 명시적으로 사용하므로써 pending되어있는 task들을 끝까지 처리해주는 것을 알 수 있다.
사용자의 요청에 따라 거대한 task를 계속해서 accumulating하고, 해당 task를 중단할 필요가 없이 순차적으로 수행하기만 하면 된다면 직접 queue logic을 설계하여 사용하는 것도 좋은 방법이다.
mainThread만을 사용해서 개별 task들을 background에서 처리한다.
coroutine과 requestIdleCallback을 통합한 개념이라고 볼 수있다.
JS-coroutine은 task를 handling할 수 있는 옵션을 제공한다.
yield | yield n | yield true | yield generator | yield Promise 와 같은 옵션을 통해서
하나의 huge task를 쪼개어서(iterator) requestIdleTime의 callback에 넣어서 task 수행을 진행한다.
하나의 거대한 task를 쪼개야한다면, coroutine + requestIdleCallback 조합을 사용하는 건 좋은 방법이다.
up to you