Study/WIL(Weekly I Learned)

크래프톤 정글 - 5-2Week 24.02.15 - 24.02.21 부제 : Malloc-lab

에린_1 2024. 2. 22. 19:45
728x90

5-2Week 24.02.15 - 24.02.21

대략 어지러운 나

회고

길고 긴 5주차가 끝났다. 무엇인가 많이 공부한 것 같지만 그래서 니가 정확히 뭐뭐했어? 라고 물어본다면 음.. 뭔가 한 것만 많은 듯한 느낌.

슬슬 공부의 루틴이 잡히는 것은 좋은 것같지만 공부의 난이도가 점점 올라가면서 내가 과연 제대로 소화를 하고 있는건가? 하는 생각이 든다. 스트레스를 받지 않는다고 생각을 했는데, 과자를 엄청나게 충동구매 한 것을 보아하니 스트레스를 알게 모르게 받았던 것같다.

나랑 같이 공부하는 사람들이 비슷비슷한 고민을 할 것이라고 생각한다. 공부할 것은 많고, 제대로 소화가 되는지는 모르겠고, 미래 취업이나 그런 여러것들은 어떻게 될지 모르겠고. 어렵다 어려워

근데 막상 생각해보면 삶이라는게 이런 불확정적인 여러 요소들중 내 선택으로 야무지게 바꿔가는것이라고 생각하면, 지금 내가 하고 있는 이 공부가 좋은 선효과를 내고 있지 않을까 생각이 든다.

os때는 더 힘들고, 나만무때는 더더더더 힘들다고 하는데, 기대도 되고 걱정도 되고 여러 생각이 마구마구 떠오른다. 그래도 재미있으니까 할만하다.

누군가 나에게 KraftonJungle을 고민하고 있다고 말하면, 나는 고민하지 않고 ‘고!!’ 라고 추천할 것 같다. 공부할 준비만 되어있다면 말이지.

아무튼 이번주도 고생많았고 다음주는 네트워크, 서버 관련이다. 여기서 더 잘 들어야 앞으로 공부할 때, 지식의 구멍이 없이 더 잘 받아드릴 수 있지 않을까 생각을 한다.

파이팅이야 파이팅~

CSAPP

11. 네트워크 프로그래밍

  • 모든 네트워크 응용들은 동일한 기본적인 프로그래밍 모델에 기초하고 있으며, 이들은 비슷한 전체 논리구조를 가지고 동일한 인터페이스를 사용한다.

11.1 클라이언트 - 서버 프로그래밍 모델

  • 모든 네트워크 응용프로그램은 클라이언트 - 서버 모델에 기초하고 있다. 이 모델을 사용해서 응용은 한 개의 서버 프로세스와 한개 이상의 클라이언트 프로세스로 구성된다. 서버는 일부 리소스를 관리하고, 이 리소스를 조작해서 클라이언트를 위한 일부 서비스를 제공한다.
  • 클라이언트 - 서버 모델에서 근본적인 연산은 트랜잭션이다. 클라이언트 - 서버 트랜잭션은 네 단계로 구성된다
    1. 클라이언트가 서비스를 필요로 할 때, 클라이언트는 한 개의 요청을 서버에 보내는 것으로 트랜잭션을 개시한다.
    2. 서버는 요청을 받고, 해석하고 , 자신의 자원들을 적절한 방법으로 조작한다.
    3. 서버는 응답을 클라이언트로 보내고, 그 후에 다음 요청을 기다린다.
    4. 클라이언트는 응답을 받고 이것을 처리한다.
  • 클라이언트와 서버는 프로세스이며, 머신이나 호스트가 아니다. 한 개의 호스트는 서로 다른 많은 클라이언트와 서버를 동시에 실행할 수 있으며, 클라이언트와 서버 트랜잭션은 동일하거나 다른 호스트에 존재 할 수 있다.

11.2 네트워크

  • 클라이언트와 서버는 종종 별도의 호스트에서 돌아가며, 컴퓨터 네트워크의 하드웨어 및 소프트 웨어 자원을 사용해서 통신한다. 호스트에게 네트워크는 단지 또 다른 I/O 디바이스이며, 데이터를 위한 소스와 싱크로 서비스한다. I/O 버스의 확장 슬롯에 꽂혀있는 어댑터는 네트워크에 물리적인 인터페이스를 제공한다. 네트워크에서 수신한 데이터는 I/O와 메모리 버스를 거쳐서 어댑터에서 메모리로, 대개 DMA 전송으로 복사된다. 비슷하게 데이터는 또한 메모리에서 네트워크로 복사 될 수 있다.
  • 물리적으로 네트워크는 기하적으로 구성된 계층구조 시스템이다. 하위수준은 LAN(local area network)으로 빌딩이나 캠퍼스에 설치된다. 가장 대중적인 LAN 기술은 현재 이더넷이다.
  • 이더넷 세그먼트는 몇 개의 전선들과 허브라고 부르는 작은 상자로 구성된다. 각 전선은 동일한 최대 비트 대역폭을 가지며, 한쪽 끝은 호스트의 어댑터 한쪽은 허브의 포트에 연결된다. 허브는 각 포트에서 수신한 비트를 종속적으로 다른 모든 포트로 복사한다.
  • 각 이더넷 어댑터는 어댑터의 비휘발성 메모리에 저장된 전체적으로 고유한 48 비트주소를 가진다. 호스트는 프레임이라고 부르는 비트들을 세그먼트의 다른 호스트에 보낼 수 있다. 각 프레임은 프레임의 소스와 목적지, 프레임의 길이를 식별할 수 있는 고정된 헤더 비트를 가지고 있으며, 그 뒤에 데이터 비트가 이어진다. 모든 호스트 어댑터는 이 프레임을 볼 수 있지만 목적지 호스트만이 실제로 이것을 읽어들인다.
  • 전선들과 브릿지라고 하는 작은 상자들을 사용해서 다수의 이더넷 세그먼트가 연결되어 브릿지형 이더넷이라고 하는 더 큰 LAN을 구성할 수 있다. 브릿지형 이더넷에서 일부 선은 브릿지를 브릿지로 연결하고, 다른 선들은 브릿지를 허브로 연결한다. 각 선의 대역폭은 다를 수 있다.
  • 브릿지는 허브보다 더 높은 전선의 대역폭을 가진다. 우수한 분산 알고리즘을 사용해서 이들은 자동으로 어떤 호스트가 어떤 포트에서 도달 가능한지 장기간에 걸쳐 학습하고, 그 후 필요한 경우 선택적으로 하나의 포트에서 다른 포트로 프레임을 복사한다.
  • 계층구조의 상부에서 다수의 비호환성 LAN들은 라우터라고 부르는 특별한 컴퓨터에 의해서 연결될 수 있으며, 라우터는 네트워크간 연결을 구성한다.(상호연결 네트워크) 각 라우터는 이들이 연결되는 각 네트워크에 대해 어댑터(포트)를 가지고 있다. 라우터는 고속의 point-to-point 전화 연결을 할 수 있으며, 이들은 WAN이라고 하는 네트워크의 사례이다. 일반적으로는 라우터는 임의의 LAN과 WAN들로부터 internet을 만들기 위해서 사용될 수 있다.
  • internet의 중요한 특성은 이것이 매우 다르고 비호환적인 기술을 갖는 여러가지 LAN과 WAN들로 이루어져 있다는 점이다. 비호환적인 네트워크들을 지나 데이터 비트를 다른 목적지 호스트로 전송할 수 있는 이유는 여러가지 네트워크간의 차이를 줄여주는 각 호스트와 라우터에서 돌고 있는 프로토콜 소프트웨어 계층 때문이다. 이 프로토콜은 두 가지 기본 기능을 제공해야 한다.
    • 명명법(Naming Scheme) : 서로 다른 LAN 기술은 주소를 호스트에 할당하는 서로 다른 비호환성을 갖는 방법을 사용한다. internet 프로토콜은 호스트 주소를 위한 통일된 포맷을 정의해서 이 차이점을 줄인다. 각 호스트는 자신을 유일하게 식별하는 internet 주소 최소한 한 개가 할당된다.
    • 전달기법(Delivery mechanism) : 서로 다른 네트워킹 기술은 서로 다른 비호환성을 갖는 비트 인코딩 방법과 프레임 내에 이들을 패키징하는 방법을 가지고 있다. internet 프로토콜은 데이터 비트를 패킷이라고 부르는 비연속적인 단위로 묶는 통일된 방법을 정의해서 이 차이점을 줄인다. 패킷은 패킷 크기와 소스 및 목적지 호스트 주소를 포함하는 헤더와 데이터 비트를 포함하는 데이터로 구성된다.

