반응형

 

사진출처: IPC


IPC가 뭘까?

예전에 프로세스에 관해 포스팅을 했었을 때, 각 프로세스는 독립적인 주소공간을 가진다고 했었습니다. 즉 프로세스끼리는 서로의 메모리를 공유할 수 없다는 뜻인데요. 진짜일까요?

당연하게도 프로세스는 서로 데이터를 공유할 수 있는 방법이 있습니다. 그걸 바로 IPC통신이라고 하는데요.

커널은 IPC자원(커널 객체)와 IPC메서드 등을 관리 및 제공하며, 개발자는 운영 체제가 제공하는 IPC자원과 API, 인터페이스를 사용하여 프로세스간 통신을 구현할 수 있습니다.


IPC의 종류

IPC통신에도 많은 종류가 있으며, 상황에 따라 맞는 방법을 선택해 사용하는 것이 중요합니다.


익명 파이프(Anonymous Pipe)

특징

  • 부모-자식 프로세스 간의 단방향 통신을 위해 사용합니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 부모-자식 프로세스 간의 파이프 연결이 다른 IPC설계보다 쉽습니다.

단점

  • 기본적으로 단방향 통신이므로 프로세스가 읽기, 쓰기를 둘다 하려면 두 개의 파이프를 만들어야 합니다.
  • 각 프로세스가 부모-자식 관계가 아니면 사용이 불가능합니다.

사용법

더보기

CreatePipe 함수(namedpipeapi.h) - Win32 apps | Microsoft Learn

 

CreatePipe 함수(namedpipeapi.h) - Win32 apps

익명 파이프를 만들고 파이프의 읽기 및 쓰기 끝에 핸들을 반환합니다.

learn.microsoft.com

익명 파이프 생성 예시

#include <iostream>
#include <Windows.h>

int main()
{
    HANDLE hRead, hWrite;

    // 이 예제는 부모-자식 프로세스에 대한 구분이 없으므로 실제로 사용할 땐, CreateProcess로 부모 프로세스가 자식 프로세스를 생성해야한다.
    // 익명 파이프 생성
    if (CreatePipe(&hRead, &hWrite, NULL, 0))
    {
        std::cout << "익명 파이프 생성 성공" << '\n';
    }
    else
    {
        std::cerr << "익명 파이프 생성 실패" << '\n';
        return 1;
    }

    // 쓰기 쓰레드: 데이터를 파이프에 쓰기
    const char* data = "Hello, Anonymous Pipe!";
    DWORD bytesWritten;

    if (WriteFile(hWrite, data, strlen(data) + 1, &bytesWritten, NULL))
    {
        std::cout << "파이프에 데이터 쓰기 성공: " << bytesWritten << " 바이트" << '\n';
    }
    else
    {
        std::cerr << "파이프 쓰기 실패" << '\n';
    }

    // 읽기 쓰레드: 데이터를 파이프에서 읽기
    char buffer[128] = { 0 };
    DWORD bytesRead;

    if (ReadFile(hRead, buffer, sizeof(buffer) - 1, &bytesRead, NULL))
    {
        std::cout << "파이프에서 데이터 읽기 성공: " << buffer << '\n';
    }
    else
    {
        std::cerr << "파이프 읽기 실패" << '\n';
    }

    // 커널 객체 반납
    CloseHandle(hRead);
    CloseHandle(hWrite);

    return 0;
}

이름 있는 파이프(Named Pipe)

특징

  • 부모-자식 프로세스가 아닌 프로세스와의 단방향 통신을위해 사용합니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.
  • 이름을 통해 특정 파이프를 식별합니다.
  • 여러 프로세스가 동일한 파이프에 접근 가능합니다.
  • 기본적으로 단방향 통신이지만, 운영체제에 따라 양방향이 가능합니다.

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 부모-자식 프로세스 관계에 제한되지 않습니다.

단점

  • 기본적으로 단방향 통신이므로 프로세스가 읽기, 쓰기를 둘다 하려면 두 개의 파이프를 만들어야 합니다.
  • 파이프의 설정과 관리가 익명 파이프보다 복잡합니다.

사용법

더보기

CreateNamedPipeA 함수(winbase.h) - Win32 apps | Microsoft Learn

 

CreateNamedPipeA 함수(winbase.h) - Win32 apps

ANSI(CreateNamedPipeA) 함수(winbase.h)는 명명된 파이프의 인스턴스를 만들고 후속 파이프 작업에 대한 핸들을 반환합니다.

learn.microsoft.com

네임드 파이프 생성 예시 (읽기, 쓰기는 없음)

#include "pch.h"
#include <iostream>

int main()
{
	return 0;
	HANDLE hPipe = CreateNamedPipe(
		L"\\.\pipe\MyPipe",			 // 파이프 이름(\\.\pipe\의 뒤에 이름)
		PIPE_ACCESS_DUPLEX,          // 양방향 통신
		PIPE_TYPE_BYTE | PIPE_WAIT,  // 바이트 스트림과 동기식 통신
		1,                           // 인스턴스 개수
		512, 512,                    // 출력/입력 버퍼 크기
		0,                           // 기본 타임아웃
		NULL                         // 보안 속성
	);
	CloseHandle(hPipe);
	return 0;
}

메시지 큐 (Message Queue)

특징

  • 커널이 관리하는 메시지 큐를 통해 데이터를 주고받습니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.
  • 송신과 수신이 독립적으로 동작하는 비동기성으로 동작합니다.
  • 운영체제가 메시지 큐의 생성 및 관리를 담당합니다. 

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 비동기적 데이터 통신방식이므로 송신자와 수신자가 동시에 동작할 필요 없음.

단점

  • 운영체제마다 API가 다르므로 주의해야 합니다.
  • 메시지 타입과 큐 ID등의 메세지를 명확히 전송해야 합니다.

사용법


공유 메모리 (Shared Memory)

특징

  • 한 메모리 영역을 여러 프로세스가 공유하여 사용하는 방식입니다.
  • 이는 서로의 메모리 주소공간을 공유한다는 것이 아니라, OS가 공유가능한 메모리 공간을 할당하여 여러 프로세스에게 접근 가능하게 해줍니다.

