C++에서 std::mutex 동기화 기본 설정 사용

Jinku Hu 2023년10월12일
C++에서 std::mutex 동기화 기본 설정 사용

이 기사는 C++에서 std::mutex 동기화 프리미티브를 사용하는 방법을 보여줍니다.

std::mutex를 사용하여 C++에서 스레드 간 공유 데이터에 대한 액세스 보호

일반적으로 동기화 프리미티브는 프로그래머가 동시성을 활용하는 프로그램에서 공유 데이터에 대한 액세스를 안전하게 제어하기 위한 도구입니다.

여러 스레드에서 공유 메모리 위치를 순서 없이 수정하면 잘못된 결과와 예측할 수 없는 프로그램 동작이 발생하므로 프로그램이 결정적인 방식으로 실행되도록 보장하는 것은 프로그래머에게 달려 있습니다. 동시 프로그래밍의 동기화 및 기타 주제는 매우 복잡하며 현대 컴퓨팅 시스템의 여러 계층의 소프트웨어 및 하드웨어 특성에 대한 광범위한 지식이 필요한 경우가 많습니다.

따라서 이 문서에서 동기화 항목의 아주 작은 부분을 다루면서 이러한 개념에 대한 사전 지식이 있다고 가정합니다. 즉, mutex라고도 하는 상호 배제의 개념을 소개합니다(종종 프로그래밍 언어에서 객체 이름에 동일한 이름이 지정됩니다(예: std::mutex)).

뮤텍스는 프로그램의 중요한 섹션을 둘러싸고 해당 섹션에 대한 액세스가 보호되도록 하는 일종의 잠금 메커니즘입니다. 공유 자원이 보호된다는 것은 하나의 스레드가 공유 객체에 대해 쓰기 작업을 수행하고 있으면 이전 스레드가 작업을 완료할 때까지 다른 스레드가 작동하지 않는다는 것을 의미합니다.

리소스 경합, 리소스 고갈 또는 기타 성능 관련 문제가 발생할 수 있으므로 이와 같은 동작은 일부 문제에 대해 최적이 아닐 수 있습니다. 따라서 일부 다른 메커니즘은 이러한 문제를 해결하고 이 문서의 범위를 벗어나는 다른 특성을 제공합니다.

다음 예제에서는 C++ STL에서 제공하는 std::mutex 클래스의 기본 사용법을 보여줍니다. 스레딩에 대한 표준 지원은 C++11 버전부터 추가되었습니다.

먼저 std::mutex 객체를 생성해야 공유 리소스에 대한 액세스를 제어하는 ​​데 사용할 수 있습니다. std::mutex에는 lockunlock의 두 가지 핵심 멤버 함수가 있습니다. lock 작업은 일반적으로 공유 리소스가 수정되기 전에 호출되고 unlock은 수정 후에 호출됩니다.

이러한 호출 사이에 삽입되는 코드를 임계 섹션이라고 합니다. 코드 레이아웃의 이전 순서가 정확하더라도 C++는 또 다른 유용한 템플릿 클래스인 std::lock_guard를 제공합니다. 이 클래스는 범위가 남아 있을 때 지정된 mutex를 자동으로 잠금 해제할 수 있습니다. lockunlock 멤버 함수를 직접 사용하는 대신 lock_guard를 사용하는 주된 이유는 예외가 발생하더라도 mutex가 모든 코드 경로에서 잠금 해제되도록 보장하기 위해서입니다. 따라서 코드 예제에서도 후자의 방법을 사용하여 std::mutex의 사용법을 보여줍니다.

main 프로그램은 임의의 정수로 된 두 개의 vector 컨테이너를 생성한 다음 둘 다의 내용을 목록으로 푸시하도록 구성됩니다. 까다로운 부분은 여러 스레드를 사용하여 목록에 요소를 추가하려는 것입니다.

실제로 두 개의 스레드로 generateNumbers 함수를 호출하지만 이들은 서로 다른 개체에서 작동하므로 동기화가 필요하지 않습니다. 정수가 생성되면 addToList 함수를 호출하여 목록을 채울 수 있습니다.

이 함수는 lock_guard 구성으로 시작한 다음 목록에서 수행해야 하는 작업을 포함합니다. 이 경우 주어진 목록 객체에 대한 push_back 함수만 호출합니다.

#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

using std::cout;
using std::endl;
using std::list;
using std::string;
using std::vector;

std::mutex list1_mutex;

const int MAX = 1000;
const int NUMS_TO_GENERATE = 1000000;

void addToList(const int &num, list<int> &l) {
  std::lock_guard<std::mutex> guard(list1_mutex);
  l.push_back(num);
}

void generateNumbers(vector<int> &v) {
  for (int n = 0; n < NUMS_TO_GENERATE; ++n) {
    v.push_back(std::rand() % MAX);
  }
}

int main() {
  list<int> list1;
  vector<int> vec1;
  vector<int> vec2;

  std::thread t1(generateNumbers, std::ref(vec1));
  std::thread t2(generateNumbers, std::ref(vec2));
  t1.join();
  t2.join();

  cout << vec1.size() << ", " << vec2.size() << endl;

  for (int i = 0; i < NUMS_TO_GENERATE; ++i) {
    std::thread t3(addToList, vec1[i], std::ref(list1));
    std::thread t4(addToList, vec2[i], std::ref(list1));
    t3.join();
    t4.join();
  }

  cout << "list size = " << list1.size() << endl;

  return EXIT_SUCCESS;
}

출력:

1000000, 1000000
list size = 2000000

이전 코드 조각에서 for 루프의 모든 반복에서 두 개의 개별 스레드를 만들고 동일한 주기의 기본 스레드에 결합하도록 선택했습니다. 이 시나리오는 스레드를 생성하고 파괴하는 데 귀중한 실행 시간이 걸리므로 비효율적이지만 mutex 사용법을 보여주기 위해서만 제공합니다.

일반적으로 프로그램에서 임의의 시간 동안 여러 스레드를 관리해야 하는 경우 스레드 풀 개념을 활용합니다. 이 개념의 가장 간단한 형태는 작업 루틴이 시작될 때 고정된 수의 스레드를 생성한 다음 대기열과 같은 방식으로 작업 단위를 할당하기 시작합니다. 한 스레드가 작업 단위를 완료하면 보류 중인 다음 작업 단위에 다시 사용할 수 있습니다. 하지만 우리의 드라이버 코드는 프로그램에서 다중 스레드 워크플로의 간단한 시뮬레이션으로만 설계되었습니다.

작가: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

Founder of DelftStack.com. Jinku has worked in the robotics and automotive industries for over 8 years. He sharpened his coding skills when he needed to do the automatic testing, data collection from remote servers and report creation from the endurance test. He is from an electrical/electronics engineering background but has expanded his interest to embedded electronics, embedded programming and front-/back-end programming.

LinkedIn Facebook