11.3 글로벌 IP Internet

  • 각 인터넷 호스트 TCP/IP 프로토콜(transmission control protocol/internet protocol)을 구현하는 소프트웨어를 실행한다. 인터넷 클라이언트와 서버는 소켓 인터페이스와 unix I/O 함수들의 혼합을 사용해서 통신한다. 소켓 함수들은 일반적으로 시스템 콜들로 구현되는데, 이 시스템 콜은 커널에서 트랩을 발생시키며, TCP/IP 에서 다양한 커널모드 함수들을 호출한다.
  • TCP/IP는 실제로 프로토콜의 집합으로 각각 서로 다른 기능을 제공한다.
  • IP는 기본 명명법과 데이터그램이라고 하는 패킷을 한 인터넷 호스트에서 다른 호스트로 보낼 수 있는 배달 메커니즘을 제공한다. IP 메커니즘은 만일 데이터그램을 잃어버리거나 네트워크내에서 중복되는 경우에 복구하려고 노력하지 않는다는 점에서 안정적이지 못하다.
  • TCP는 IP위에 구현한 복잡한 프로토콜로 프로세스들 간에 완전한 양방향 연결을 제공한다.
  • 인터넷은 다음의 특징을 가지는 전 세계적인 호스트의 집합이다.
    • 호스트의 집합은 32비트 IP주소 집합에 매핑된다.
    • IP주소의 집합은 인터넷 도메인 네임이라고 부르는 식별자의 집합에 매핑된다.
    • 하나의 인터넷 호스트의 프로세스는 연결을 통해서 다른 인터넷 호스트의 프로세스와 통신 할 수 있다.

11.3.1 IP주소

  • IP주소는 비부호형 32비트 정수이다. 네트워크 프로그램은 IP주소를 IP주소 구조체에 저장한다.
  • 인터넷 호스트들이 서로 다른 호스트 바이트 순서를 가질 수 있기 때문에 TCP/IP는 네트워크 패킷 헤더에 포함되는 IP주소 같은 모든 정수형 데이터 아이템에 대해서 통일된 네트워크 바이트 순서(빅 엔디안 바이트 순서)를 정의한다. IP주소 구조체의 주소는 호스트 바이트 순서가 리틀 엔디안인 경우에도 항상 네트워크 바이트 순서(빅 엔디안)로 저장된다.
  • IP주소는 대개 사람들에게 dotted-decimal 표기라고 하는 형식으로 제시되며, 이것은 각 바이트가 10진수 값을 사용하고, 다른 바이트들과는 점을 사용해서 구분된다.
    • 128.2.194.242 는 0x8002c2f2의 dotted-decimal 표현이다.

11.3.2 인터넷 도메인 이름

  • 인터넷 클라이언트와 서버는 서로 통신할 때 IP주소를 사용한다. 그렇지만 크기가 큰 정수는 사람들이 기억하기 어려워서 인터넷에서는 도메인 이름들의 집합을 IP주소의 집합으로 매핑하는 메커니즘과 함께 사람들에게 친숙한 별도의 도메인 이름 집합을 정의한다.
  • 도메인 이름들의 집합은 계층구조를 형성하고 있으며, 각각의 도메인 이름은 계층구조에서 자신의 위치를 인코드한다. 계층구조는 트리로 나타낸다. 트리의 노드들은 루트로 돌아가는 경로로 형성되는 도메인 이름을 나타낸다. 서브트리는 서브 도메인이라고 한다. 계층구조의 첫 번째 단계는 이름이 없는 루트 노드이다. 다음 계층은 ICANN(internet coporation for assinged names and numbers)이라고 하는 비영리조직이 정의한 일 단계 도메인 이름의 집합이다.
    • ex) com, edu, gov, org, net
  • 다음 계층에는 2단계 도메인 이름이 포함되며 이들은 수많은 ICANN이 인정하는 대행사가 요청한 순서에 의해 그 이름이 할당된다. 일단 한 조직이 2단계 이름을 받은 후에는 서브 도메인 내에서는 어떤 새로운 이름도 자유롭게 생성할 수 있다.
  • 인터넷의 도메인 이름의 집합과 IP주소 집합 사이에 매핑을 정의한다. 매핑은 DNS(Domain Name System)라고 하는 전세계에 분산된 데이터 베이스에 의해 관리된다. 개념적으로 DNS 데이터 베이스는 수 백만 개의 호스트 엔트리로 구성되어 있으며, 이들 각각은 도메인 이름 집합과 IP주소들의 동일성(equivalence) 클래스로 생각할 수 있다.
  • 각 인터넷 호스트는 지역적으로 정의된 도메인 이름 localhost를 가지고 있으며, 이것은 항상 루프백 주소 127.0.0.1에 매핑된다.
  • localhost 이름은 같은 머신에서 돌고있는 클라이언트와 서버들을 참조하는 편리하고 포터블한 방법을 제공하며, 이것은 특히 디버깅시에 유용하다.
  • 가장 간단한 경우, 도메인 이름과 IP주소 사이에는 일대일 매핑이 존재한다. 그러나 일부 경우에, 다수의 도메인 이름이 동일한 IP주소에 매핑된다. 대부분의 일반적인 경우에, 다수의 도메인 이름들은 다수의 IP주소로 매핑될 수 있다. 일부 유효한 도메인 이름들은 어떤 IP주소에도 매핑되어 있지 않다는 것을 발견했다.

11.3.3 인터넷 연결

  • 인터넷 클라이언트와 서버는 연결을 통해서 바이트 스크림을 주고 받는 형식으로 통신한다. 이 연결은 두 개의 프로세스를 연결한다는 점에서 point-to-point 연결이다. 데이터가 동시에 양방향으로 흐를 수 있다는 의미에서 이것은 완전양방향(full-duplex)이다. 그리고 소스 프로세스가 보낸 바이트 스크림이 결국의 보낸것과 동일한 순서로 목적지 프로세스에서 수신된다는 의미에서 안정적이다.
  • 소켓(socket)은 연결의 종단점이다. 각 소켓은 인터넷 주소와 16비트 정수 포트로 이루어진 소켓 주소를 가지며, 이것은 address : port로 나타낸다. 클라이언트 소켓 주소 내의 포트는 클라이언트가 연결 요청을 할 때 커널이 자동으로 할당하며, 이것은 단기(ephemeral)포트라고 한다. 그러나 서버의 소켓 주소에 있는 포트는 대개 영구적으로 이 서비스에 연결되는 잘 알려진 포트다. 잘 알려진 포트를 갖는 각 서비스에 연관되어 이에 대응되는 잘 알려진 서비스 이름들이 존재한다. 잘 알려진 이름과 포트들 간의 매핑은 /etc/services 파일에 보관되어 있다.
  • 연결은 두 개의 종단점의 소켓 주소에 의해 유일하게 식별된다. 이 두개의 소켓 주소는 소켓 쌍이라고 알려져 있으며 tuple로 나타낸다.
  • (cliaddr : cliport , servaddr : servport) - 클라이언트 주소 : 클라이언트 포트, 서버 주소 : 서버 포트

11.4 소켓 인터페이스

  • 소켓 인터페이스는 네트워크 응용을 만들기 위한 Unix I/O 함수들과 함께 사용되는 함수들의 집합이다.

11.4.1 소켓 주소 구조체

  • 리눅스 커널으 관점에서 보면, 소켓은 통신을 위한 끝점이다. Unix 프로그램의 관점에서 보면 소켓은 해당 식별자를 가지는 열린 파일이다.

11.5 웹 서버

11.5.1 웹 기초

  • 웹 클라이언트와 서버는 HTTP(Hypertext Transfer Protocol) 라고 하는 텍스트 기반 응용 수준 프로토콜을 사용해서 상호연동한다. 웹 클라이언트(브라우저)는 서버로의 인터넷 연결을 오픈하고 컨텐츠를 요청한다. 서버는 요청한 컨텐츠로 응답하고, 그 후에 연결을 닫아준다. 브러우저는 컨텐츠를 읽고 이것은 스크린에 보여준다.