장점

  • 프로세스가 데이터를 메모리에 복사하는 것이 아닌, 직접 쓰고 읽으므로 속도가 빠릅니다.
  • 여러 프로세스가 동일한 데이터를 읽기, 쓰기가 가능합니다.
  • 다른 IPC에 비해 비교적 큰 데이터를 처리하기가 간편합니다.

단점

  • 데이터의 동기화 문제에 주의해야 합니다.
  • 메모리에 직접 접근하므로 보안에 취약할 수 있습니다.
  • 할당한 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

사용법

더보기

명명된 공유 메모리 만들기 - Win32 apps | Microsoft Learn

 

명명된 공유 메모리 만들기 - Win32 apps

데이터를 공유하기 위해 여러 프로세스는 시스템 페이징 파일이 저장하는 메모리 매핑 파일을 사용할 수 있습니다.

learn.microsoft.com

쓰기

#include <windows.h>
#include <iostream>

// 쓰는 프로세스
int main() 
{
    // 공유 메모리 생성
    HANDLE hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,          // 물리적 파일이 아닌 메모리 사용
        nullptr,                       // 기본 보안 속성
        PAGE_READWRITE,                // 읽기/쓰기 가능
        0,                             // 고위 메모리 크기 (사용 안 함)
        256,                           // 공유 메모리 크기 (256 바이트)
        L"SharedMemoryExample");       // 공유 메모리 이름

    // 공유 메모리 매핑
    LPVOID pBuf = MapViewOfFile(
        hMapFile,                      // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        0, 0, 0);                      // 전체 영역 매핑

    // 데이터 쓰기
    const char* message = "Hello!";
    CopyMemory(pBuf, message, strlen(message) + 1);
    Sleep(5000);
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);

    return 0;
}

읽기

#include <windows.h>
#include <iostream>

// 읽는 프로세스
int main() 
{
    Sleep(1000);
    // 공유 메모리 열기
    HANDLE hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        FALSE,                         // 자식 프로세스 상속 여부 (아니오)
        L"SharedMemoryExample");       // 공유 메모리 이름 (생성 시와 동일해야 함)

    // 공유 메모리 매핑
    LPVOID pBuf = MapViewOfFile(
        hMapFile,                      // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        0, 0, 0);

    // 데이터 읽기
    std::cout << "공유 메모리에서 데이터를 읽었습니다: " << static_cast<char*>(pBuf) << std::endl;

    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
}

소켓 (Socket)

해당 사진은 TCP에 대한 예시사진

특징

  • 네트워크 인터페이스를 통해 두 프로세스가 데이터를 주고받는 방식입니다.
  • 데이터를 양방향으로 주고받을 수 있습니다.
  • TCP(연결 지향)방식과 UDP(비연결 지향)방식이 있습니다.

장점

  • 같은 시스템 내에서의 통신 뿐만이 아니라, 네트워크를 통한 다른 컴퓨터와의 통신이 가능합니다.
  • 대상이 서로 다른 운영체제여도 통신이 가능합니다.
  • 실시간으로 데이터 전송이 가능합니다.

단점

  • 소켓 통신은 패킷의 처리, 예외 처리 등에 대한 구현이 필요하므로 복잡합니다.
  • 네트워크 자원을 사용하기 때문에 연결 수가 많아질수록 리소스 사용이 급격히 많아집니다.
  • 데이터를 암호화하지 않으면 보안에 취약할 수 있습니다.

사용법

더보기

socket 함수(winsock2.h) - Win32 apps | Microsoft Learn

 

socket 함수(winsock2.h) - Win32 apps

소켓 함수는 특정 전송 서비스 공급자에 바인딩된 소켓을 만듭니다.

learn.microsoft.com

 

소켓 서버/클라이언트 예시 코드

 

*서버

#include<iostream>
#include<string>
#include<winsock2.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 6060
#define BUFFER_SIZE 100

SOCKET gListen, gAccept;
SOCKADDR_IN gAddress;
CHAR buffer[BUFFER_SIZE] = { 0 };

