TigerCow.Door



안녕하세요. 문범우입니다.

지난 포스팅에서 프로세스 및 스레드에 대한 개념을 정리해보았습니다.

이번에는 스레드를 직접적으로 만들어서 확인해보도록 하겠습니다.

실습은 C언어로 진행합니다.


1. 싱글 스레드(Single-Thread)

먼저 싱글 스레드를 만들어서 확인해보도록 하죠.

코드는 아래와 같습니다.

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
33
34
35
36
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
 
DWORD WINAPI ThreadFunc(LPVOID);
// DWORD는 더블워드 = long
// WINAPI : Windows의 API
// LPVOID : ms에서 사용하는 void* 
 
int global = 1;
 
int main()
{
    HANDLE hThrd; //스레드를 접근하기 위한 손잡이 개념
    DWORD threadId; 
    int i;
    // for 루프문
    for (i=0; i<5; i++)
    {
        ThreadFunc( (LPVOID) i); // single thread
    }
    // Wait for the threads to complete.
    Sleep(3000);
 
    return EXIT_SUCCESS;
}
 
DWORD WINAPI ThreadFunc(LPVOID n)
{
    int i;
    for (i=0;i<100;i++){
        printf("%d%d%d%d%d%d%d%d global = %d\n",n,n,n,n,n,n,n,n,global++);
    }
    return 0;
}
cs


11번 라인은 추후 멀티 스레드에서 확인하기 위한 변수입니다.

21번 라인을 보시면 ThreadFunc 함수를 호출 하도록 되어 있습니다.

그리고 해당 함수에 대해서는 코드 하단에 나와 있습니다.

24번 라인은 결과를 끝까지 확인하기 위해 일시적으로 추가한 코드입니다.


해당 코드에서는 스레드를 추가적으로 만들지 않습니다.

따라서 프로세스에는 Primary thread 하나만 존재함으로써 단일 스레드를 유지합니다.

코드에 대한 결과는 어떻게 될까요?

싱글스레드 실행결과 중간


싱글스레드 실행결과 끝


위의 실행결과를 확인하면 코드에서 for문에 의해서 ThreadFunc 함수가 5번 호출되는 것을 알 수 있습니다.

처음에 ThreadFunc함수가 호출되고 100번 반복될때 까지 다음 코드를 실행하지 않고 함수가 리턴될때 까지 기다립니다.

이것은 출력되는 결과를 보면 알 수 있죠.

함수가 for문의 i를 인자로 받아서 이를 출력하기 때문에 몇번째로 호출된 함수인지를 확인할 수 있습니다.


2. 멀티 스레드(Multi-Thread)

그럼 해당 코드를 약간 바꿔서 단일스레드가 아닌 멀티스레드로 확인해보겠습니다.


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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
 
DWORD WINAPI ThreadFunc(LPVOID);
// DWORD는 더블워드 = long
// WINAPI : Windows의 API
// LPVOID : ms에서 사용하는 void* 
 
int global = 1;
 
int main()
{
    HANDLE hThrd; //스레드를 접근하기 위한 손잡이 개념
    DWORD threadId; 
    int i;
    // for 루프문
    for (i=0; i<5; i++)
    {
        //ThreadFunc( (LPVOID) i); // single thread 
        //CreateThread함수를 5번 호출
        hThrd = CreateThread(NULL,
            0
            ThreadFunc, //함수포인터: 함수가 시작하는 메모리 주소
                        //아래에서 함수 확인!
            (LPVOID)i, //
            0,
            &threadId ); //
        if (hThrd)
        {
            printf("Thread launched %d\n", i);
            CloseHandle(hThrd);
        }
    }
    // Wait for the threads to complete.
    Sleep(1000);
 
    return EXIT_SUCCESS;
}
 
DWORD WINAPI ThreadFunc(LPVOID n)
{
    int i;
    for (i=0;i<100;i++){
        printf("%d%d%d%d%d%d%d%d global = %d\n",n,n,n,n,n,n,n,n,global++);
    }
    return 0;
}
 
cs


