1-1. What is Concurrency?

C++ Concurrency In Action 2016.05.30 21:19

<C++ cuncurrency in Action Practical Multi-threading>


Chapter 1. Hello! World of Concurrency in C++ ! 

@translater: nolleh

(# 역자주, 의역)



이번 챕터에서는 

* 동시성과 멀티스레딩의 의미

* 왜 동시성과 멀티스레딩을 필요로 할까 

* C++ 동시성 지원의 역사

* C++ 프로그램의 간단한 멀티스레딩


C++ 표준이 공개된 1998년으로부터 13년이 지난 지금, C++ 표준 커미티(# 작성자들)는 언어를 지원하고 그것의 지원라이브러리를 주요한 점검대상(overhaul)으로 제공하고 있습니다. 

새로운 C++ 표준 ( C++ 11 or C++0x ) 은 2011년에 공개되었고 이것의 모든 변화의 묶음(swathe)은 C++ 의 사용을 좀더 쉽고 생산적으로 만들어 주었습니다.


C++ 11 표준에서 가장 중요한 특징은 multi_threded 프로그램에 대한 지원입니다. 

처음으로 C++ 표준이 multi_threded 어플리케이션의 존재를 인정하고 라이브러리의 컴포턴트로 multi_threded 어플리케이션을 쓰기 위한 방법을 제공한 것입니다.

이 지원은 플랫폼에 의존적인 확장기능에 의지하지 않고 multi-thread 프로그램을 작성하는데 도움을 주며 따라서 호환적인 multi_threded 코드를 믿을 수 있는 동작을 보장하며 작성하게 해줍니다.

또한 프로그래머가 어플리케이션의 성능을 향상시키기 위해 일반적인 동시성을,

특히 multi-threaded 프로그래밍에 더욱 더 유의할때 등장하게 됩니다. 

(# 성능향상을 위해 multi-threaded 프로그래밍을 이용할때 사용하게 됩니다.) 


이 책에서는 C++로 다중스레드를 이용한 동시성 프로그램을 다루며 그것들을 가능케 하는 C++언어 특징과 라이브러리 기능들을 다룹니다. 동시성과 멀티스레딩이 무엇을 의미하는지부터 시작하여 당신의 어플리케이션에서 왜 동시성을 추구해야 하는지에 대한 내용부터 다룹니다. 

왜 당신이 동시성을 사용하지 않기를 원할 것인가에 대한 내용을 살펴보고 C++ 의 동시성지원에 대한 overview를 제공하고 C++ 동시성의 간단한 예제로 이번 챕터를 마무리하겠습니다. 이미 멀티스레딩 개발 경험이 있는 독자는 이 섹션을 넘어가도 좋습니다. 다음 챕터들에서는 더 확장된 예제, 라이브러리기능들을 더 깊이 있게 다룹니다. 이 책은 멀티스레딩과 동시성을 위한 C++표준 라이브러리에 대한 레퍼런스(찾아보기)들과 함께 마무리가 됩니다. 

그럼, 동시성과 멀티스레딩이란 어떤걸 의미하는 걸까요 ?



1.1 What is concurrency?

가장 간단하고 기초적인 수준에서 동시성은 두개 이상의 분리된 활동들이 동시에 일어나는 것에 대한 것입니다. 우리는 삶의 일부로 동시성을 마주하고 있죠;우리는 이야기하며 걸을 수 있고, 각각의 손으로 다른 일을 할 수 있으며, 다른사람과 함께,독립적으로 각각 살고 있습니다-내가 수영하는 동안에 당신은 football 게임을 관전할 수 있죠. 그외 등등. 



1.1.1 concurrency in computer systems

컴퓨터의 용어로 동시성이란, 평행하게 하나의 시스템에서 동작하고 있는 다중의 독립된 활동입니다. (순차적으로 하나 수행후 다른 하나를 수행하는게 아니라!) 이건 새로운 현상이 아닙니다.: OS 의 multitasking 은 하나의 컴퓨터에서 다수의 어플리케이션이 동시에 task switching을 통해 동작할 수 있게 하는데, 수년간 흔히 자리잡아온 기능이며 진정한 의미의 동시성을 제공하는 다중 프로세서 high-end 서버머신의 공급은 심지어 더 오래된 이야기입니다. 새로운 것은 복수의 태스크를 그렇게 하는 척이 아닌 정말로 동시에 동작하게 하는 컴퓨터의 보급입니다. 


역사적으로, 대부분의 컴퓨터는 하나의 처리기만을 갖고 있었으며 오늘날의 컴퓨터들도 많은 수가 그렇습니다. 

이런 머신들은 정말로 하나의 테스크만을 한 순간에 수행할 수 있는 대신, 테스크를 1초에 여러번 스위칭 할 수 있습니다. 


한 조각에서는 하나의 테스크를 하고, 그리고 다른 조각에서는 다른 테스크를 처리하고.이 반복은 테스크가 동시에 수행되는 것처럼 보입니다. 이 기법은 task switching 이라 불립니다. 


우리는 이런 시스템에서도 동시성이 있다라고 여전히 얘기할 수 있습니다; 테스크 스위치가 정말 빨라서, 처리기가 다른 것으로 스위치를 할 때 어떤 지점에서 하나의 테스크가 일시 정지한다.라고 말할 수 없기 때문입니다.


테스크 스위칭은 사용자에게도, 어플리케이션 자체에 대해서도 동시성에 대한 착각을 불러일으킵니다. 동시성에대한 환상만이 있기 때문에 단독 처리기의 테스크 스위칭 환경에서 실행하는 어플리케이션의 동작은 진짜 동시성이 제공되는 환경에서의 동작과 갑자기 다를 수 있습니다. 

특히, 메모리 모델에 대한(chapter 5) 잘못된 추측은 이런 환경에서 문제로 대두되지 않을 수 있습니다. 이것은 chapter 10에서 더 깊이 다뤄봅니다.



다수의 처리기를 보유한 컴퓨터는 서버에서 사용되어왔고 고성등의 컴퓨팅 테스크를 처리하기 위해 수년간 사용되어 왔습니다. 그리고 현재 하나의 칩에 하나이상의 코어가 있는 처리기(multicore processors)를 사용하는 컴퓨터는 점차 흔한 데스크탑으로 자리잡고 있습니다. 

다수의 처리기를 보유하든, 다수의 코어를 하나의 프로세서에 보유하든 (혹은 둘다이든), 이 컴퓨터들은 정말로 병렬적으로 task를 처리하기위한 능력을 갖고 있습니다. 우리는 이것을 hardware concurrency라고 일컫습니다.


Figure 1.1 은 두 테스크를 10개의 동일한 사이즈로 조각 내어 동작시키는 컴퓨터의 이상적인 시나리오를 보여주고 있습니다. 듀얼 코어 머신에서는 각각의 테스크가 하나씩 코어를 사용하여 실행될 수 있습니다. 테스크 스위칭을 사용하는 하나의 코어 머신은 각각의 테스크의 조각을 끼워 넣게 됩니다. 

하지만 테스크 조각만이 아니라, 아무것도 하지 않는 공간 역시 추가 되게 됩니다. (다이어그램에서 각 조각사이에서 구분 선이 듀얼 코어의 그것보다 더 두껍게 보여지고 있다.); 이 interleaving(끼워넣기)을 위해서, 하나의 테스크에서 다른 테스크로 전환할 때마다 시스템은 문맥교환을 필요로 하고, 

이것에 시간이 들기 때문입니다. 

문맥 교환을 수행하기 위해서 OS는 CPU 상대의 저장/현재 동작중이던 테스크의 포인터 제공/어떤 테스크로 스위치 될지 확인한 뒤 해당 테스크의 CPU 상태를 reload 합니다.


그 후, 잠재적으로 메모리에 명령(instructions)과 데이터들을 새로운 테스크를 위해 로드하고 CPU 가 명령을 수행하는 데 있어서 지연되는 일이 없도록 캐쉬에 담아야 할 것입니다. (# 메모리에 정보들을 올린뒤 캐쉬에도 올립니다.) 


하드웨어의 동시성이 가능한 프로세서들에서라도 어떤 처리기들은 하나의 코어에서 복수개의 스레드를 실행할 수 있습니다. 이 결정을 위한 중요 고려 요소는 hardware threads의 '수' 입니다 : 얼마나 많은 독립된 테스크를 하드웨어가 정말로 동시성을 보장하며 수행할 수 있는가가 기준인겁니다.


하드웨어 동시성을 보장하는 시스템이라도 병렬적으로 수행 할 수 있는 수 이상으로 테스크가 많을 수 있으며 이때에는 테스크 스위칭을 이용하게 됩니다. 


예를들면, 전형적인 데스크탑 컴퓨터는 평소에는 많은 작업이 없지만 백그라운드 동작을 수행하면서 수백개의 테스크를 동작시킬수 있습니다. 백그라운드 테스크를 동작시키게하는 것이 바로 테스크스위치이며 당신이 워드 프로세서, 컴파일러, 에디터, 웹브라우져를 ( 등등 ) 한번에 이용하게 합니다. 

Figure 1.2 는 4개의 테스크가 듀얼코어 머신에서 이상적인 시나리오로 같은 사이즈 조각으로 나누어 테스크스위칭을 하는 것을 보여주고 있습니다. 실제상황에서는 많은 이슈들이 분할이 동등하게 이뤄지지 않도록 하며 스케쥴링 또한 정규적으로 이뤄지지 않게 합니다. 이런 이슈들의 일부는 Chapter 8에서 동시성 코드의 성능에 영향을 주는 요소를 다룰때 함께 다루고 있습니다.


이 책에서 다뤄지는 모든 테크닉, 함수, 클래스들은 당신의 어플리케이션이  싱글코어 프로세서에서 동작하든, 많은 멀티코어 프로세서에서 동작하든 동시성이 테스크 스위칭을 통해 이뤄지든, 하드웨어 동시성을 통해 이뤄지든 영향받지 않고 이용될 수 있습니다. 

하지만 당신이 상상할수 있듯 당신의 어플리케이션에서 어떻게 동시성을 이용하게 구현할지는 하드웨어 동시성의 여부에 따라 의존될 수도 있습니다. 이런 내용은 chapter 8에서 다루며 C++의 코드에서 동시성을 디자인하는 내용과 함께 다루고 있습니다.

 

1.1.2 Approches to concurrency

프로그래머 한 쌍이 소프트웨어 프로젝트에 함께 일하는 상황을 상상해 봅시다. 만약 당신의 개발자가 다른 사무실에 있다면 둘은 각각의 작업을 서로 방해 받지 않으며 각각 자신의 작업을 레퍼런스 메뉴얼을 작성하며 분리해서 진행할 것입니다. 하지만 커뮤니케이션은 쉽지 않겠죠; 의자를 돌려서 서로 이야기하는 것이 아니라 핸드폰과 이메일로, 사무실로 걸어가서 소통해야 할 테니까요. 또한, 두 사무실이라는 오버헤드, 중복된 레퍼런스 메뉴얼의 카피라는 문제도 있습니다.


이제 당신의 짝이 같은 사무실로 이사를 왔다고 상상해봅시다. 이제 서로 자유롭게 어플리케이션의 디자인에 대해서 토론하며 쉽게 종이와 화이트보드에 디자인 아이디어와 설명을 위해 다이어그램을 그릴수도있습니다. 이제 당신은 하나의 사무실만 관리하면되며 하나의 집합의 리소스들만 있으면 대부분 충분합니다. 부정적인 측면으로는, 당신은 아마도 집중하기 힘들수도 있고 리소스를 공유하는게 문제가 될 수도 있겠습니다. ( 레퍼런스 메뉴얼 어디간거야 ? )


당신의 짝 개발자를 조직하는 이 두가지 방법이 동시성에 대한 두가지 기본적인 접근을 설명합니다. 각각의 개발자는 스레드를 의미하며, 각각의 사무실은 프로세스를 의미합니다. 첫번째 접근은 복수의 싱글스레드 프로세스를 나타내며 이건 각각의 개발자가 하나의 사무실을 보유하는 것과 비슷합니다. 그리고 두번째 접근은 다중스레드 싱글프로세스를 나타내며 이건 두 개발자를 같은 사무실에 두는것과 비슷합니다.


당신은 이 임의의 방식을 결합할수있고 멀티스레디드나 싱글스레디드인 복수 프로세스를 가질수도 있습니다. 하지만 원리는 같습니다. 이제 간략하게 동시성에 대한 두 접근을 어플리케이션에서 보도록 합시다.


Concurrency With Multiple Processes

하나의 어플리케이션에서 동시성을 만드는 첫번째 방법은 어플리케이션을 다수의 분리된 싱글스레디드 프로세스들로 나누는 것이며 이 프로세스들은 동시에 동작하여 당신이 웹브라우져와 워드 프로세서를 동시에 사용하는 것과 같습니다. 

이 분리된 프로세스들은 메시지를 서로 normal interprocess communication channels ( signals, sockets, files, pipes, and so on ) 을 이용하여 Figure 1.3 처럼 교환할 수 있습니다. 

이런 프로세스간의 통신의 하나의 단점은 운영체제가 다른 프로세스의 데이터를 실수로 수정하는 일이 없도록 프로세스간의 보안시스템이 구축되어있기 때문에 종종 구축에 복잡할 수 있고 느릴수도 있다는 것입니다. 다른 단점은, 다중 프로세스 구동에서의 상속된 오버헤드 입니다.: 프로세스를 실행하기 위해서는 운영체제가 관리에 필요한 리소스를 할당하는 등의 시간이 소요됩니다. 


물론, 단점만 있는 것은 아닙니다: 운영체제의 추가된 보안은 일반적으로 프로세스와 고수준의 커뮤니케이션 메커니즘사이에 더 쉽게 안전한 동시성 코드를 제공합니다. (스레드 보다) 

정말로, Erlang 프로그래밍 언어에서 제공되는 환경은 좋은 효과를 내기 위해 프로세스를 동시성의 근본 구축 블록으로 사용합니다.


Concurrency with multiple threads

다른 동시성에 대한 접근은 다중 스레드를 하나의 프로세스에서 구동시키는 겁니다. 스레드들은 가벼운 프로세스들과 비슷한 개념입니다: 각각의 스레드는 다른 스레드와 독립적으로 구동되며 다른 명령들의 시퀀스로 동작합니다. 

하지만 모든 스레드들은 프로세스에서 같은 주소공간을 공유하며 대부분의 데이터는 모든 스레드에서 직접적으로 접근될수 있습니다- 전역 변수는 전역적으로 남아있으며 객체에대한 포인터나 레퍼런드, 데이터는 스레드 사이에서 전달될 수 있게 됩니다. 


종종 프로세스들 사이에서 메모리를 공유하는 것은 가능하지만 같은 데이터에 대한 메모리 주소가 다른 프로세스에서는 필요치 않기 때문에 이건 종종 복잡하고 관리하기 어려울 수도 있습니다. Figure 1.4 에서는 하나의 프로세스에 존재하는 두 스레드가 공유된 메모리를 통하여 통신하는 모습을 보여주고 있습니다.


공유된 주소공간과 데이터 보호의 부재는 멀티플 스레드를 사용하는 데 필요한 오버헤드를 다중 프로세스를 사용하는 데 필요한 오버헤드와 비교했을때 운영체제가 기록할(bookkeeping) 필요가 더 적기 때문에 상당히 작습니다. 

하지만 공유된 메모리의 유연성은 비용으로 다가 올 수도 있습니다. 만약 데이터가 다중 스레드에서 접근이 될때, 어플리케이션 프로그래머가 보여지는 데이터의 뷰를 각각의 스레드가 접근할 때마다 일관성을 유지해야하기 때문입니다. 이 스레드간 데이터 공유를 둘러싼 이슈와 피하기 위한 가이드라인들은 이 책의 3,4,5,8 chapter 에서 다루고 있습니다. 

이 문제는 코드를 작성할때의 적절한 관심으로 극복할 수 있으며, 여기서 말한 적절한 관심이, 그렇다고해서 엄청난 비용을 스레드 간에 통신을 위해 필요로 한다는 것을 의미하는 것은 아닙니다.


론칭과 통신에 관계된 상대적으로 멀티프로세스에 비해 적은 오버헤드는 잠재적인 공유 메모리 문제에도 불구하고 C++ 언어를 포함하여 선호되는 접근방법이라는 것을 의미합니다. 


더하여, C++ 표준은 프로세스간의 통신을 위한 어떤 내장된 지원도 하고 있지 않으므로 다중 프로세스를 이용하는 어플리케이션은 결국 플랫폼에 특화된 API 들에 의존해야 합니다. 

이 책은 멀티스레딩만을 배제적으로 다룰 것이며 앞으로의 동시성에 대한 레퍼런스들 또한 다중 스레드를 통해 성취될 수 있다고 가정합니다.

동시성에 대한 의미를 갖고 이제 왜 당신의 어플리케이션에서 동시성을 사용해야 하는지 살펴봅시다.

저작자 표시 비영리 변경 금지
신고

'C++ Concurrency In Action' 카테고리의 다른 글

1-1. What is Concurrency?  (2) 2016.05.30

Tutorial 5. Synchronising handlers in multithreaded programs

Programming/Boost asio 2015.04.11 23:00

다음 내용에 기반함 

http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/tutorial/tuttimer5.html


이번 튜토리얼 에서는 boost::asio::strand 클래스를 사용하여 멀티스레드 프로그램에서 콜백 핸들러를 동기화하는 방법을 알아보자.

이전 4개의 튜토리얼에서는 io_service::run() 함수를 오로지 하나의 스레드에서만 호출하였기 때문에 핸들러 동기화 이슈를 피할수 있었다. ( asio 라이브러리는 콜백 핸들러들이 io_service::run() 을 호출한 스레드에서만 실행될 수 있도록 보장해준다. 거기에, io_service::run() 을 하나의 스레드에서만 부름으로써, 동시에 실행되지 않는 것을 보장받은 것이다.) 


스레드를 하나만 이용하여 접근하는 것은 asio 를 이용한 어플리케이션을 시작하기에 좋았지만, 서버와 같은 프로그램에서 다음과 같은 제한이 존재한다는 것이다. 


* 핸들러가 많은 시간을 소요할 경우 응답성이 떨어진다.

* 멀티 프로세서 시스템에서 확장성이 없다.


이 문제를 해결하기 위해, io_service::run() 을 스레드 풀에서 호출하는 것으로 차선책을 찾아볼 수 있다.

그러나, 이렇게 되어 핸들러가 동시에 실행되는 것이 가능하게 되어, 스레드 safe 하지 않은 공용되는 데이터에 접근하는 핸들러들을 동기화하는 방법이 필요하게 된다.


이전 튜토리얼과 마찬가지로 Printer 를 정의하는 것에서부터 시작해보자. 타이머를 동시에 두개 돌리는 것으로 확장해볼 것이다.

class printer
{
public:


여기에 boost::asio::deadline_timer 멤버 쌍을 초기화 하는 코드와 boost::asio::strand 타입인 stand_ 멤버를 초기화하는 코드를 추가한다. strand 를 통해 디스패치 되는 핸들러들이 다음에 호출 되는 핸들러가 시작되기 전에 끝나는 것을 보장한다. 

이건 몇개의 스레드가 io_service::run() 을 호출 했냐와는 관계가 없다. 물론, 핸들러들은 동시 다발적으로 다른 핸들러들에서 실행 되어 boost::asio::strand 로 인해 디스패치 되지 않을 것이며, 다른 boost::asio::strand 객체를 통해 디스패치 되지 않을 것이다. 


  printer(boost::asio::io_service& io)
    : strand_(io),
      timer1_(io, boost::posix_time::seconds(1)),
      timer2_(io, boost::posix_time::seconds(1)),
      count_(0)
  {


비동기 동작들을 초기화 할때, 각각의 콜백들은 boost::asio::strand 객체를 통해 "wrapped" 된다. strand::wrap() 함수는 핸들러를 포함한 객체를 boost::asio::strand 객체를 통해 새롭게 반환한다. 핸들러를 동일한 boost::asio::strand 를 통해 'wrapping' 함으로써, 동시에 실행되지 않음을 보장할 수 있다.


    timer1_.async_wait(strand_.wrap(boost::bind(&printer::print1, this)));
    timer2_.async_wait(strand_.wrap(boost::bind(&printer::print2, this)));
  }

  ~printer()
  {
    std::cout << "Final count is " << count_ << "\n";
  }


멀티스레드 프로그램에서, 비동기 동작들은 공유 리소스에 접근할때는 동기화가 이루어 져야한다. 이 튜토리얼에서는, 핸들러 (print1 과 print2 ) 에서 공유되는 리소스가 std::cout 과 count_ 라고 하자.

  void print1()
  {
    if (count_ < 10)
    {
      std::cout << "Timer 1: " << count_ << "\n";
      ++count_;

      timer1_.expires_at(timer1_.expires_at() + boost::posix_time::seconds(1));
      timer1_.async_wait(strand_.wrap(boost::bind(&printer::print1, this)));
    }
  }

  void print2()
  {
    if (count_ < 10)
    {
      std::cout << "Timer 2: " << count_ << "\n";
      ++count_;

      timer2_.expires_at(timer2_.expires_at() + boost::posix_time::seconds(1));
      timer2_.async_wait(strand_.wrap(boost::bind(&printer::print2, this)));
    }
  }

private:
  boost::asio::io_service::strand strand_;
  boost::asio::deadline_timer timer1_;
  boost::asio::deadline_timer timer2_;
  int count_;
};


메인 함수에서는 이제 io_service::run() 이 두개의 스레드에서 호출되게 된다 : 하나는 메인스레드, 하나는 추가된 스레드에서 - 이것은 boost::thread 객체를 통해 이루어진다. 


싱글스레드에서 그랬던 것처럼 io_service::run() 에 대한 동시적 호출은 work 가 가 남아 있는 동안 실행되게 된다. 백그라운드의 스레드는 비동기 작업이 완료되기 전까지 완료되지 않는다.


int main()
{
  boost::asio::io_service io;
  printer p(io);
  boost::thread t(boost::bind(&boost::asio::io_service::run, &io));
  io.run();
  t.join();

  return 0;
}


full-source code



사용 코드 - main


실행 결과




저작자 표시 비영리 변경 금지
신고

Tutorial 4. member function handler

Programming/Boost asio 2015.03.17 23:42

다음에 기반함


http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/tutorial/tuttimer4.html


이전 튜토리얼과 달라진 점은 클래스로 교체하면서 timer 와 count 를 멤버변수로 변경하고, 이에 따라 불필요해진 timer 와 count 함수인자를 제거했다. 

(이 튜토리얼 과정을 정확히 인지하지 않은채로 포스팅하다보니 이번 포스팅에서 배우는 내용인 멤버함수의 bind 호출과 같은 내용이 이미 소개가 되어 큰 의미가 없는 튜토리얼이 됐다. 

그냥 흘깃 넘어가자.)


MemberHandler


 
main 함수


결과 




저작자 표시 비영리 변경 금지
신고


티스토리 툴바