// 서버
int main()
{
    // 윈속 초기화
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    if ((gListen = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    gAddress.sin_family = AF_INET;
    gAddress.sin_addr.s_addr = INADDR_ANY;
    gAddress.sin_port = htons(PORT);

    // 소켓 바인딩
    if (bind(gListen, (PSOCKADDR)&gAddress, sizeof(gAddress)) == SOCKET_ERROR)
    {
        printf("bind() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    // 연결 요청 수신 대기
    if (listen(gListen, 5))
    {
        printf("listen() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    std::cout << "Waiting for a connection..." << std::endl;

    // 연결 수락
    if ((gAccept = accept(gListen, NULL, NULL)) == INVALID_SOCKET)
    {
        printf("accept() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    std::cout << "Connection!" << std::endl;

    // 받기
    if (recv(gAccept, buffer, sizeof(buffer), 0) <= 0)
    {
        printf("WSARecv() failed with error %d\n", WSAGetLastError());
        return -1;
    }
    std::cout << "Message received: " << buffer << std::endl;

    // 종료
    closesocket(gListen);
    closesocket(gAccept);
    WSACleanup();
    return 0;
}

 

*클라이언트

#include<iostream>
#include<string> 
#include<WinSock2.h> 
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 6060
#define ADDR "127.0.0.1"
#define BUFFER_SIZE 100

SOCKET gSocket;
SOCKADDR_IN gAddress;
CHAR buffer[BUFFER_SIZE] = {0};

// 클라
int main()
{
    Sleep(1000);
    // 윈속 초기화
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }
    gAddress.sin_family = AF_INET;
    gAddress.sin_port = htons(PORT);

    if (inet_pton(AF_INET, ADDR, &(gAddress.sin_addr)) != 1)
    {
        printf("Invalid IP address %d\n", WSAGetLastError());
        return -1;
    }
    else
    {
        std::cout << "Server IP address: " << ADDR << std::endl;
        if ((gSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
        {
            printf("socket() failed with error %d\n", WSAGetLastError());
            return -1;
        }
        if (connect(gSocket, (SOCKADDR*)&gAddress, sizeof(gAddress)) < 0)
        {
            printf("connect() failed with error %d\n", WSAGetLastError());
            return -1;
        }
        std::cout << "Connected to server!" << std::endl;
        std::string sendMessage;

        std::cout << "Input massge: ";
        std::cin >> sendMessage;

        /* 5. 메시지 전송 */

        send(gSocket, sendMessage.c_str(), sendMessage.length(), 0);

        closesocket(gSocket);
    }
    WSACleanup();
    return 0;
}

메모리 맵 (Memory Map)

특징

  • 파일을 프로세스에 매핑시켜 파일의 메모리에 접근하여 읽거나 쓰는 기법입니다.
  • 파일을 매핑할 때 실제 물리 메모리 페이지에 해당 파일 데이터를 로드하고, 이 물리 메모리를 가상 메모리 주소로 매핑합니다.
  • 공유 메모리와 비슷하나 다른 점은, OS가 메모리 공간을 할당해주는 것이 아닌, 파일의 메모리를 읽고 쓴다는 것입니다.

장점

  • 프로세스가 데이터를 메모리에 복사하는 것이 아닌, 직접 쓰고 읽으므로 속도가 빠릅니다.
  • 여러 프로세스가 동일한 데이터를 읽기, 쓰기가 가능합니다.
  • 다른 IPC에 비해 비교적 큰 데이터를 처리하기가 간편합니다.

단점

  • 데이터의 동기화 문제에 주의해야 합니다.
  • 메모리에 직접 접근하므로 보안에 취약할 수 있습니다.
  • 할당한 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

번외1) 메모리 맵 vs 파일 I/O

파일을 읽고 쓰는거면 파일IO(std::filesystem 등)랑 다른게 뭘까요? 몇몇 차이점이 존재합니다.

1. 메모리 맵은 실제 메모리 공간을 사용하는 기법이나, 파일I/O는 파일의 데이터를 버퍼로 복사하고, 버퍼로 씁니다.

2. 메모리 맵은 파일의 용량에 상관없이 필요한 데이터만 로드하지만, 파일IO는 모든 데이터를 직접 로드하고 쓰기 때문에 느립니다.

번외2) 그러면 수십 기가바이트의 파일을 어떻게 로드할까?

앞서, 메모리 맵은 파일의 용량에 상관없이 필요한 데이터만 로드한다고 했는데요. 어떤 식으로 그렇게 로드할 수 있는걸까요?

간단합니다! 필요한 데이터가 있는 페이지만 로드하게 됩니다. 때문에 파일의 용량이 아무리 커도 속도에 영향을 받지 않습니다.

 

 

사용법

더보기

CreateFileMappingA 함수(winbase.h) - Win32 apps | Microsoft Learn

 

CreateFileMappingA 함수(winbase.h) - Win32 apps

지정된 파일에 대한 명명되거나 명명되지 않은 파일 매핑 개체를 만들거나 엽니다. (CreateFileMappingA)

learn.microsoft.com

MapViewOfFile 함수(memoryapi.h) - Win32 apps | Microsoft Learn

 

MapViewOfFile 함수(memoryapi.h) - Win32 apps

호출 프로세스의 주소 공간에 파일 매핑 보기를 매핑합니다.

learn.microsoft.com

 

메모리 맵 사용 예시

#include <iostream>
#include <windows.h>

#define FILE_PATH "example.txt"
#define BUFFER_SIZE 256

int main() {
    // 파일 열기(없으면 생성)
    HANDLE hFile = CreateFileA(
        FILE_PATH,                       // 파일 경로
        GENERIC_READ | GENERIC_WRITE,    // 읽기/쓰기 권한
        0,                               // 공유 모드 없음
        NULL,                            // 보안 속성
        OPEN_ALWAYS,                     // 파일이 없으면 생성
        FILE_ATTRIBUTE_NORMAL,           // 일반 파일 속성
        NULL                             // 템플릿 파일 없음
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        std::cerr << "CreateFile failed: " << GetLastError() << std::endl;
        return -1;
    }

    // 파일 매핑 생성
    HANDLE hMapFile = CreateFileMappingA(
        hFile,                // 파일 핸들
        NULL,                 // 보안 속성
        PAGE_READWRITE,       // 읽기/쓰기 권한
        0,                    // 파일 크기 상위 32비트
        BUFFER_SIZE,          // 파일 크기 하위 32비트
        NULL                  // 이름 없음
    );

    if (hMapFile == NULL) {
        std::cerr << "CreateFileMapping failed: " << GetLastError() << std::endl;
        CloseHandle(hFile);
        return -1;
    }

    // 매핑된 메모리 접근
    LPVOID pBuf = MapViewOfFile(
        hMapFile,               // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,    // 접근 권한
        0,                      // 오프셋 상위 32비트
        0,                      // 오프셋 하위 32비트
        BUFFER_SIZE             // 매핑 크기
    );

    if (pBuf == NULL) {
        std::cerr << "MapViewOfFile failed: " << GetLastError() << std::endl;
        CloseHandle(hMapFile);
        CloseHandle(hFile);
        return -1;
    }

    // 데이터 쓰기
    strcpy_s((char*)pBuf, BUFFER_SIZE, "Hello, File Mapping!");

    std::cout << "Data written to file via mapping: " << (char*)pBuf << std::endl;

    // 메모리 해제
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
    CloseHandle(hFile);

    return 0;
}

 

 


 

반응형

'프로그래밍 > CS' 카테고리의 다른 글

[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 데드락(Deadlock)  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19
[CS] MMU, TLB  (0) 2024.11.04
[CS] 세그멘테이션  (0) 2024.11.04
반응형

참고 : [운영체제] 페이지 교체 알고리즘

저번에 페이징에 대해서 포스팅을 했던적이 있는데요?

페이지 교체 알고리즘에 대해선 안다뤘었습니다.

그래서 이번 기회에 한번 다뤄보려고 합니다.

바로 ㄱㄱ


페이지 교체 알고리즘

페이지 교체 알고리즘은 운영체제가 가상 메모리를 효율적으로 관리하기 위해 사용하는 방법 중 하나로, 메모리 부족 상황에서 어떤 페이지를 교체할지 결정하는 역할을 합니다. 페이지 교체 알고리즘은 무수히 많지만, 하지만 오늘은 그중 많이 다루는 몇가지만 다루려고 합니다.

페이지 교체 알고리즘들.png


 

1. FIFO (First In, First Out)

  • 개념: 가장 먼저 들어온 페이지를 가장 먼저 교체합니다. (큐를 사용해서 구현)
  • 장점: 구현이 간단합니다.
  • 단점: 오래된 페이지가 여전히 많이 사용되는 경우 성능 저하 유발합니다. 페이지 수가 늘어날 수록 page fault가 발생하는 빈도가 늘어나는 경우(Belady's Anomaly)가 생길 수 있습니다.

2. OPT (Optimal)

4번 시점에서 이후에 A와 B 페이지가 사용될 것을 알고 있기 때문에 C 페이지를 대상 페이지로 선정해 스왑 아웃시킨다.
  • 개념: 앞으로 가장 오래 사용되지 않을 페이지를 교체합니다.
  • 장점: 가장 적은 페이지 폴트를 보장합니다. (이론상 최강).
  • 단점: 미래의 페이지 접근 패턴을 알아야 하는데, 이는 실질적으로 구현이 불가능하다고 합니다.

3. LRU (Least Recently Used)

  • 개념: 가장 오랫동안 사용되지 않은 페이지를 교체합니다.
  • 장점: FIFO보다 성능이 좋습니다.
  • 단점: 최근 사용 기록을 저장하고 갱신하는 비용이 발생합니다.

4. LFU (Least Frequently Used)

  • 개념: 사용 빈도가 가장 낮은 페이지를 교체합니다.
  • 장점: 자주 사용되는 페이지를 보호할 수 있습니다.
  • 단점: 페이지의 사용 빈도를 저장하고 갱신하는 비용이 발생하게 됩니다. 최신 페이지가 비교적 불리한 방향이므로 특정 상황에서 성능 저하가 생길 수 있습니다.

5. Clock (Second Chance)

위 그림과 같이 대상 페이지를 가리키는 포인터를 사용하는데, 이 포인터가 큐의 맨 아래로 내려가면 시계처럼 다음번에는 다시 큐의 처음을 가리키게 된다.
교체 시, 비트가 0일 경우 교체, 1일 경우 다음 포인터로 넘어가서 다시 검사

  • 개념: FIFO 변형으로, 각 페이지에 사용 비트를 사용합니다. (1이면 사용 중)
  • 장점: 최근에 사용된 페이지는 교체되지 않습니다. LRU처럼 과거 사용 기록을 저장하지 않아도 됩니다.
  • 단점: 참조 비트만으로는 페이지가 얼마나 자주 사용되었는지, 최근에 사용되었는지 구체적으로 알 수 없습니다. 구현이 비교적 복잡하고 페이지의 사용 비트를 저장 및 갱신하는 비용이 발생합니다.

현대 OS는 어떤 알고리즘을 주로 사용할까?

일반적으로 운영체제는 LRU와 Clock을 실제로 사용하는 경우가 많습니다.

LRU와 Clock을 혼용하여 가장 오랫동안 사용되지 않은 페이지를 교체하며, 최근에 참조된 페이지를 보존하는데 효과적이도록 페이지 교체를 수행한다고 합니다.

반응형

'프로그래밍 > CS' 카테고리의 다른 글

[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 데드락(Deadlock)  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19
[CS] MMU, TLB  (0) 2024.11.04
[CS] 세그멘테이션  (0) 2024.11.04
반응형

아안뇨오엉하세요엉ㅇ

저번에 뮤텍스와 세마포어에 대해서 글을 적어봤는데요?

동기화기법은 신이고 무적같지만 사실 허점이 하나 있습니다.

바로 프로세스나 스레드간 교착 상태에 빠질 수 있다는 점인데요...

이 현상을 바로 데드락이라고 합니다!

데드락이 뭔지 같이 알아봅시당


데드락(Deadlock)이 뭘까?

데드락은 둘 이상의 프로세스나 스레드가 각각 서로 점유하고 있는 자원을 기다릴 때 무한 대기에 빠지는 상황을 일컫습니다.

아래 사진은 데드락이 생기는 과정을 사진으로 간단하게 나타낸 것입니다.

 

자원 A를 가진 스레드1이 있고,

자원 B를 가진 스레드2가 있을때,

스레드1: 야 B내놔ㅋ

스레드2: 싫어 A내놔ㅋ

이러고 있는거임ㅋㅋ


데드락이 생기는 조건?

데드락은 다음 네 가지 조건이 모두 만족될 때 발생합니다:

  1. 상호 배제(Mutual Exclusion):
    • 자원은 동시에 하나의 프로세스만 사용할 수 있습니다.
  2. 점유와 대기(Hold and Wait):
    • 프로세스는 자신이 이미 점유하고 있는 자원을 유지하면서, 추가로 다른 자원을 요청하며 대기합니다.
    • 예: 스레드1은 자원 A를 점유한 상태에서 자원 B를 요청
  3. 비선점(No Preemption):
    • 점유한 자원은 해당 프로세스가 작업을 완료하기 전까지 강제로 빼았지 못합니다.
    • 예: 프로세스가 사용하는 자원은 작업 완료 전까지 다른 프로세스가 가져갈 수 없음.
  4. 순환 대기(Circular Wait):
    • 프로세스들이 순환적으로 자원을 요청하며 서로 대기합니다.
    • 예: A → B → C → A 순환 구조로 자원을 기다리는 상황.

 

세상에 이런 상황이 있다고?

놀랍게도 이런 상황이 나올 수 있고, 생각보다 간단하게 나옵니다.

아래는 데드락이 발생할 수 있는 예제코드입니다.

더보기
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1, mutex2;

void Worker1() 
{
    mutex1.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 다른 스레드가 실행될 여유를 줌
    mutex2.lock();

    mutex1.unlock();
    mutex2.unlock();
}

void Worker2() 
{
    mutex2.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 다른 스레드가 실행될 여유를 줌
    mutex1.lock();

    mutex2.unlock();
    mutex1.unlock();
}

int main() {
    std::thread t1(Worker1);
    std::thread t2(Worker2);

    t1.join();
    t2.join();

    return 0;
}

이렇게 되면 worker1은 mutex1을 통해 임계영역에 접근을 했지만, mutex2를 통해 또 다른 자원에 접근하고 싶지만, 이미 worker2가 mutex2를 통해 해당 자원을 점유한 상태입니다.
(따라서 초딩같이 싸우고 있다는거임ㅋㅋ)


데드락을 해결하려면?

데드락을 해결하려면 무슨 방법이 있을까요?


1) 예방(Prevention):

데드락 발생 조건 중 하나를 제거하여 문제를 사전에 방지.

  • 상호 배제 제거: 자원을 공유 가능하게 설계 (단, 현실적으로 어려움).
  • 점유와 대기 제거: 모든 자원을 한 번에 요청하도록 설계.
  • 비선점 제거: 자원을 강제로 해제하거나 다른 프로세스로 넘김.
  • 순환 대기 제거: 자원 요청 순서를 정해 순환 대기를 방지.

2) 회피(Avoidance):

자원 할당 시 데드락 발생 가능성을 분석해 안전한 상태(Safe State)만 유지.

  • 은행원 알고리즘(Banker's Algorithm):
    • 은행원이 고객에게 대출을 승인하기 전에 시스템 상태를 분석하는 방식에서 이름이 유래했습니다. 이를 프로세스와 자원 할당 문제에 대입하면 다음과 같은 원리로 작동합니다.
      • 안전 상태(Safe State): 모든 프로세스가 작업을 완료하고 자원을 해제할 수 있는 상태입니다. 즉 모든 요청이 처리 가능한 상태.
      • 불안전 상태(Unsafe State): 특정 자원 요청이 승인을 받으면 다른 프로세스가 자원을 기다리면서 데드락이 발생할 가능성이 있는 상태입니다.

3) 탐지(Detection):

데드락이 발생했는지 확인하고, 이를 해결하기 위한 전략을 사용하는 방법입니다. 탐지는 데드락을 허용한 상태에서 주기적으로 시스템의 자원 상태를 검사하여 데드락을 확인합니다. 탐지 후에는 데드락을 해결하기 위한 적절한 조치를 취합니다.

  • 시스템 상태 검사
    • 자원 할당 상태와 프로세스의 요청 상태를 바탕으로 Deadlock Detection Algorithm을 실행합니다. 데드락의 주요 조건인 사이클(Cycle) 또는 교착 상태(Wait-for Graph)를 탐지합니다. 
    • 입력 데이터를 통해 현재 상태를 보내줍니다. 
      • Available (가용 자원): 현재 시스템의 남아 있는 자원 수.
      • Allocation (현재 할당): 각 프로세스에 이미 할당된 자원의 양.
      • Request (현재 요청): 각 프로세스가 추가로 요청한 자원의 양.

4) 회복(Recovery):

데드락 탐지 이후 이미 발생한 데드락을 해결하는 과정입니다. 시스템이 데드락 상태임을 확인한 후, 프로세스를 종료하거나 자원을 회수하여 데드락을 해소하는 방법입니다.

  • 데드락 회복 전략
    • 프로세스 강제 종료(Termination) : 데드락 상태를 해소하기 위해 프로세스를 강제로 종료하여 자원을 해제하는 방법입니다.
    • 자원 강제 회수 (Resource Preemption)데드락 상태를 해소하기 위해 프로세스를 강제로 종료하여 자원을 해제하는 방법입니다.
    • 데드락에 얽히지 않은 프로세스 우선 실행 : 데드락 상태의 프로세스는 중단하거나 대기시키고, 데드락에 얽히지 않은 다른 프로세스부터 실행합니다.

데드락을 해결하는 방법을 알아봤지만, 결국엔 알고리즘과 상태검사로 인해 성능 오버헤드가 발생할 수 있습니다.

가장 좋은 방법은 데드락이 발생하지 않도록 예방하는 것이 중요하다고 생각합니다!

무엇이든 예방이 중요하다...


현대 OS는 데드락을 어떻게 처리할까?

현대의 운영체제, 즉 UNIX를 포함한 대부분의 OS는 일반적으로 데드락 탐지나 회복을 직접적으로 처리하지 않는 경우가 많습니다. 대신, OS는 데드락이 발생할 가능성을 낮추거나 개발자나 시스템 관리자가 이 문제를 처리하도록 설계하는 데 중점을 둡니다.


현대 OS가 데드락을 처리하지 않는 이유는?

(1) 성능 오버헤드

  • 데드락을 탐지하거나 예방하는 알고리즘은 복잡한 연산을 포함하며, 이를 실시간으로 처리하려면 시스템 리소스에 큰 부하가 걸릴 수 있습니다.
  • 특히 멀티스레드 환경이나 분산 시스템에서는 데드락 상태를 정확히 탐지하는 것이 어렵고, 탐지 비용이 높습니다.

(2) 일반화된 해결책 부족

  • 데드락은 특정 상황에서 발생하므로, 모든 프로그램에 대해 범용적으로 작동하는 데드락 처리 방법을 제공하기 어렵습니다.
  • 데드락을 해결하기 위한 프로세스 종료나 자원 회수는 애플리케이션의 상태를 망가뜨릴 수 있어, OS가 임의로 조치하는 것은 적절하지 않을 수 있습니다.

현대 OS는 데드락 처리를 시스템 수준에서 자동화하지 않고, 다음과 같은 방식을 통해 간접적으로 문제를 다룹니다:

  1. 개발자가 예방적 설계를 할 수 있도록 동기화 메커니즘과 도구 제공.
  2. 시스템 로그와 디버깅 툴로 데드락 원인 파악 지원.
  3. 사용자에게 프로세스 종료와 같은 수동적 해결책 제공.

운영체제는 성능과 안정성을 위해 데드락 관련 문제를 개발자의 책임으로 위임하는 방향으로 설계되었습니다.

따라서 우리 개발자는 데드락이 발생하지 않도록 신중히 코드를 짤 필요가 있습니다...ㅇㅅㅇ

반응형

'프로그래밍 > CS' 카테고리의 다른 글

[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19
[CS] MMU, TLB  (0) 2024.11.04
[CS] 세그멘테이션  (0) 2024.11.04
반응형

안녕하세요?

오늘은 뮤텍스와 세마포어에 관해 포스팅을 해보겠습니다!

바로 ㄱㄱ싱

참고자료

Mutex vs Semaphore - GeeksforGeeks

Priority Inversion & Priority Inheritance (우선순위 역전과 우선순위 상속)


동기화 문제

현대 CPU는 거의 멀티 코어를 지원하기 때문에 멀티스레딩을 필수적으로 해야하는데요.

멀티스레딩 기법을 쓰면 데이터의 무결성을 침해하게 되는 경우가 빈번하게 있습니다.

예를 들자면, 스레드A가 특정 자원을 읽는 도중에, 스레드B가 해당 자원을 수정하는 상황이 있습니다.

이 때, 스레드A가 읽는 자원은 경쟁 상태(Race Condition)이 되어 데이터의 무결성을 보장받을 수 없습니다. 이러한 상황을 동기화 문제라고 하는데요.

이러한 상황을 해결하기 위해 동일 자원에 대해서 동기화 문제가 발생할 가능성이 있어 하나의 스레드만 접근할 수 있도록 보호해줘야 되는 영역을 임계영역(Critical Section)이라고 합니다.

임계영역의 구현은 여러가지지만, 오늘은 대표적으로 뮤텍스와 세마포어에 대해 알아보겠습니다. 가보시죠 ㅇㅅㅇ


뮤텍스(Mutex)

뮤텍스는 상호 배제 (mutual exclusion)의 줄임말이며, 한 자원에 대하여 하나의 스레드만 접근할 수 있도록 보호해주는 객체입니다.

뮤텍스는 락킹(Locking)을 통해 오직 하나의 스레드만 임계영역에 들어올 수 있습니다. 락킹은 다음과 같은 연산이 있습니다.

  • 락(lock): 현재 스레드가 임계 구역에 들어가기 위해 뮤텍스를 획득합니다. 만일 다른 프로세스나 스레드가 임계 구역을 사용 중이라면 뮤텍스가 해제될 때까지 대기합니다.
  • 언락(unlock): 현재 스레드가 임계 구역의 사용이 끝났음을 알리기 위해 뮤텍스를 해제합니다. 이후 다른 스레드가 lock을 통해 임계 구역에 진입할 수 있습니다. 뮤텍스를 소유한 스레드만 해제할 수 있습니다.

● 특징

뮤텍스는 우선순위 역전 문제를 피하기 최소화하기 위하여 우선순위 상속 메커니즘(priority inheritance mechanism)을 사용할 수 있습니다. 뮤텍스는 현재 우선순위가 더 높은 태스크가 가능한 한 가장 짧은 시간 동안 블로킹(blocking) 상태에 있을 수 있도록 합니다. 우선순위 역전 문제를 완전히 해결할 수는 없지만, 그 영향을 줄일 수 있습니다.

● 장점

  1. 경쟁 상태(Race Condition) 방지: 한 번에 하나의 프로세스만 임계영역에 접근하므로 동기화 문제가 발생하지 않습니다.
  2. 데이터 무결성 유지: 공유 자원의 일관성을 보장합니다.
  3. 간단한 잠금 메커니즘: 임계영역에 진입 시 잠금(Lock)하고, 나올 때 해제(Unlock)합니다.

● 단점

  1. 기아(Starvation) 문제: 스레드가 임계영역에 진입한 상태에서 슬립 상태가 되거나, 우선순위가 높은 프로세스에 의해 선점되면 다른 스레드가 대기 상태에 빠질 수 있습니다.
  2. 바쁜 대기(Busy Waiting) 문제: 임계영역 접근 권한을 얻기 위해 기다리는 것이 아니라 확인하기 위해 무한 루프를 돌게 되는데, 이 과정에서 CPU 자원이 매우 낭비된다. (그래서 mutex lock을 spinlock이라고 부르기도 한다네요?)
  3. 잠금 및 해제 메커니즘의 제한: 이전 스레드가 임계영역을 나가기 전까지 다른 프로세스는 접근할 수 없습니다. 즉, 별도의 잠금/해제 방식을 제공하지 않습니다.

● 직접 써보자

더보기

직접 써봐야 더 이해가 잘 될거같아서 해보았읍니다.

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

std::vector<int> q;
std::mutex mtx;

// 이 매크로를 끄면 뮤텍스를 안씁니다.
#define USE_MUTEX

void UseMutexWorker(int id)
{
	mtx.lock(); // 뮤텍스 획득

	// TODO : 작업 실시
	q.push_back(id);
	std::cout << "Thread ID " << id << " - push_back()\n";
	// 작업 종료

	mtx.unlock(); // 뮤텍스 해제
}

void DefaultWorker(int id)
{
	q.push_back(id);
	std::cout << "Thread ID " << id << " - push_back()\n";
}

int main()
{
	std::vector<std::thread> workers;
	for (int i = 0; i < 10; ++i)
	{
#ifdef USE_MUTEX
		workers.push_back(std::thread(UseMutexWorker, i));
#else
		workers.push_back(std::thread(DefaultWorker, i));
#endif // USE_MUTEX
	}
	for (int i = 0; i < workers.size(); ++i)
	{
		// 스레드가 종료되었는가?
		workers[i].join();
	}
	for (int i = 0; i < q.size(); ++i)
	{
		std::cout << "Queue[" << i << "] = " << "Thread ID " <<  q[i] << " Visit\n";
	}
}

스레드에게 ID값을 0~9까지 만들고, 각 스레드들은 전역변수의 큐에 자신의 ID값을 넣는 간단한 코드입니다.

변화를 보기 위해 #define USE_MUTEX 을 사용해 뮤텍스를 쓰는 코드와 안쓰는 코드를 스레드에게 넘겨줬습니다.

 

1. 뮤텍스를 안쓴 경우

작업이 뒤죽박죽인 모습. 간헐적인 크래시도 발생

뮤텍스를 쓰지 않으면 큐에 넣는 작업의 순서도 뒤죽박죽이며, 간헐적으로 크래시가 난다. 동시 접근에 대한 에러로 인해 나는 크래시인듯 하다.

이런 느낌으로 나네요?

 

2. 뮤텍스를 쓴 경우

작업 순서도 깔끔하고, 크래시도 발생하지 않는다.

뮤텍스를 쓰면 깔끔해지는 모습이다. 근데 사실 이러면 싱글스레드를 쓴거나 다름없다. 오히려 싱글스레드보다 느리다.

 


세마포어(Semaphore)

세마포어 - 위키백과, 우리 모두의 백과사전

세마포어(Semaphore)는 여러 스레드간에 공유되는 카운터 값인 0이상의 정수형 변수로 이루어진 객체입니다.

여기서 

세마포어는 신호형 메커니즘을 기반으로 동작하며, 한 스레드가 다른 스레드에게 신호를 보내는 방식으로 동작합니다.

세마포어는 신호를 주고받아 스레드의 자원 사용을 허용합니다. 신호를 주고받는 연산은 다음과 같습니다.

  • wait(): 세마포어의 카운터 값을 감소시키고, 값이 0이 되면 해당 스레드는 대기 상태가 됩니다.
  • signal(): 세마포어의 값을 증가시키고, 대기 중인 스레드가 있으면 실행 상태로 전환합니다.

● 특징

세마포어는 뮤텍스와 다르게 하나의 스레드에만 임계영역의 접근 권한을 제한하지 않습니다. 또한 특정 스레드가 반드시 lock과 unlock을 해야하는 엄격한 소유권(Strict Ownership)이 없습니다.

 

● 장점

  1. 다중 스레드 접근 허용: 여러 스레드가 동시에 임계영역에 접근할 수 있습니다.
  2. 기계 독립성: Semaphore는 특정 하드웨어에 종속되지 않으므로 다양한 환경에서 사용 가능합니다.
  3. 유연한 자원 관리: 제한된 수의 리소스를 효율적으로 관리할 수 있습니다.

● 단점

  1. 우선순위 역전(Priority Inversion) 문제: 낮은 우선순위의 스레드가 자원을 점유하고 있을 경우, 높은 우선순위의 스레드가 대기 상태에 빠질 수 있습니다.
  2. 프로그래밍 오류: wait() 및 signal() 호출 순서가 이상해지면 데드락이나 상호 배제 속성 위반이 발생할 가능성이 높습니다.
  3. 운영 체제의 부하: 운영 체제가 모든 wait() 및 signal() 호출을 추적하고 관리(커널 콜)해야 하므로 부하가 증가할 수 있습니다.
  4. 바쁜 대기(Busy Waiting) 문제: 임계영역 접근 권한을 얻기 위해 기다리는 것이 아니라 확인하기 위해 무한 루프를 돌게 되는데, 이 과정에서 CPU 자원이 매우 낭비된다.

 

● 직접 써보자

더보기

직접 써봐야 더 이해가 잘 될거같아서 해보았읍니다.

#include <iostream>
#include <windows.h>
#include <thread>
#include <vector>

#define MAX_WORK 3   // 동시에 실행할 수 있는 최대 스레드 수
#define TOTAL_TASKS 10  // 작업 총 개수

std::vector<int> q;
HANDLE semaphore;  // 세마포어 핸들

// 이 매크로를 끄면 세마포어를 안씁니다.
//#define USE_SEMAPHORE

void UseSemaphoreWorker(int id)
{
    // 세마포어 대기 (카운트가 0이면 대기 상태)
    WaitForSingleObject(semaphore, INFINITE);

    // 작업 시작
    q.push_back(id);
    std::cout << "Thread ID " << id << " - push_back()\n";

    // 세마포어 해제 (카운트 증가)
    ReleaseSemaphore(semaphore, 1, NULL);
}

void DefaultWorker(int id)
{
    q.push_back(id);
    std::cout << "Thread ID " << id << " - push_back()\n";
}


int main() 
{
    // 세마포어 생성
    semaphore = CreateSemaphore(
        NULL,             // 보안 속성
        MAX_WORK,         // 초기 카운트
        MAX_WORK,         // 최대 카운트
        NULL              // 세마포어 이름 (익명)
    );

    std::vector<std::thread> workers;

    for (int i = 0; i < TOTAL_TASKS; ++i) 
    {
#ifdef USE_SEMAPHORE
        workers.push_back(std::thread(UseSemaphoreWorker, i));
#else
        workers.push_back(std::thread(DefaultWorker, i));
#endif // USE_MUTEX
    }

    // 모든 스레드가 종료될 때까지 대기
    for (auto& t : workers) { t.join(); }

    for (int i = 0; i < q.size(); ++i)
    {
        std::cout << "Queue[" << i << "] = " << "Thread ID " << q[i] << " Visit\n";
    }

    // 세마포어 닫기
    CloseHandle(semaphore);

    return 0;
}

위 뮤텍스 코드와 동작은 같습니다. 다만 세마포어 객체를 만들어 동기화 관리를 한다는 점에서 다릅니다.

위 코드에선 최대 3개의 스레드가 동시에 일할 수 있도록 설정해놓았습니다.

 

세마포어를 안쓴 경우는 위 뮤텍스 코드랑 다를게 없어서 넘어가겠습니다.

 

- 세마포어를 쓴 결과

엄 사실 짜고보니까 예시가 세마포어를 써야하는 상황이랑 안맞는 것 같습니다.

어쨋든 동작은 최대 3개의 스레드가 동시에 일하도록 만든 코드입니다...


바쁜 대기(Busy Waiting)이 무조건 나쁠까?

바쁜 대기(Busy Waiting)란?

OS에서는 원하는 자원을 얻기 위해 기다리는 것이 아니라 권한을 얻을 때까지 반복해서 확인하는 것을 의미합니다.

이는 CPU의 자원을 쓸데 없이 낭비하기 때문에 좋지 않은 쓰레드 동기화 방식입니다.

// 예시
while (!flag) {
    // 아무것도 안함. 그냥 루프만 돌기
    // 혹은 break조건 확인만 하기
}

하지만 이를  "무조건 나쁘다"고 단정할 수는 없습니다. 상황에 따라 효율적인 선택이 될 수도 있기 때문입니다. 

어째서??

크게 두가지의 이유가 있습니다.

 

  • 컨텍스트 스위칭 비용 회피
    • 컨텍스트 스위칭은 운영 체제가 다른 스레드나 프로세스를 실행하기 위해 현재 상태(레지스터, 스택 등)를 저장하고, 새 작업으로 교체하는 작업입니다.
    • 이 과정에서 발생하는 **오버헤드(CPU 비용)**는 바쁜 대기보다 클 수 있습니다.
    • 짧은 대기 시간이 예상된다면, 컨텍스트 스위칭 없이 바쁜 대기를 사용하는 것이 더 효율적일 수 있습니다.
  • 짧은 시간 내에 락 해제가 예상되는 경우
    • 락이 곧 해제될 것이 거의 확실한 상황이라면, 바쁜 대기가 오히려 자원을 빠르게 확보할 수 있는 전략이 됩니다.
    • 예: 큐가 거의 비어있고, 생산자-소비자 문제에서 잠깐의 대기만 필요한 경우.
  • 특정 알고리즘 또는 자원의 경량 동기화가 필요한 경우
    • 대규모 락이나 세마포어 같은 무거운 동기화 도구 대신, 간단히 상태를 확인하면서 대기하는 방법이 더 적합한 경우입니다.
    • 예: 아직 안배웠지만, 원자적 변수와 std::atomic을 이용한 동기화에서.

Mutex와 Semaphore에 대한 오해

Mutex와 이진 세마포어(Binary Semaphore)는 종종 혼동됩니다. 많은 사람들이 Mutex가 이진 세마포어라고 생각할 수 있지만, 이는 사실이 아닙니다! Mutex와 세마포어는 다른 목적을 가지고 있습니다. 비록 구현 방식이 비슷하기 때문에 이진 세마포어로 불리기도 하지만, Mutex와 세마포어는 용도와 동작 방식에서 차이가 있습니다.

  • Mutex는 자원에 대한 접근을 동기화하기 위해 락킹 매커니즘을 사용합니다.
    • 하나의 작업(스레드 또는 프로세스)이 Mutex를 획득할 수 있으며, Mutex를 소유한 작업만 잠금을 해제할 수 있습니다.
  • Semaphore는 운영 체제에서 신호(Signaling) 메커니즘으로 사용되며, 공유 자원에 대한 접근을 제어합니다.
    • 예를 들어, 컴퓨터에서 대용량 파일을 다운로드(Task A)하면서 동시에 문서를 인쇄(Task B)하려고 한다고 가정해봅시다.
    • 인쇄 작업이 시작되면 Semaphore는 다운로드가 완료되었는지 확인합니다.
    • 다운로드가 진행 중이면, Semaphore는 인쇄 작업을 대기 상태로 만들어 "다운로드가 끝날 때까지 기다려 주세요"라고 신호를 보냅니다.
    • 다운로드가 완료되면 Semaphore는 인쇄 작업을 시작하라는 신호를 보냅니다.
    • 이처럼 Semaphore는 두 작업이 서로 간섭하지 않도록 하며, 시스템 자원을 효율적으로 관리하여 작업 간 충돌 없이 부드럽게 실행되도록 합니다.

바람직한 동기화 문제 해결

오늘은 뮤텍스와 세마포어를 이용해 동기화 문제를 해결하는 방법을 알아보았습니다.

그럼 동기화 문제가 발생했을 때는 꼭 동기화 객체를 이용해야 동기화 문제를 잘 해결했다고 볼 수 있을까요? 라고 묻는다면 그건 아닙니다.

 

다음과 같은 이유가 있습니다.

1. 프로그래밍에 있어서 동기화 뮤텍스와 세마포어는 커널 객체기 때문에 시스템 호출을 기본적으로 떠안고 가야됩니다. (성능 오버헤드)

2. Busy Wait가 일어나기 때문에 CPU의 자원 낭비가 이루어집니다.

3. 올바른 방법을 사용하지 않고 오용할 경우 데드락(Deadlock)이 발생해 교착상태에 빠질 수 있습니다.

이 외에도 많지만, 그럼에도 쓰는 이유는 올바르게 사용하기만 하면 데이터 무결성을 보장받을 수 있기 때문입니다.

 

역설로 가보자면, 데이터 무결성만 보장할 수 있다면 동기화 객체를 사용하지 않아도 된다는 것입니다. 즉, 임계영역이 없게끔 설계하는 것이 중요합니다.

 

예를 들어보겠습니다.

스레드A, B, C가 있고, 30개의 원소가 담긴 배열이 있습니다.

30개의 원소를각각 1로 초기화를 하고싶은데, 이 작업을 스레드 3개로 나눈다고 합시다.

그럼 굳이 동기화 객체를 사용할 필요가 없이 배열 10칸씩 스레드에게 작업을 맡기면 됩니다.

이러면 각 스레드가 서로의 작업 영역에 침범하지 않으므로 데이터가 무결할 수 있습니다.

반응형

'프로그래밍 > CS' 카테고리의 다른 글

[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 데드락(Deadlock)  (0) 2024.11.25
[CS] MMU, TLB  (0) 2024.11.04
[CS] 세그멘테이션  (0) 2024.11.04
[CS] 물리 메모리와 가상 메모리  (0) 2024.11.04

+ Recent posts