해당 코드를 보면 단순히 ThreadFunc 함수를 호출하는 것이 아니고,

CreateThread 함수를 통해 스레드를 만들고 해당 스레드가 ThreadFunc 함수를 호출하게 합니다.

그럼 그 결과가 어떻게 바뀔까요?


멀티스레드 실행결과 중간


멀티스레드 실행결과 끝


좀 전에 확인한 싱글스레드와의 차이가 보이시나요?

각각의 스레드가 각자 실행되면서 global 변수의 값을 증가시키고 있습니다.


3. 싱글 스레드와 멀티 스레드 시간 차이

이러한 차이가 어떤 결과를 보여줄까요?

각각의 코드에 시간측정함수를 더해 확인해보도록 하겠습니다.

먼저 싱글 스레드의 코드입니다.


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
33
34
35
36
37
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define MAXTHREAD 5
 
DWORD WINAPI ThreadFunc(LPVOID n);
int main () {
    clock_t start, finish;
    double duration;
    DWORD threadId; 
    HANDLE hThrd[MAXTHREAD];
 
    int i;
    start = clock();//현재 시간 반환
 
    for (i=0; i<MAXTHREAD; i++){
        ThreadFunc((LPVOID)i);
    }
    WaitForMultipleObjects(MAXTHREAD, hThrd, TRUE, INFINITE);
    for (i=0; i<MAXTHREAD; i++){
        CloseHandle(hThrd[i]);
    }
    //종료
    finish = clock();
    duration = (double) (finish-start) / CLOCKS_PER_SEC;
    printf("%f 초입니다\n", duration);
    return 0;
}
DWORD WINAPI ThreadFunc(LPVOID n)
{
    for (int i = 0; i<1000000000; i++){
        3+5+8*24;
    }
    return 0;
}
cs


위의 코드를 보면 18번 for문을 통해 ThreadFunc 함수가 총 5번 실행됩니다.

ThreadFunc에서는 단일 연산을 1억번 수행하게됩니다.

그러므로, 해당 싱글스레드에서는 단일 연산을 총 5억번 수행합니다.

21번 라인은 멀티스레드가 종료할때까지 기다려라 라는 함수인데 이에 대한 것은 추후에 설명드리도록 하겠습니다.

해당 코드에 대한 결과는 아래와 같습니다.



대략적으로 12초가 걸렸습니다.

그럼 멀티스레드에서는 어떨까요? 먼저 코드를 확인해보겠습니다.


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
33
34
35
36
37
38
39
40
41
42
43
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define MAXTHREAD 5
 
DWORD WINAPI ThreadFunc(LPVOID n);
int main () {
    clock_t start, finish;
    double duration;
    DWORD threadId; 
    HANDLE hThrd[MAXTHREAD];
 
    int i;
    start = clock();//현재 시간 반환
 
    for (i=0; i<MAXTHREAD; i++){
        hThrd[i] = CreateThread(NULL
            0,        
            ThreadFunc, 
            (LPVOID)i, 
            0,            
            &threadId    
            ); 
    }
    WaitForMultipleObjects(MAXTHREAD, hThrd, TRUE, INFINITE);
    for (i=0; i<MAXTHREAD; i++){
        CloseHandle(hThrd[i]);
    }
    //종료
    finish = clock();
    duration = (double) (finish-start) / CLOCKS_PER_SEC;
    printf("%f 초입니다\n", duration);
    return 0;
}
DWORD WINAPI ThreadFunc(LPVOID n)
{
    for (int i = 0; i<1000000000; i++){
        3+5+8*24;
    }
    return 0;
}
cs


19번~25번 라인을 통해 각각 스레드를 만듭니다.

각각의 스레드가 1억번 단일연산을 수행하므로 위와 동일하게 총 5억번의 연산을 수행합니다.

이러한 멀티스레딩에서의 시간 결과는 어떻게 나올까요?



싱글스레드에서는 약 12초가 소요되었던 5억번의 단일연산이 멀티스레드에서는 3.6초로 줄어들었습니다.