11.5.2 웹 컨텐츠

  • 웹 클라이언트와 서버에게, 컨텐츠는 연관된 MINE(Multipurpose Internet Mail Extensions) 타입을 갖는 배열이다.
  • 웹 서버는 두 가지 서로 다른 방법으로 클라이언트에게 컨텐츠를 제공한다.
    • 디스크 파일을 가져와서 그 내용을 클라이언트에게 보낸다. 디스크 파일은 정적 컨텐츠라고 하며, 파일을 클라이언트에게 돌려주는 작업 정적컨텐츠를 처리한다고 말한다.
    • 실행 파일을 돌리고 그 출력을 클라이언트에게 보낸다. 실행 파일이 런타임에 만든 출력을 동적 컨텐츠라고 하며, 프로그램을 실행하고 그 결과를 클라이언트에게 보내주는 과정을 동적 컨텐츠를 처리한다고 말한다.
  • 웹 서버가 리턴하는 모든 내용들은 서버가 관리하는 파일에 연관된다. 이 파일 각각은 URL(universal presource locator) 라고 하는 고유의 이름을 가진다.
  • 어떻게 서버가 URL 접미어를 해석하는지에 대해서 이해해야 할 몇 가지 사실들.
    • URL이 정적 또는 동적 컨텐츠를 참조하는지를 결정하기 위한 표준 규칙은 없다. 각각의 서버는 자신이 관리하는 파일들을 위한 자신만의 규칙들을 가진다.
    • 접미어 앞 부분의 ‘/’ 는 리눅스의 루트 디렉토리를 나타내는 것은 아니다. 오히려 이것은 어떤 종류의 컨텐츠가 요청되든 간에 홈 디렉토리를 나타낸다.
    • 최소한의 URL 은 ‘/’ 문자이면, 모든 서버는 이것을 /index.html 같은 특정 기본 홈페이지로 확장한다.

11.5.3 HTTP 트랜잭션

  • HTTP가 인터넷 연결 위로 전송된 텍스트 라인들을 기초하고 있기 때문에 리눅스 TELNET 프로그램을 사용해서 인터넷 상의 모든 웹 서버와 트랜잭션을 실행할 수 있다.
  • TELNET 프로그램은 연결을 통해서 텍스트 라인으로 클라이언트와 대화하는 서버를 디버그 하는데 매우 간편하다.

HTTP 요청

  • HTTP는 많은 서로 다른 메소드를 지원하며 여기에는 GET, POST, OPTIONS … 등 포함되어있다. 이중 GET은 전체 HTTP 요청의 대부분에 해당한다. GET 메소드는 서버에게 URI(uniform resource identifer)에 의해 식별되는 내용을 리턴 할 것을 지시한다. URI는 파일 이름과 옵션인 인자들을 포함하는 URL의 접미어다.
  • Host 헤더는 프록시 캐시에 의해 사용되며 ,이것은 때로는 브라우저와 요청된 파일을 관리하는 본래의 서버 사이의 중간자 역할을 한다. 하나의 클라이언트와 하나의 본래 서버 사이에는 소위 프록시 제안 내에서 다중 프록시가 존재 할 수 있다. 원점 서버의 도메인 이름을 식별하는 Host 헤더 내의 데이터는 프록시 체인의 중간에 있는 프록시가 요청한 컨텐츠의 지역적으로 캐시된 사본을 가질 수 있는지 결정할 수 있게 한다.

12. 동시성 프로그램

  • 논리적 제어흐름은 이들이 시간적으로 중첩되면 동시적이다. 이와 같은 현상을 동시성이라고 한다.
  • 응용수준 동시성은 다양한 경우 유용하다.
    • 느린 I/O 디바이스 접근하기 : 응용프로그램은 유용한 작업을 I/O요청과 겹치게 한다.
    • 사람들과 상호 작용하기 : 사용자가 어떤 동작을 요청할 때 마다, 이 동작을 수행하기 위해 별도의 동시성의 논리 흐름이 생성된다.
    • 작업을 지연시켜서 시간지연 줄이기 : 다른 동작을 지연시키고 이들을 동시에 수행해서 특정 동작 시간 지연을 축소하기 위해 동시성을 이용한다.
    • 다수의 네트워크 클라이언트 처리 : 클라이언트마다 별도의 논리흐름을 생성하는 동시성 서버
    • 멀티코어 머신에서 병렬로 계산하기
  • 동시성 프로그램을 만들기 위한 세 개의 기본 접근방법
    • 프로세스 : 각 논리적 흐름은 커널이 스케줄하고 관리하는 프로세스이다. 프로세스가 별도의 가상 주소공간을 가지기 때문에, 서로 통신하기를 원하는 흐름들은 모종의 명시적 프로세스간 통신(IPC : interprocess communication) 메커니즘을 사용해야 한다.
    • I/O 다중화 : 동시성 프로그램의 한 형태로, 응용들은 명시적으로 자신의 논리흐름을 한 개의 프로세스 컨텍스트 내에서 스케줄한다. 논리적 흐름들은 파일 식별자에 도착하는 데이터로 인해 메인 프로그램이 명시적으로 하나의 상태에서 다른 상태로 전환하는 상태머신으로 모델 할 수 있다. 프로그램이 한 개의 프로세스이므로 모든 흐름들은 동일한 주소공간을 공유한다.
    • 쓰레드 : 쓰레드는 한 개의 프로세스 컨텍스트에서 돌아가는 논리흐름으로 커널에 의해 스케줄된다. 쓰레드를 다른 두 개의 방식의 하이브리드 형태로 생각할 수 있다. 커널에 의해 스케줄되며 동일한 가상 주소공간을 공유한다.

12.1 프로세스를 사용한 동시성 프로그래밍

  • 동시성 프로그램을 만드는 가장 간단한 방법
    • ex)동시성 서버를 구현하는 방법은 부모에서 클라이언트 연결 요청을 수락하는 것이며, 그 후에 새로운 자식 프로세스를 생성해서 새로운 각각의 클라이언트를 서비스한다.

12.1.1 프로세스 기반 동시성 서버

  • 서버를 만드는 데 중요한 몇 가지 사항이 있다.
    • 첫째, 서버들은 대개 장시간 동안 돌아가므로 좀비 자식을 청소하는 SIGCHID 핸들러를 포함 해야 한다.
    • 둘째, 부모와 자식은 자신의 connfd사본을 닫아야 한다.
    • 마지막으로, 소켓의 파일 테이블 엔트리 내의 참조 횟수 때문에 클라이언트로의 연결은 부모와 자식의 connfd 사본이 모두 닫힐 때 까지 종료되지 않는다

12.1.2 프로세스의 장단점

  • 프로세스는 부모와 자식 사이에 상태 정보를 공유하는 것에 대해서 깔끔한 모델을 가지고 있다.
    • 파일은 공유되고 사용자 주소공간은 공유되지 않는다.
  • 프로세스들이 분리된 주소공간을 가지는 것은 장점이자 단점이다. 한 개의 프로세스가 우연히 다른 프로세스의 가상메모리에 쓰는 것은 불가능하며, 이로 인해서 많은 혼란스러운 오류들을 제거 할 수 있다.
  • 반면에 별도의 주소공간은 프로세스가 상태정보를 공유하는 것을 어렵게 한다. 정보를 공유하기 위해서 명시적인 IPC메커니즘을 사용해야 한다. 그리고 프로세스 제어와 IPC의 오버헤드가 크기 때문에 더 느려지는 경향이 있다.

12.2 I/O 다중화를 이용한 동시성 프로그래밍

  • 기본 아이디어는 select 함수를 사용해서 커널에게 이 프로세스를 정지할 것을 요구해서 한 개 이상의 I/O 이벤트가 발생한 후에만 응용에게 제어를 돌려준다.

12.2.1 I/O 다중화의 장단점

  • 한 가지 장점은 이벤트기반 설계로 프로그래머가 프로세스 기반 설계보다 자신의 프로그램을 더 잘 제어할 수 있다는 점이다.
  • 또 다른 장점은 I/O 다중화에 기초한 이벤트 기반 서버는 단일 프로세스의 컨텍스트에서 돌아가며, 그래서 모든 논리흐름은 프로세스의 전체 주소공간에 접근할 수 있다. 이 흐름들 간에 데이터 공유를 쉽게 해준다. GDB같은 친숙한 디버깅 도구를 이용해서 순차 프로그램에서 하는 것처럼 동시성 서버를 디버그 할 수 있다.
  • 마지막으로 이벤트 기반 설계는 대개 프로세스 기반 설계보다 훨씬 더 효율적이며, 그 이유는 이들이 새로운 흐름을 스케줄 하기 위해서 프로세스 문맥전환을 요구하지 않기 때문이다.
  • 이벤트 기반 설계의 한 가지 중요한 단점은 코딩 복잡도다. 프로세스 기반 서버보다 세 배 더 많은 코드를 필요로 한다. 불행히도 동시성의 크기가 감소하면 할수록 복잡성은 증가한다. 크기라는 것은 각 논리흐름이 타임 슬라이스 동안에 각 논리흐름이 실행하는 인스트럭션의 수를 의미한다.
  • 또 다른 중요한 단점은 이들이 멀티코어 프로세서를 완전히 활용할 수 없다는 것이다.

