본문 바로가기
-----ETC-----/C++ 고급 프로그래밍과 응용 프로젝트 시리즈

[C++ 고급 프로그래밍과 응용 프로젝트 시리즈] Day 18: 프로젝트 1 - 멀티스레딩을 이용한 동시성 처리

by cogito21_cpp 2024. 8. 1.
반응형

멀티스레딩을 이용한 동시성 처리

멀티스레딩을 사용하면 여러 클라이언트의 요청을 동시에 처리할 수 있어 서버의 성능을 크게 향상시킬 수 있습니다. 오늘은 C++ 표준 라이브러리의 스레드를 사용하여 웹 서버의 동시성 처리를 구현하겠습니다.

 

멀티스레딩의 필요성

싱글 스레드 서버는 하나의 요청을 처리하는 동안 다른 요청을 기다리게 합니다. 멀티스레딩을 사용하면 여러 요청을 동시에 처리할 수 있어 응답 시간을 줄이고 서버의 처리 능력을 향상시킬 수 있습니다.

 

멀티스레딩 구현

1. 스레드 풀 구현

스레드 풀은 일정 수의 스레드를 미리 생성하고, 작업을 큐에 추가하여 스레드가 작업을 처리하도록 합니다. 이를 통해 스레드 생성 및 소멸의 오버헤드를 줄일 수 있습니다.

 

thread_pool.h

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();

    void enqueue(std::function<void()> task);

private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex queueMutex_;
    std::condition_variable condition_;
    std::atomic<bool> stop_;

    void worker();
};

#endif // THREAD_POOL_H

 

thread_pool.cpp

#include "thread_pool.h"

ThreadPool::ThreadPool(size_t numThreads) : stop_(false) {
    for (size_t i = 0; i < numThreads; ++i) {
        workers_.emplace_back([this] { this->worker(); });
    }
}

ThreadPool::~ThreadPool() {
    stop_ = true;
    condition_.notify_all();
    for (std::thread& worker : workers_) {
        if (worker.joinable()) {
            worker.join();
        }
    }
}

void ThreadPool::enqueue(std::function<void()> task) {
    {
        std::unique_lock<std::mutex> lock(queueMutex_);
        tasks_.push(task);
    }
    condition_.notify_one();
}

void ThreadPool::worker() {
    while (!stop_) {
        std::function<void()> task;
        {
            std::unique_lock<std::mutex> lock(queueMutex_);
            condition_.wait(lock, [this] { return !tasks_.empty() || stop_; });
            if (stop_ && tasks_.empty()) {
                return;
            }
            task = std::move(tasks_.front());
            tasks_.pop();
        }
        task();
    }
}

 

2. 서버에 스레드 풀 통합

서버에서 스레드 풀을 사용하여 클라이언트 요청을 처리하도록 합니다.

 

server.h

#ifndef SERVER_H
#define SERVER_H

#include <string>
#include <netinet/in.h>
#include "router.h"
#include "thread_pool.h"

class Server {
public:
    Server(const std::string& address, int port, size_t numThreads);
    ~Server();

    void start();
    void stop();

    void addRoute(const std::string& path, Router::Handler handler);

private:
    void handleClient(int clientSocket);

    std::string address_;
    int port_;
    int serverSocket_;
    std::atomic<bool> running_;
    struct sockaddr_in serverAddr_;
    Router router_;
    ThreadPool threadPool_;
};

#endif // SERVER_H

 

server.cpp

#include "server.h"
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>

Server::Server(const std::string& address, int port, size_t numThreads)
    : address_(address), port_(port), running_(false), threadPool_(numThreads) {
    serverSocket_ = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket_ == -1) {
        throw std::runtime_error("Failed to create socket");
    }

    serverAddr_.sin_family = AF_INET;
    serverAddr_.sin_addr.s_addr = inet_addr(address.c_str());
    serverAddr_.sin_port = htons(port);

    if (bind(serverSocket_, (struct sockaddr*)&serverAddr_, sizeof(serverAddr_)) == -1) {
        throw std::runtime_error("Failed to bind socket");
    }

    if (listen(serverSocket_, 10) == -1) {
        throw std::runtime_error("Failed to listen on socket");
    }
}

Server::~Server() {
    stop();
    close(serverSocket_);
}

void Server::start() {
    running_ = true;
    while (running_) {
        int clientSocket = accept(serverSocket_, nullptr, nullptr);
        if (clientSocket == -1) {
            std::cerr << "Failed to accept client" << std::endl;
            continue;
        }
        threadPool_.enqueue([this, clientSocket] { handleClient(clientSocket); });
    }
}

void Server::stop() {
    running_ = false;
}

void Server::addRoute(const std::string& path, Router::Handler handler) {
    router_.addRoute(path, handler);
}

void Server::handleClient(int clientSocket) {
    char buffer[1024];
    int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
    if (bytesReceived < 0) {
        std::cerr << "Failed to receive request" << std::endl;
        close(clientSocket);
        return;
    }
    buffer[bytesReceived] = '\0';

    std::string requestStr(buffer);
    Request request(requestStr);

    std::string path = request.getPath();

    router_.handleRequest(path, clientSocket);

    close(clientSocket);
}

 

3. 서버 실행

서버를 실행하여 여러 클라이언트의 요청을 동시에 처리할 수 있습니다.

 

main.cpp

#include "server.h"
#include "response.h"

void handleRoot(int clientSocket) {
    Response response(200);
    response.setHeader("Content-Type", "text/html");
    response.setBody("<html><body><h1>Welcome to the Home Page!</h1></body></html>");

    std::string responseStr = response.toString();
    send(clientSocket, responseStr.c_str(), responseStr.size(), 0);
}

void handleAbout(int clientSocket) {
    Response response(200);
    response.setHeader("Content-Type", "text/html");
    response.setBody("<html><body><h1>About Us</h1></body></html>");

    std::string responseStr = response.toString();
    send(clientSocket, responseStr.c_str(), responseStr.size(), 0);
}

int main() {
    try {
        Server server("127.0.0.1", 8080, 4);  // 4개의 스레드를 가진 스레드 풀 사용
        server.addRoute("/", handleRoot);
        server.addRoute("/about", handleAbout);
        server.start();
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}

 

이제 서버를 실행하고 여러 클라이언트의 요청을 동시에 처리할 수 있는지 테스트해보겠습니다. 멀티스레딩을 통해 서버의 성능이 향상되는 것을 확인할 수 있습니다.

 

이제 18일차의 학습을 마쳤습니다. 멀티스레딩을 사용하여 웹 서버의 동시성 처리를 구현해보았습니다.

 

질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "에러 핸들링 및 로깅"에 대해 학습하고 구현하겠습니다.

반응형