물론 개인 컴퓨터 사양에 따라 시간은 변동될 수 있지만, 그런 점들을 감안해도

싱글스레드와 멀티스레드에 대한 시간효율 차이는 너무나 명백하게 확인할 수 있습니다.




블로그 이미지

Tigercow.Door

Web Programming / Back-end / Database / AI / Algorithm / DeepLearning / etc


안녕하세요. 문범우입니다.

이번 포스팅에서는 스레드(Thread) 와 프로세스(Process) 에 대해서 알아보겠습니다.

스레드에 대해 좀 더 명확히 이해하기 위해서는 먼저 프로세스에 대한 개념이 필요합니다.

따라서 프로세스, 스레드, 멀티 프로세싱, 멀티 프로그래밍, 멀티 태스킹, 멀티스레딩, 프로세스와 스레드 차이 순으로 알아보도록 하겠습니다.



1. 프로세스(Process)

프로세스에 대한 설명을 먼저 위키백과에서 어떻게 하고 있는지 확인해볼게요.


프로세스(process)는 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램을 말한다. 종종 스케줄링의 대상이 되는 작업(task)이라는 용어와 거의 같은 의미로 쓰인다. 여러 개의 프로세서를 사용하는 것을 멀티프로세싱이라고 하며 같은 시간에 여러개의 프로그램을 띄우는 시분할 방식을 멀티태스킹이라고 한다. 프로세스 관리는 운영 체제의 중요한 부분이 되었다.

(출처: 위키백과)


위의 설명처럼 프로세스란, 실행되고 있는, 실행중인 프로그램을 말합니다.

프로그램은 실행이 되기 전의 명령어와 데이터의 묶음인데, 이러한 정적인 요소인 프로그램이 실행 중에 있을때 그것들을 우리는 프로세스라고 말합니다.

다시 말해서, 하드디스크에 저장되어 있는 명령어와 데이터의 묶음 자체는 프로그램이며

프로세스는 그러한 프로그램을 구동하여(실행하여), 프로그램 자체와 프로그램의 상태가 메모리 상에서 실행되는 작업단위를 말합니다.

예를 들어서, 컴퓨터에 A라는 프로그램이 존재합니다. A라는 것을 실행시키기 전에는 단순히 프로그램으로써 존재합니다. 헌데 A를 실행시켜서, 현재 실행되고 있는 A를 프로세스라고 합니다.


일반적으로 CPU는 한번에 하나의 프로세스만 관리할 수 있습니다.

그런데 우리는 한번에 여러개의 프로그램을 실행하고 있죠? 즉, 우리는 한번에 여러개의 프로세스를 사용하고 있습니다.

CPU는 하나의 프로세스만 관리가 가능한데, 이점을 극복하고 우리가 사용하듯이 여러개의 프로세스가 동시에 실행되도록 하는 것이 멀티태스킹입니다.

이에 대한 자세한 설명은 밑에서 드리겠지만 간략히 말씀드리면, 멀티태스킹은 엄밀히 말해 한번에 다수의 프로세스를 실행하고 있는 것이 아니고 사용자로 하여금 다수의 프로세스가 동시에 실행되는 것 '처럼' 보이게 만드는 것입니다.

즉, CPU가 빠른 시간동안 각각의 프로세스를 순차적으로 실행하는 것 입니다.

예를 들어 A 프로세스를 몇 초간, 그리고 B 프로세스를 몇 초간, 다시 A 프로세스, 다시 B 프로세스, ... 이렇게 반복해가며 다수의 프로세스를 관리하기 때문에 사용자는 한번에 다수의 프로세스가 실행되고 있는 것 처럼 보이는 것입니다.



1-1. 프로세스 제어 블록(PCB: Process Control Block)

CPU는 한번에 하나의 프로세스만 실행이 가능합니다.

운영체제가 CPU에게 실행중인 프로세스 A에서 넘어가 B라는 프로세스를 관리하라고 할때 실행중이던 프로세스 A의 정보를 PCB, 프로세스 제어 블록에 저장합니다. 그리고 이후 A가 CPU 제어권을 다시 받게되면 PCB에 있던 정보를 불러와서 프로세스가 실행됩니다.