12.3 쓰레드를 이용한 동시성 프로그래밍

  • 쓰레드는 프로세스의 컨텍스트 내에서 돌아가는 논리흐름이다.
  • 쓰레드는 커널에 의해서 자동으로 스케줄된다. 각 쓰레드는 고유의 정수 쓰레드ID(TID), 스택, 스택 포인터, 프로그램 카운터, 범용 레지스터, 조건 코드를 포함하는 자신만의 쓰레드 컨텍스트를 가진다. 한 개의 프로세스에서 돌고 있는 모든 쓰레드는 이 프로세스의 전체 가상주소를 공유한다.

12.3.1 쓰레드 실행모델

  • 각 쓰레드는 메인 쓰레드라고 부르는 한 개의 쓰레드로 생명을 시작한다. 어던 시점에서 메인 스레드는 피어(Peer) 쓰레드를 생성하고, 이때부터 두 쓰레드가 동시에 돌아간다. 피어 쓰레드는 제어를 메인 쓰레드로 돌려주기 전에 잠시동안 실행하는 식으로 진행된다.
  • 쓰레드의 실행은 일부 중요한 부분에서 프로세스와 다르다. 쓰레드 컨텍스트가 프로세스 컨텍스트보다 훨씬 작기 때문에 쓰레드 문맥전환은 프로세스보다 빠르다. 또 프로세스와는 달리 쓰레드는 경직된 부모 - 자식 계층구조에서 구성되지 않았다. 하나의 프로세스에 연계된 쓰레드들은 피어들의 풀을 구성하고, 쓰레드는 이들과는 독립적으로 다른 쓰레드에 의해서 생성되었다. 메인 쓰레드는 항상 프로세스에서 돌아가는 첫 번째 쓰레드라는 의미에서만 다른 쓰레드와 구별된 피어의 풀에 관한 이 개념의 주요 영향은 쓰레드가 자신의 피어모드를 죽일 수 있거나 자신의 피어들이 종료하는 것을 기다릴 수 있다는 것이다. 또, 각 피어는 동일한 공유 데이터를 읽고 쓸 수 있다.

12.3.2 POSIX 쓰레드,

  • POSIX 쓰레드는 C프로그램에서 쓰레드를 조작하는 표준 인터페이스다. pthreads는 데이터를 피어 쓰레드와 안전하게 공유하기 위해서, 시스템 상태의 변화를 피어들에게 알리기 위해 프로그램이 쓰레드를 생성하고, 죽이고, 청소하도록하는 약 60개의 함수를 정의한다.

12.3.6 쓰레드 분리하기

  • 언제나 쓰레드는 연결가능하거나 분리되어 있다. 연결 가능한 쓰레드는 다른 쓰레드에 의해 청소되고 종료될 수 있다. 자신의 메모리 자원들은 다른 쓰레드에 의해 청소될 때 까지는 반환되지 않는다. 반대로, 분리된 쓰레드는 다른 쓰레드에 의해서 청소되거나 종료 될 수 없다. 자신의 메모리 자원들은 이 쓰레드가 종료할 때 시스템에 의해 자동으로 반환된다.
  • 기본적으로 쓰레드는 연결 가능하게 생성된다.

12.4 쓰레드 프로그램에서 공유변수

  • 변수는 다수의 쓰레드가 이 변수의 일부 인스턴스를 참조할 때만 공유된다.

12.4.1 쓰레드 메모리 모델

  • 동시성 쓰레드의 풀은 한 개 프로세스의 컨텍스트에서 돌아간다. 각각의 쓰레드는 자신만의 별도의 쓰레드 컨텍스트를 가진다. 각 쓰레드는 나머지 프로세스 컨텍스트를 다른 쓰레드와 공유한다. 여기에는 전체 사용자 가상 주소공간이 포함된다. 또한 동일한 오픈된 파일들을 공유한다.
  • 동작적인 측면에서, 하나의 쓰레드가 다른 쓰레드의 레지스터를 읽거나 쓰는것은 불가능한 반면, 모든 쓰레드는 공유 가상메모리 내의 모든 위치에 접근할 수 있다. 만일 어떤 쓰레드가 한 메모리 위치를 수정하면, 그 위치를 읽는 다른 모든 쓰레드는 결국 이 변경사항을 알 수 있게된다. 그래서 레지스터들은 절대 공유되지 않지만, 가상메모리는 항상 공유된다.
  • 별도의 쓰레드 스택을 위한 메모리 모델은 깔끔하지 않다. 이 스택들을 가상 주소공간의 스택 영역에 포함되어 있고, 대개 이들 각각의 쓰레드에 의해 독립적으로 접근된다. 항상이 아닌 대개라고 하는 이유는 서로 다른 쓰레드 스택이 다른 쓰레드로부터 보호되지 않기 때문이다. 그래서 만일 어떤 쓰레드가 다른 쓰레드의 스택을 가리키는 포인터를 획득하게 된다면, 이 스택의 모든 부분을 읽고 쓸 수 있다.

12.4.2 변수들을 메모리로 매핑하기

  • 쓰레드를 사용하는 C프로그램의 변수들은 이들의 저장클래스에 따라 가상메모리에 매핑된다.
  • 전역변수
    • 전역변수는 함수 밖에서 선언된 모든 변수를 말한다. 런타임에 가상메모리의 읽기/쓰기 영역은 쓰레드에 의해 참조될 수 있는 각각의 전역변수의 정확히 한 개의 인스턴스를 포함한다.
  • 지역 자동 변수
    • 지역 자동 변수는 함수 내에서 static특성 없이 선언된다. 런타임에 각 쓰레드의 스택은 자신만의 지역 자동 변수의 인스턴스를 가진다. 이것은 심지어 다수의 쓰레드가 동일한 쓰레드 루틴을 사용하는 경우에도 그렇다.
  • 지역 정적 변수
    • 지역 정적 변수는 함수 안에서 static 특성으로 선언된 변수다. 전역변수 처럼 가상메모리의 읽기/쓰기 영역은 프로그램에서 선언된 각 지역 정적 변수의 정확히 한 개의 인스턴스를 포함한다.

12.4.3 공유변수

  • 어떤 변수 v는 자신의 인스턴스중의 한 개가 하나 이상의 쓰레드에 의해 참조되는 경우에만 공유되어 있다고 말한다.

12.5 세마포어로 쓰레드 동기화하기

  • 공유변수들은 편리하지만 심각한 동기화 오류를 가져올 수 있다.
  • 일반적으로, 운영체제가 여러분의 쓰레드를 위해서 정확한 순서를 선택하게 될지 여부를 예측할 수 있는 방법은 없다. 이러한 부정확성을 갖는 인스트럭션 순서를 진행그래프라고 하는 도구를 이용해서 명확히 할 수 있다.

12.5.1 진행그래프(Progress Graph)

  • 진행그래프는 n개의 동시성 쓰레드를 n차원 직교 좌표공강을 지나는 궤적으로 모델링한다. 각각의 축 k는 쓰레드 k의 진행에 대응된다. 각각의 점은 쓰레드 k가 인스트럭션 Ik를 완료한 상태를 나타낸다. 그래프의 원점은 초기 상태를 나타내며, 쓰레드 모두가 아직 한 개의 인스트럭션도 완료하지 못한 상태이다.
  • 진행그래프는 인스트럭션의 실행을 하나의 상태에서 다른 상태로의 전환으로 모델링한다. 전환은 한 점에서 인접하는 한 점으로의 화살표로 표시한다. 합법적인 전환은 오른쪽으로 이동하거나 위로 이동하는 경우다. 두 인스트럭션은 동시에 완료할 수 없다. 프로그램이 뒤로 실행되는 경우는 없으며, 따라서 아래로 또는 왼쪽으로 이동하는 것은 규칙에 위배된다.
  • 쓰레드 i에 대해서 공유변수의 내용을 조작하는 인스트럭션(Li, Ui, Si)는 크리티컬 섹션을 형성하며 다른 크리티컬 섹션과 중첩되면 안된다. 각각의 쓰레드가 자신의 크리티컬 섹션 내의 인스트럭션들을 실행하는 동안에는 상호 배타적으로 공유변수를 접근하도록 보장하기를 원한다. 이 현상을 상호배제(mutual exclusion)라고 한다.
  • 진행그래프에서, 두 개의 크리티컬 섹션의 교차점은 위험영역이라는 상태공간의 구역을 정의한다. 위험영역은 둘레에 있는 상태들과 접해있지만 포함하지는 않는다는 점에 유의해라. 위험영역을 둘러싼 궤적은 안전궤적이라고 한다. 역으로, 위험영역의 어떤 부분이라도 닿은 궤적은 위험궤적이다.
  • 모든 안전궤적은 공유 카운터를 정확히 갱신하게 된다. 쓰레드 프로그램을 정확히 실행하도록 보장하려면 쓰레드들을 어떤 방식으로든 동기화해서 이들이 항상 안전궤적을 가지도록 해야한다. 고전적인 방법은 세마포어 개념에 기초한다.