PCB에는 주로 다음과 같은 정보가 저장되어 있습니다.


프로세스 식별자(Process ID): 각각의 프로세스를 식별하기 위한 고유 ID입니다.

프로세스의 상태(State): 생성, 실행, 준비, 대기, 종료 상태로 나누어져 있으며 각각에 대해서는 밑에서 설명드리겠습니다.

프로그램 계수기(Program Counter): 해당 프로세스가 다음에 실행할 명령어의 주소를 가지고 있습니다.

프로세스의 우선순위 (Priority): 해당 프로세스의 우선순위 정보를 표현합니다.

을 비롯한 메모리 관리 정보, 프로세스 계정 정보 등 다양한 정보를 가집니다.


PCB는 프로세스의 중요한 정보를 포함하고 있습니다. 때문에 일반적인 사용자가 접근하지 못하도록 되어 있습니다.



1-2. 프로세스의 상태 (Process state)

일반적으로 프로세스의 상태는 커널에 의해 관리됩니다.

프로세스의 상태가 PCB에 저장되어 있는데 위에서 말한 것과 같이 PCB는 일반적인 사용자가 접근하지 못하는 영역에 저장되어 있습니다. 이때 커널 스택 영역은 편리하면서도 보호받는 위치이기 때문에 주로 이용됩니다.


프로세스의 상태로는 생성(create), 실행(running), 준비(ready), 대기(waiting), 종료(terminated) 상태가 있습니다.

그림과 설명을 통해 프로세스의 상태에 대해 좀 더 알아볼게요.

그림에서 다섯개의 파란색 동그라미가 각각 프로세스의 상태입니다. (new는 생성, create와 같은 의미입니다.)

생성(create or new) 상태란, 프로세스 자체는 생성되었지만 아직 프로그램이 메모리에 적재되지 않은 상태를 말합니다.

준비(ready) 상태란, 실행된 프로세스가 메모리에 적재되고 CPU를 할당 받기 위해서 준비중인 상태입니다.

실행(running) 상태란, 프로세스가 CPU를 할당받아서 명령어를 실행하고 있는 상태입니다.

대기(waiting or Block) 상태란, 실행상태에 있던 프로세스가 급작스런 이벤트에 의해 실행을 일시적으로 멈춘 상태를 말합니다.

종료(terminated) 상태란, 프로세스의 실행이 모두 정상적으로 끝나서 종료된 상태를 말합니다.


위의 그림에서 볼 수 있듯이 각각의 상태는 특정 이벤트에 의해 변화됩니다.

우리는 이러한 것을 프로세스의 상태 전이라고 말합니다.

각각의 프로세스의 상태 전이에 대한 설명은 아래와 같습니다.


admitted: 생성된 프로세스가 승인을 받아서 실행이 됩니다.

scheduler dispatch: 보통 dispatch라고도 말하며, 준비상태에 있던 프로세스가 CPU를 할당받아서 실행되는 것입니다.

interrupt: 실행 중이던 프로세스가 특정 이벤트에 의해서 다시 준비상태로 넘어가는 것입니다. 프로세스가 할당된 시간에 모든 명령을 실행하지 못하여 넘어가는 Time out 등이 있습니다.

I/O or event wait: CPU가 프로세스를 실행 중에 있을 때 급작스런 이벤트에 의해서 CPU가 다른 프로세스를 할당하게 될때 현재 실행중인 프로세스는 대기상태로 넘어가는데 이러한 상태전이를 말합니다.

I/O or event completion: 급작스런 이벤트에 의한 CPU할당을 받는 프로세스의 실행이 끝나면 대기상태에 있던 프로세스는 다시 준비상태로 넘어가게 됩니다. 이러한 상태 전이를 말합니다.

exit: 프로세스의 모든 명령이 실행되고 종료되는 상황을 말합니다.



2. 스레드(Thread)

스레드가 무엇일까요?

위키백과에는 아래와 같은 설명으로 나와있습니다.