12.5.2 세마포어

  • 세마포어 s는 비음수 정수값을 갖는 전역변수로 두 개의 특별한 연산인 P,V를 통해서만 조작할 수 있다.
  • P(s) : s가 0이 아니면 P는 s를 감소시키고 즉시 리턴한다. 만일 s가 0이면 이 쓰레드는 s가 0이 아닌 값을 가지고, 쓰레드가 V연산에 의해 재시작될 때까지 정지된다. 재시작후에 P연산은 s를 감소시키고 제어를 호출자에게 돌려준다.
  • V(s) : V연산은 s를 1증가 시킨다. 만일 s가 0이 아닌 값이 되는것을 기다리면서 P연산에서 멈춰있는 쓰레드가 있다면, V연산은 이중에서 정확히 한 개의 쓰레드를 시작하고, 그 후에 s를 감소시켜서 자신의 P연산을 완료한다.
  • P에서 테스트와 감소연산은 일단 세마포어 s가 0이 아니면 s의 감소가 중단없이 일어난다는 의미에서 개별적으로 일어난다. V에서 증가연산 또한 개별적으로 일어나는데, 그것은 이 연산이 세마포어를 중단없이 로드하고, 증가하고, 저장하기 때문이다. V의 정의가 기다리고 있는 쓰레드들이 재시작되는 순서를 정의하지 않는다는 것에 주목하라. 유일한 요구사항은 V가 정확히 한 개의 대기하는 쓰레드를 재시작해야 한다는 것이다. 그래서 여러개의 쓰레드가 하나의 세마포어를 기다리고 있을 때, 어떤것이 V의 결과로 재시작되는지는 예측 할 수 없다.
  • P와 V의 정의는 돌고 있는 프로그램이 적절히 초기화된 세마포어가 음수 값을 가지는 상태로 절대 들어갈 수 없도록 보장해준다. 이 특성을 세마포어 불변성이라고 하며, 동시성 프로그램의 궤적을 제어하기 위한 강력한 도구를 제공한다.

12.5.3 상호배제를 위한 세마포어 이용하기

  • 공유변수들을 상호배타적으로 접근하기 위한 방법. 하나의 세마포어 s를 초기값 1로 시작해서 각각의 공유변수에 연계하고 그 후에 대응하는 크리티컬 섹션을 P(s)와 V(s) 연산으로 둘러싸는 것.
  • 이런 방법으로 공유변수들을 보호하기 위해 이용된 세마포어는 바이너리 세마포어라고 부르며, 그 이유는 이들의 값이 항상 0 또는 1이기 때문이다. 상호 배타성을 제공하는 목적의 바이너리 세마포어는 뮤텍스(mutex)라고도 한다. 뮤텍스는 P연산을 수행하는 것을 뮤텍스를 잠근다(locking)라고 한다. 비슷하게 V연산을 수행하는 것을 뮤텍스를 연다(unlocking)라고 한다. 뮤텍스를 잠갔으나 아직 열지 못한 쓰레드는 뮤텍스를 소유하고 있다고 말한다.
  • 가능한 자원들의 집합에 대한 카운터로 이용된 세마포어는 카운팅 세마포어라고 부른다.

12.5.4 세마포어를 이용한 공유 자원 스케줄링하기

  • 세마포어의 또 다른 중요한 용도는 상호배제를 제공할 뿐만 아니라, 공유자원으로의 접근을 스케줄링하는 것이다. 이 시나리오에서, 쓰레드는 세마포어 연산을 이용해서 프로그램 상태의 어떤 조건이 참이 되었다는 것을 다른 쓰레드에 알려준다. 두 개의 고전적이고 유용한 사례는 생산자 - 소비자와 reader - writer 문제다.

생산자 - 소비자 문제

  • 생산자와 소비자 쓰레드는 n개의 슬롯을 갖는 제한된 버퍼를 공유한다. 생산자 쓰레드는 반복적으로 새 아이템을 만들고 이들을 버퍼에 삽입한다. 소비자 쓰레드는 반복적으로 아이템을 버퍼에서 제거하고 그 후에 이들을 소모한다(이용한다).
  • 아이템들을 추가하고 제거하는 것이 공유변수들의 갱신과 관련되어 있으므로 버퍼에 접근할 때 상호배타성을 보장해야 한다. 그러나 상호배타성을 보장하는 것으로는 충분하지 않다. 버퍼로의 접근을 스케줄링 할 필요도 있다. 만일 버퍼가 차 있으면(빈 슬롯이 없으면), 생산자는 슬롯이 가능해질 때 까지 기다려야 한다. 비슷하게 버퍼가 비어있으면(가용한 아이템이 없다면) 소비자는 아이템이 가용할 때까지 기다려야 한다.

읽기 - 쓰기 문제

  • 읽기 - 쓰기 문제는 상호 배제 문제의 일반화된 형태다. 동시성 스레드들의 집합은 메인메모리의 자료구조나 디스크 상의 데이터베이스 같은 공유객체에 접근하고 있다. 일부 쓰레드들은 객체를 읽기만 하고, 다른 쓰레드들은 이것을 수정만 한다. 객체를 읽기만 하는 쓰레드를 reader, 수정만 하는 쓰레드를 writer라고 한다. writer는 객체로 배타적인 접근을 해야 하지만, reader는 이 객체를 무수히 많은 다른 reader들과 공유할 수도 있다.
  • reader - writer 문제는 몇 가지 변화가 있을 수 있는데, 이들 각각은 reader와 writer의 우선순위에 근거를 두고 있다. reader를 선호하는 reader - writer 문제는 어떠한 reader도 writer가 이미 이 객체를 이용하도록 허가하지 않았으면 계속 기다려서는 안된다. 두 번째는 writer를 선호하는, writer가 쓸 준비가 되었다면 가능한 한 빨리 자신의 쓰기 작업을 수행하는 것을 요구한다.
  • 두 개의 reader - writer 문제에 대한 정확한 해결책은 기아문제로 이어질 수 있으며, 이 경우 쓰레드는 영원히 정지하고 진행하지 못한다.
  • 12.6 병렬성을 위해서 쓰레드 이용하기
  • 모든 프로그램의 집합은 중첩되지 않도록 순차적, 동시성 프로그램으로 나눌 수 있다. 순차 프로그램은 단일 논리흐름으로 작성 할 수 있다. 동시성 프로그램은 다수의 동시성 흐름으로 작성할 수 있다. 병렬 프로그램은 다중 프로세서에서 돌아가는 동시성 프로그램이다. 그래서 병렬 프로그램의 집합은 동시성 프로그램 집합의 부분 집합이다.
  • 서로 다른 쓰레드들에 작업을 할당하는 가장 직접적인 접근방법은 이 배열을 t개의 중첩되지 않은 영역으로 나누고, 그 후에 t개의 서로 다른 쓰레드 각각을 자신의 영역에서 동작하도록 할당한다.
  • 메인 쓰레드는 고유의 쓰레드 ID를 각각 피어쓰레드로 전달한다. 각각의 피어쓰레드는 자신의 쓰레드 ID를 사용해서 자신이 작업해야 할 배열의 부분을 결정한다. 이와같이 작은 고유 쓰레드 ID를 피어쓰레드로 보내는 아이디어는 많은 병렬 응용에서 사용되는 일반적인 기술이다.
  • 동기화 오버헤드는 비싸고, 가능하면 피해야한다. 만일 피할 수 없다면, 오버헤드는 가능한 한 유용한 계산만큼 축소되어야 한다.

12.7 다른 동시성의 이슈

12.7.1 쓰레드 안정성

  • 쓰레드로 프로그램 할 때, 쓰레드 안정성이라고 부르는 특성을 가지는 함수를 작성하도록 유의해야한다.
  • 어떤 함수는 다수의 동시성 스레드로부터 반복적으로 호출될 때 항상 정확한 결과를 만드는 경우에만 쓰레드 - 안전 이라고 한다. 만일 어떤 함수가 쓰레드 - 안전하지 못하다면 이것은 쓰레드 - 위험이라고 한다.
  • 쓰레드 - 위험한 함수의 네 가지 클래스
    • 클래스1
      • 공유변수를 보호하지 않는 함수들 이 함수는 보호되지 않은 전역 카운터 변수를 증가시킨다. 이 쓰레드 - 위험한 함수의 클래스는 비교적 쓰레드 - 안전하게 만들기 쉽다 : 공유변수를 P와 V같은 동기화 연산으로 보호한다. 한 가지 장점은 호출하는 프로그램에는 아무 변경도 할 필요가 없다는 것이다. 한 가지 단점은 동기화 연산들이 이 함수를 느리게 할 것이라는 점이다.
    • 클래스2
      • 다중호출에 대해서 상태를 유지하는 함수들 rand 함수는 쓰레드 - 위험한데, 그 이유는 현재 호출의 결과가 이전 반복 실행으로부터의 중간 결과에 의존하기 때문이다.
      • rand 같은 함수를 쓰레드 안전하게 만드는 유일한 방법은 이 함수를 재작성해서 static 데이터를 전혀 사용하지 않도록 하는 것이며, 이를 위해서는 호출자가 상태정보를 인자들로 전달해야한다. 단점은 프로그래머가 이제는 호출하는 루틴에서도 코드를 변경해야 한다는 것이다. 잠재적으로 수 백개의 서로 다른 호출 위치가 존재하는 큰 규모의 프로그램에서 이와 같은 수정을 하는 것은 간단한 일이 아니며 에러가 발생하기 쉽다.
    • 클래스3
      • 정적변수를 가리키는 포인터를 리턴하는 함수. 만일 이러한 함수를 동시성 쓰레드로부터 호출한다면 재앙이 발생할 수 있으며, 그 이유는 한 개의 쓰레드가 사용하는 결과들이 다른 쓰레드에 의해 조용하게 덮어써지기 때문이다.
      • 이 클래스의 쓰레드 - 위험 함수들을 다루는데 두 가지 방법이 있다. 한 가지 옵션은 함수를 다시 작성해서 호출자가 결과를 저장하는 변수의 주소를 전달하는 것이다. 이렇게 하면 모든 공유 데이터를 없앨 수 있지만, 이를 위해서는 프로그래머가 함수의 소스 코드에 접근할 수 있어야 한다.
      • 만일 쓰레드 - 위험 함수가 수정하기 어렵거나 불가능하다면(ex : 코드가 매우 복잡하거나 소스코드가 없는 경우), 다른 옵션은 lock-and-copy 기술을 이용하는 것이다. 기본 아이디어는 뮤텍스를 쓰레드 - 위험 함수와 연계하는 것이다. 각각의 호출 위치에서 뮤텍스를 잠그고, 쓰레드 - 위험 함수를 호출하며, 함수가 리턴한 결과를 사적 메모리 위치로 복사하고, 그 후에 뮤텍스를 풀어준다. 프로그래머는 호출자의 변경을 최소화 하기 위해 lock-and-copy를 수행하는 쓰레드 안전한 래퍼함수를 정의해야 하며, 쓰레드 - 위험 함수로의 모든 호출을 이 래퍼로의 호출로 대체한다.
    • 클래스4
      • 쓰레드 - 위험 함수를 호출하는 함수들. 만일 어떤 함수 f가 쓰레드 - 위험 함수 g를 호출한다면 f는 쓰레드 - 위험한가? → 그때 그때 다르다. 만일 g가 다수의 호출에 걸쳐서 상태에 의존하는 클래스2의 함수라면 f도 쓰레드 위험이며 g를 재시작하는 것 말고는 다른 방도가 없다. 그렇지만 만일 g가 클래스1 또는 클래스3 함수고, 프로그래머가 호출 위치와 다른 결과로 생성되는 공유데이터를 뮤텍스로 보호한다면 f는 여전히 쓰레드 안전이다.

12.7.2 재진입성

  • 재진입(reentrancy) .가능한 함수라고 하는 쓰레드 - 안전 함수의 중요한 클래스가 있으며, 이들은 다수의 쓰레드에 의해 호출될 때 공유데이터는 전혀 참조하지 않는 특성으로 규정된다. 비록 쓰레드 - 안전과 재진입 가능이라는 용어가 유사어로 종종 사용될지라도(부정확하게) 여기에는 준수할 가치가 있는 명확한 기술적 구분이 존재한다.
  • 모든 함수들의 집합은 쓰레드 - 안전과 쓰레드 - 위험 함수들의 중첩되지 않은 집합으로 나누어진다. 재진입가능 함수들의 집합은 쓰레드 - 안전 함수들의 적절한 부분집합이다.
  • 재 진입 가능 함수들은 대개 재진입 불가능 쓰레드 - 안전 함수들보다 더 효율적인데, 그 이유는 이들이 동기화 연산을 필요로 하지 않기 때문이다. 추가로, 클래스2 쓰레드 - 위험 함수를 쓰레드 - 안전 함수로 변환하는 유일한 방법은 이 함수를 다시 작성해서 재진입 가능하게 만드는 것이다.
  • 일부 함수의 코드를 검사하고 사전에 이것이 재진입 가능인지 선언하는 것이 가능할까? 불행하게도 상황에 따라 다르다. 만일 모든 함수 인자가 값으로 전달되고(x포인터), 모든 데이터 참조가 지역 자동 스택 변수들로 이루어진다면 이 함수는 명시적으로 재진입 가능이며, 이것이 우리가 이 함수가 재진입성을 이것이 어떻게 호출되었는지에 관계없이 선언할 수 있다는 의미에서 그렇다.
  • 하지만 만일 우리의 가정을 약간 완화하고, 그렇지 않으면 명시적으로 재진입 가능한 함수에서 일부 매개변수들을 참조 형태로 전달되게 해주며, 그 후에 만일 호출하는 쓰레드들이 포인터를 공유되지 않은 데이터로 조심스럽게 전달하려고 한다면, 이것이 유일하게 재진입 가능하다는 의미에서 간접적으로 재진입 가능 함수를 가진다. 우리는 재진입 가능이라는 용어를 명시적이고 묵시적인 재진입 가능한 함수들 모두를 포함하기 위해서 항상 사용한다. 그렇지만, 재진입성은 때로는 호출자와 피호출자 모두의 특징이며 피호출자만을 위한것은 아니다.

12.7.3 쓰레드 프로그램에서 기존 라이브러리 함수 사용하기

  • 대부분 리눅스 함수들은 표준 C라이브러리에서 정의된 함수를 포함해서 일부 예외를 제외하고는 쓰레드 - 안전하다.
  • 몇 함수는 쓰레드 - 위험한데 rand와 strtok을 제외하고, 이들은 클래스3의 변형된 형태이며, 이들은 정적변수로의 포인터를 리턴한다. 만일 쓰레드를 이용하는 프로그램에서 이중 하나의 함수를 호출할 필요가 있다면, 호출자에게 가장 덜 피해를 주는 방법은 lock-and-copy 방식이다. 그렇지만 lock-and-copy 방식은 많은 단점이 있다. 첫째, 추가적인 동기화 과정 때문에 프로그램 속도가 느려진다. 둘째, 복잡한 구조체의 구조체를 가리키는 포인터를 리턴하는 함수들은 전체 구조체 계층구조를 복사하기 위해서는 구조체 가장 말단까지 복사해야한다. 셋째, lock-and-copy 방식은 호출들에 대해서 정적 상태의 의존하는 rand 같은 클래스2 쓰레드 - 위험 함수들에 대해서는 동작하지 않는다.

12.7.4 경쟁상대

  • 경쟁(race)은 프로그램의 정확성이 다른 쓰레드가 y지점에 도착하기 전에 자신의 제어흐름에서 x지점에 도착하는 하나의 쓰레드에 의존할 때 일어난다. 경쟁은 대개 프로그래머들이 쓰레드가 실행상태 공간을 지나가는 어떤 특정궤적을 따른다고 가정하며, 쓰레드 프로그램이 모든 가능한 궤적에 대해서도 정확하게 동작해야 한다는 불문율을 잊어버리기 때문에 일어난다.