스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이러한 실행 방식을 멀티스레드(multithread)라고한다. (출처: 위키백과)


위에서 우리는 프로세스에 대해서 알아보았습니다.

실행 중인 프로그램을 프로세스라고 했는데, 스레드는 그러한 프로세스 내부에서 실행되는 흐름의 단위 입니다.

즉 프로세스의 내부에 있는 개체를 말합니다. 하나의 프로세스에는 최소 하나이상의 스레드가 존재합니다.

하나의 스레드가 있을 때는 단일 스레드라고 말하며 두개 이상의 스레드가 존재하면 멀티스레드, 다중스레드라고 말합니다.

이에 대해서는 밑에서 다시 설명드리겠습니다.


스레드는 왜 사용될까요?

프로세스에 대한 설명을 드리면서 멀티태스킹에 대해 간략히 설명을 드렸습니다. 멀티태스킹에서는 다수의 프로세스가 만들어져서 순차적으로 실행됩니다.

헌데 만약 A라는 프로세스와 B라는 프로세스가 대규모의 동일한 데이터를 필요로 한다면 어떨까요?

이러한 프로세스들은 단순히 동일한 데이터를 공유하기 위해서 운영체제에서의 복잡한 메커니즘을 사용하게 됩니다.

여기서 프로그래머들은 생각합니다. 프로세스가 생성되는 초기부터 데이터를 공유하도록 해준다면 어떨까? 이러한 발상이 스레드의 시작이 됩니다.

그리고 멀티스레드를 통해 메모리를 효율적으로 사용할 수 있게 됩니다.


때문에 다수의 프로세스가 독립적으로 실행되며 별개의 메모리를 차지하는 것보다 효율적으로 메모리를 사용하게 됩니다.

또한 프로세스간의 전환 속도보다 스레드간의 전환 속도가 빠르게 됩니다.

하지만 스레드를 사용하면 동일한 데이터를 전역 변수로 이용하므로 다양한 문제가 발생될 수 있다는 단점이 있습니다.


3. 멀티 ~ (Multi ~ )

멀티프로세싱, 멀티프로그래밍, 멀티테스킹, 멀티스레드

이러한 개념들은 각각 무엇을 말하는 걸까요? 하나씩 간단히 나마 알아보도록 하겠습니다.


3-1. 멀티 프로세싱(Multi-processing)

멀티 프로세싱은 한마디로 말해서 '두개 이상, 다수의 프로세서가 협력적으로 작업을 동시에 처리하는 것' 입니다.

위에서 설명한 '프로세스'가 아닌 '프로세서'를 말하는 것에 혼동되면 안됩니다.

프로세서는 대략적으로 CPU라고 생각하시면 됩니다.

아래 사진을 보면 더 이해하기 쉬울 것 입니다.



각각의 프로세서가 하나의 작업만을 처리하는 것이 아니라 다수의 작업을 처리하며,

하나의 작업은 하나의 프로세서에 의해 처리되는 것이 아니라 다수의 프로세서에 의해 처리됩니다.

멀티 프로세싱을 하는 장점으로는 여러가지가 있습니다.

여러개의 프로세스가 처리되어야 할때 동일한 데이터를 사용한다면 이러한 데이터를 하나의 디스크에 두고 모든 프로세서가 이를 공유하도록 한다면 비용적으로 저렴합니다.

또한, 만약 하나의 프로세서가 하나의 작업만을 처리한다면 특정 프로세서가 고장이 났을 때 해당 작업은 정지됩니다. 하지만 멀티 프로세싱을 사용한다면 작업은 정지되지 않습니다. 단지 속도가 느려지는 정도의 손해만 발생하겠죠.


3-2. 멀티 프로그래밍(Multi-programming)

멀티 프로그래밍의 개념은 위에서 잠깐 언급한 멀티테스킹과 혼동될 수 있으니 잘 구분하길 바랍니다.

멀티 프로그래밍이란, 특정 프로세스 A에 대해서 프로세서가 작업을 처리할때 낭비되는 시간동안 다른 프로세스를 처리하도록 하는 것 입니다.