12.7.5 교착상태

  • 세마포어는 교착상태(deadlock)라고 부르는 못된 종류의 잠재적인 런타임 에러를 유발하며, 이것은 다수의 쓰레드가 절대로 참이 될 수 없는 조건을 기다리면서 정지되어있는 경우를 말한다. 진행그래프는 교착상태를 이해하기 위한 소중한 도구다
    • 프로그래머가 P와 V연산을 잘못 배치해서 두 개의 세마포어를 위한 금지된 구역이 겹치게 된다. 만일 일부 실행 궤적이 교착상태 d에 도달하게 되었다면, 더 이상의 진행은 불가능한데, 그 이유는 겹치는 금지 구역이 모든 합법적인 방향으로의 진행을 막기 때문이다.
    • 겹치는 금지구역은 교착구역이라고 부르는 상태들의 집합을 만든다. 만일 어떤 궤적이 교착구역 내의 상태와 닿는다면 교착상태는 피할 수 없다. 궤적들은 교착구역에 진입할 수 는 있지만, 결코 떠날 수는 없다.
    • 교착 상태는 언제나 예측 가능한 것이 아니므로 특히 어렵다. 프로그램이 어떤 머신에서는 잘 돌다가도 다른 머신에서는 교착상태에 빠질 수 있다. 가장 나쁜경우는 서로 다른 실행들이 서로 다른 궤적을 가지기 때문에 에러가 반복되지 않는다는 점이다.

코드리뷰에 대해

TEST-Drive Development - 테스트가 기본이다

  • 요구사항과 구현을 분리하라
  • 테스트는 언제나 코드와 싱크되어 있다.
  • no test, no source code
  • 자잘하게 많이 커밋해라

Pair Programming - 선배 어깨 너머로 배우기

  • 둘이 나란히 한 컴퓨터로 프로그래밍
  • 실력이 정말 빠르게 증가한다.

Code Review - 이해하기 쉽고, 유지보수하기 쉽게

  • 꼭 필요한 한 단계이다.
  • 당신의 동료로부터 내 코드를 피드백 받는 것을 의미한다.

It is not about code review

  • 소프트웨어 설계를 어떻게 할까 - 이런것은 화이트보드에 코드리뷰는 코드 체인징에 관해서 한다.
  • 소프트웨어에 요구 계획 추가나 프로젝트 추가

Step

  • LGTM이 나올 때까지 코드리뷰
  • 코드 리뷰는 읽는 사람을 위한 것이다. - 한달 후 내가 될 수도 있다.

왜 why 필요한가?

  • 당신의 CL(Change List)를 이해하기 쉽게 만든다.
    • 당신의 동료가 당신의 코드를 이해할 수 있어야 한다.
  • 당신의 CL에 동료가 남긴 의견으로부터 tip&lessons을 배운다.
    • 숙련된 개발자로부터 코딩스타일과 팁을 배운다
    • 당신의 팀이 공통된 코딩스타일을 공유할 수 있다.
  • 결함을 줄일 수 있다.
  • 당신의 coding decision에 대한 개발 역사를 보관한다.
    • 동료의 의견은 코드 설계와 결정사항에 대해 이해하는데 매우 큰 도움이 된다.
    • 새로운 개발자는 committed log와 의견으로부터 코드의 구조와 결정사항을 이해할 수 있다.
  • 당신의 코드에 일관적인 코딩스타일을 유지할 수 있다.
    • 새로 온 개발자가 기존의 코딩 스타일을 따를 수 있도록 도와준다.
    • 일관적인 코딩 스타일은 이후에 코드를 refactoring 하거나 디버깅 할 때 큰 도움이 된다.
  • 이러 이러한 이유로 이렇게 바꾸면 좋습니다. 레퍼런스는 여기를 참조하시면 좋을 것 같습니다.
    • 이런식으로 코드리뷰를 하면 매우 좋다.
  • 남의 코드를 보면서 자기가 생각하는 것을 말하면서 많이 배울 수 있다.
    • 코드 리뷰를 통해서 대화
  • 코드리뷰를 하다보면 코드가 투명해지고, 확장성이 생긴다.
  • 코드의 표준이 올라간다.

Possible Downsides of Code Review

  • 거칠고 무례한 의견 때문에 CL owners may get discouraged
  • 리뷰가 늘어지면 개발기간이 늦어진다
  • 코드 리뷰를 제대로 하려면 시간이 걸린다.
  • 경험이 부족한 개발자의 잘못된 CL을 리뷰하느라 숙련된 개발자의 시간이 허비 될 수 있다.
  • 코드레뷰를 위해서는 어느정도 숙련된 개발자가 필요하다.

Google C++ Style Guide

코드리뷰를 위한 것

  • 묵시적인 것으로 에러가 발생한다면 나중에 디버깅하기 매우 힘들기 때문에 명시적으로 선언
  • 새로운 기술보다 기본에 충실하라. 하지만 최적화 부분이나, 속도가 차이난다면 속도를 우선시해라.
  • 도구가 할 수 있는것은 도구에게 맡겨라.
  • 함수는 최소한 작게 구현해라 - 함수는 하나의 일만 해야한다.
  • 함수의 이름에서 무슨 역할을 수행하는 함수인지 들어나야한다.

당신의 팀에는 일관적인 스타일이 있는것이 좋다.

Testing

  • Testing Rocks! Debug Sucks
  • 디버깅은 보통 문제를 찾는데 오랜 시간이 걸린다.
  • 테스팅은 새로 작성한 코드에서도 결합을 검출 할 수 있다.
  • 테스팅은 테스트 코드를 필요로 하기 때문에, 유지보수 부담을 줄인다.
  • unit testing : 함수 하나 하나를 테스팅
  • integration testing : 서로 다른 시스템들의 상호작용이 잘 이뤄지는지 테스트하는 것

Project Scalability

  • 새로운 개발자도 테스트 코드를 잘 작성해서 프로젝트에 기여할 수 있다.
  • 동료나 외부 기여자에게서 도움을 받기에 가장 적합하다.

Code Refactoring

  • Refactoring은 SW의 동작을 바꾸지 않으면서 내부 구조를 개선하는 것이다. 즉, 코드의 구조를 잘 정해진 규정대로 수정하는 기술이다.
  • SW를 더 이해하기 쉽게 만들고, 수정하는 비율을 줄인다.
  • Why
    • SW 설계 개선
    • Refactoring이 없으면 프로그램의 설계가 낡아진다.
    • 설계가 좋지 않은 코드는 보통 같은 일을 하는데 코드가 길고, 같은 일을 여러곳에서 한다.
    • 이해하기 쉽다.
    • 대부분의 SW 개발 환경에서, 누군가 언젠가는 당신의 코드를 읽어야 할 때가 오기 때문에, 그들을 위해 이해하기 쉬운 코드를 작성해야 한다.
    • 결함을 검출 할 때도 있다.
    • 프로그램의 속도를 향상 시킬 수 있다.
      • 프로그램에 대해 더 잘 이해할 수 있기 때문에

C++

copy()

  • copy(원본 시작 이터레이터, 원본 끝 이터레이터, 복사받는 시작 이터레이터)

string::find()

  • string 클래스의 멤버함수로서, str.find(”찾는 문자”)로 사용한다.
  • 반환값은 찾는 문자의 첫 번째 인덱스값을 반환한다.
  • 찾는 문자가 없을 경우에 string::npos를 리턴한다.
    • npos는 no position으로 쓰레기값이 나온다.
#include <iostream>
#include <string>

using namespace std;

String str = "Hello World!";

int main() 
{
	if (str.find("Hello") != string::npos) 
	{
    	cout << "찾는 문자가 존재합니다";
		  int index = str.find("Hello");   //해당 문자의 시작 인덱스 반환
	}
}

constexpr

  • C++11에 constexpr 이라는 키워드가 추가되었다.
  • constexpr은 변수 또는 함수의 값을 컴파일 시점에 도출하여 상수화 시켜주는 아주 강력한 기능이다.
  • 컴파일 시점에 상수로 처리되기 때문에 switch case 문에서도 상수로 취급된다.

constexpr을 함수에 사용할 때 제약사항

  • constexpr을 사용하는 데 있어서 몇 가지 제약사항이 있다.
    • 반환값이 무조건 Literal Type이어야 한다.
    • virtual로 재정의된 함수가 아니어야 한다.
    • 재귀 함수로 사용될 수 있다.
    • 함수에 constexpr을 붙일 경우 inline을 암시한다.
    • C++11 에서는 함수 본문에 지역변수를 둘 수 없고, 하나의 return 구문만 있어야 했다.
    • 위 제약은 C++14에서 사라졌다.

구현 예시

  • 25206 너의 평점은
#include <bits/stdc++.h>
using namespace std;

constexpr unsigned int Hash(const char* str) 
{
	return str[0] ? static_cast<unsigned int>(str[0]) + 0xEDB8832Full * Hash(str + 1) : 8603;
}

int main()
{
	ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
	char grade[50];
	string subject;
	float my_grade,sum_grade=0,major_GPA=0;
	for (int i = 0; i < 20; ++i)
	{
		cin >> subject >> my_grade >> grade;
		switch (Hash(grade))
		{
		case Hash("A+"):
			sum_grade += my_grade;
			major_GPA += my_grade * 4.5;
			break;
		case Hash("A0"):
			sum_grade += my_grade;
			major_GPA += my_grade * 4.0;
			break;
		case Hash("B+"):
			sum_grade += my_grade;
			major_GPA += my_grade * 3.5;
			break;
		case Hash("B0"):
			sum_grade += my_grade;
			major_GPA += my_grade * 3.0;
			break;
		case Hash("C+"):
			sum_grade += my_grade;
			major_GPA += my_grade * 2.5;
			break;
		case Hash("C0"):
			sum_grade += my_grade;
			major_GPA += my_grade * 2.0;
			break;
		case Hash("D+"):
			sum_grade += my_grade;
			major_GPA += my_grade * 1.5;
			break;
		case Hash("D0"):
			sum_grade += my_grade;
			major_GPA += my_grade * 1.0;
			break;
		case Hash("F"):
			sum_grade += my_grade;
			major_GPA += my_grade * 0.0;
			break;
		case Hash("P"):
			break;
		default:
			break;
		}
	}
	
	cout << major_GPA/sum_grade;
	return 0;
}

입력함수 : cin(), getline() and cin.ignore()

  • C++의 입력 함수
  • 입력버퍼를 비우는데 사용하는 cin.ignore()

cin()

  • <iostream> 헤더에 정의되어 있다.
  • 표준 입력 버퍼에서 공백 혹은 개행 문자(\n) 이전 까지의 값만을 받아들인다.
  • 연산자를 사용하여 공백이 포함된 문자열을 입력 받을 경우, 공백 전의 문자만 입력된다는 단점이 존재한다.

    • 공백이 포함된 문자열을 입력받으려면 getline() 함수를 사용해야한다.

getline()

  • 다음과 같은 2가지로 나누어진다.
    • <istream>라이브러리에 속하는 cin.getline()
    • <string> 라이브러리에 속하는 getline()

<istream> cin.getline()

cin.getline(변수 주소, 최대 입력 수, 제한자);
  • C 형식 문자열 방식인 마지막 글자에 ‘NULL(\0)’ 문자가 포함된 문자 배열을 받는데 사용한다.
  • N-1개의 문자를 읽어와서 문자형 배열에 저장하고, 마지막 문자는 자동으로 NULL로 바꾼다.
  • 세 번째 인자 delim(제한자) 직전까지 읽어서 문자형 배열에 저장한다.
    • 제한자를 별도로 지정하지 않으면 개행문자(\n)로 인식한다.

<string> getline()

getline(입력 스트림, string 객체, 구분자);
  • 지정한 구분자(Delimiter)를 만날 때까지 문자열을 입력받아 string객체에 저장한다.
  • 구분자는 따로 지정해주지 않아도 된다.

cin.ignore() 함수 사용하기 전 참고사항

  • cin으로 입력받을 경우, 버퍼에 ‘\n’이 남는다.
  • cin 다음 입력을 cin으로 받을 경우, 전 버퍼에 있던 공백 및 개행문자를 무시하기 때문에 없다.
  • cin 다음 입력을 getline 으로 받을경우, 전 버퍼에 있던 공백 및 개행문자를 포함해서 입력받기 때문에 버퍼를 지워주는 작업이 필요하다.
  • getline 다음 입력을 getline 으로 받을경우, getline은 ‘\n’ 문자를 버퍼에 포함시키기 않기 때문에 버퍼를 비워줄 필요가 없다.

Sort()

  • sort 알고리즘은 <algorithm> 헤더파일에 속해있다.
  • sort(start,end)를 이용해서 범위에 있는 인자 (element)를 오름차순으로 정렬해준다.
  • quick sort(퀵 정렬)을 기반으로 함수가 구현되어있어, 평균 시간복잡도는 nlogn이다.
sort(arr,arr+n);
sort(v.begin(), v.end())
sort(v.begin(), v.end(), compare());         // 사용자 정의 함수
sort(v.begin(), v.end(), greater<자료형>()); //  내림차순
sort(v.begin(), v.end(), less<자료형>());    //  오름차순

TCP/IP

네트워크 프로그래밍과 소켓의 이해

01-1 네트워크 프로그래밍과 소켓의 이해

  • 네트워크 프로그래밍 : 서로 다른 두 컴퓨터가 데이터를 주고 받을 수 있도록 하는 것.
  • 소켓(Socket) : 물리적으로 연결된 네트워크 상에서 데이터 송수신에 사용할 수 있는 소프트웨어적인 장치. 프로그래밍에서의 ‘소켓’은 네트웨크 망의 연결에 사용되는 도구다. 연결이라는 의미가 담겨있어서 ‘소켓’이라는 표현을 사용한다. 그리고 그 의미를 조금 더 확장해서 소켓은 네트워크를 통한 두 컴퓨터에 연결을 의미하기도 한다.

socket 함수를 통해 소켓생성

  1. 소켓생성 - socket 함수 호출
  2. IP주소와 PORT번호 할당 - bind 함수호출
  3. 연결요청 가능상태로 변경 - listen 함수호출
  4. 연결요청 대한 수락 - accept 함수호출
  • 클라이언트 프로그램에서는 socket 함수 호출을 통한 소켓의 생성과 connect 함수 호출을 통한 서버로의 연결요청 과정만이 존재한다.

01-2 리눅스 기반 파일 조작하기

  • 리눅스에서의 소켓 조작은 파일 조작과 동일하게 간주된다.
  • 리눅스는 소켓을 파일의 일종으로 구분한다. 따라서 파일 입출력 함수를 소켓 입출력에, 네트워크상에서의 데이터 송수신에 사용할 수 있다.
    • 윈도우는 리눅스와 달리 파일과 소켓을 구분하고 있다.

저 수준 파일 입출력(Low-level File Access)과 파일 디스크립터(File Descriptor)

  • 파일 디스크립터란 시스템으로부터 할당받은 파일 또는 소켓에 부여된 정수를 의미한다.
  • 표준 입출력 및 표준 에러에도 리눅스에서는 파일 디스크립터를 할당하고 있다.
  • 일반적으로 파일과 소켓은 생성의 과정을 거쳐야 다일 디스크립터가 할당된다.
  • 파일 또는 소켓을 생성할 때마다, 운영체제는 해당 파일 또는 소켓에 부여된 숫자 하나를 건네준다.

파일열기

  • open(path, flag)
    • path : 대상이 되는 파일의 이름 및 경로 정보
    • flag : 파일의 오픈 모드 정보(파일의 특성 정보)
    • 데이터를 읽거나 쓰기 위해서 파일을 열 때 사용하는 함수

파일닫기

  • close(fd)
    • fd(디스크립터) : 닫고자 하는 파일
    • 파일은 사용 후 반드시 닫아줘야 한다. 파일을 닫을 때 사용하는 함수 파일 뿐만 아니라 소켓을 닫을 때도 사용한다.

파일에 데이터 쓰기

  • write(fd, buf, nbytes)
    • fd : 데이터 전송 대상을 나타내는 파일 디스크립터
    • buf : 전송할 데이터가 저장된 버퍼의 주소 값
    • nbytes : 전송할 데이터의 바이트 수 전달.
    • 프로그래머에 의해 정의되는 자료형 이름과의 구분을 위해서, 시스템(운영체제)에서 정의하는 자료형의 이름에는 _t가 붙어있다.

파일에 저장된 데이터 읽기

  • read(fd, buf, nbytes)
    • fd : 데이터 수신 대상을 나타내는 파일 디스크립터
    • buf : 수신한 데이터를 저장할 버퍼의 주소 값
    • nbytes : 수신할 최대 바이트 수
    • 데이터를 입력(수신)하는 기능의 함수다.

01-3 윈도우 기반으로 구현하기

  • 윈속(winsock)의 초기화
  • 윈속 프로그래밍을 할 때에는 반드시 WSAstartup 함수를 호출해서, 프로그램에서 요구하는 윈도우 소켓의 버전을 알리고, 해당 버전을 지원하는 라이브러리의 초기화 작업을 진행해야한다.
  • WSAstartup(WORD wversionRequested, LPWSADATA lpWSAData)
    • WORD wversionRequested : 프로그래머가 사용할 윈속의 버전 정보
    • LPWSADATA lpWSAData : WSADATA라는 구조체 변수의 주소 값
  • 윈도우 소켓에는 몇몇 버전이 존재한다. 따라서 사용할 소켓의 버전정보를 WORD 행으로 구성해서 전달한다.
728x90