예를 들어 A라는 프로세스를 처리중에 있을때 입출력 이벤트가 발생했는데 프로세서가 입출력 이벤트에 대한 응답을 위해 무작정 대기하고 있다면 프로세서의 자원을 낭비하는 결과를 초래합니다. 프로세서, CPU는 한번에 하나의 프로세스만 처리하도록 되어있기 때문에 A 프로세스에 대한 입출력 이벤트에 대한 응답을 대기하는 동안 아무일도 하지 않기 때문이죠.

멀티 프로그래밍은 이렇게 낭비되는 시간동안 프로세서가 다른 프로세스를 수행할 수 있도록 하는 것 입니다.


3-3. 멀티 테스킹(Multi-tasking)

멀티 테스킹이란 다수의 Task(프로세스보다 보다 확장된 개념이라고 생각하시면 됩니다.)를 운영체제의 스케줄링에 의해 번갈아 가면서 수행하는 것 입니다.

프로세서가 각각의 Task를 조금씩 자주 번갈아가면서 처리하기 때문에 사용자는 마치 동시에 여러 Task가 수행되는 것처럼 보게 됩니다.


위에서 말한 멀티프로그래밍과의 차이점으로는,

멀티프로그래밍은 프로세서의 자원이 낭비되는 것을 최소화하기 위한 것이며

멀티테스킹은 일정하게 정해진 시간동안 번갈아가면서 각각의 Task를 처리하는 것입니다.


3-4. 멀티 스레딩(Multi-threading)

위에서 프로세스와 스레드의 개념을 잘 이해하셨다면 멀티스레딩 또한 쉽게 이해하실 수 있습니다.

하나의 프로세스내에는 여러개의 스레드가 존재하게 됩니다.

또한 이런 다수의 스레드는 하나의 데이터 자원을 공유 합니다. 때문에 메모리에 대한 효율성을 가질 수 있습니다.


멀티 스레딩과 멀티 프로세싱의 차이에 대해서도 혼동될 수 있습니다.

멀티 스레딩은 하나의 프로그램 안에서 병렬 처리의 이점을 보는 것이며

멀티 프로세싱은 여러 개의 프로그램들을 병렬로 처리할 수 있는 것 입니다.

위키백과에 나와있는 것에 의하면,

멀리 프로세싱 시스템이 여러 개의 완전한 처리 장치들을 포함하는 반면

멀티 스레딩은 스레드 수준뿐 아니라 명령어 수준의 병렬 처리에까지 신경을 쓰면서 하나의 코어에 대한 이용성을 증가하는 것에 초점을 두고 있습니다.

멀티 스레딩의 장점으로는 자원을 공유하여 메모리에 대한 효율성을 가져올 수 있는 것과 이로 인해 경제성 또한 증가하는 것 등이 있습니다.



4. 프로세스와 스레드의 차이점

위에서 프로세스와 스레드에 대해서 이해하셨다면 그 차이점에 대해서도 어느정도 느낌이 오셨을 겁니다.

프로세스가 실행될때 운영체제로 부터 어떤 것들을 할당받을까요?

프로세스는 자신을 실행할 프로세서를 할당받으며 필요한 메모리공간과 데이터등을 할당받습니다.

그리고 스레드란 이러한 프로세스 안에서 실행되는 흐름의 단위로써, 프로세스 안에서의 주소나 데이터를 스레드 간에 공유하면서 실행됩니다.

스레드가 존재함으로써 데이터와 같은 자원을 메모리에 할당하는 동작이 줄어들어서 자원을 효율적으로 관리하고 운영할 수 있습니다.

해당 내용에 대해 아래 링크를 참고하시면 보다 쉽게 이해가 가능합니다.

https://brunch.co.kr/@kd4/3


추가적으로 궁금한 점이나 내용에 대한 피드백은

댓글 및 이메일(doorbw@outlook.com)을 이용해주세요 :)

블로그 이미지

Tigercow.Door

Web Programming / Back-end / Database / AI / Algorithm / DeepLearning / etc