Computer >> 컴퓨터 >  >> 프로그램 작성 >> C 프로그래밍

암시적 스레딩 및 언어 기반 스레드

<시간/>

암시적 스레딩

멀티스레드 애플리케이션의 설계를 더 잘 지원하고 어려움을 해결하는 한 가지 방법은 스레딩 생성 및 관리를 애플리케이션 개발자에서 컴파일러 및 런타임 라이브러리로 이전하는 것입니다. 이를 암시적 스레딩이라고 하는 것은 오늘날 인기 있는 추세입니다.

암시적 스레딩 스레드 관리를 숨기기 위해 주로 라이브러리 또는 기타 언어 지원을 사용합니다. 가장 일반적인 암시적 스레딩 라이브러리는 C 컨텍스트에서 OpenMP입니다.

OpenMP 공유 메모리 환경에서 병렬 프로그래밍을 지원하는 C, C++ 또는 FORTRAN으로 작성된 프로그램용 API 및 컴파일러 지시문 세트입니다. OpenMP는 병렬 영역을 병렬로 실행될 수 있는 코드 블록으로 식별합니다. 응용 프로그램 개발자는 병렬 영역의 코드에 컴파일러 지시문을 삽입하고 이러한 지시문은 OpenMP 런타임 라이브러리에 해당 영역을 병렬로 실행하도록 지시합니다. 다음 C 프로그램은 printf() 문을 포함하는 병렬 영역 위의 컴파일러 지시문을 보여줍니다.

예시

#include <omp.h>
#include <stdio.h>
int main(int argc, char *argv[]){
   /* sequential code */
   #pragma omp parallel{
      printf("I am a parallel region.");
   }
   /* sequential code */
   return 0;
}

출력

I am a parallel region.

OpenMP가 지시문을 만났을 때

#pragma omp parallel

시스템에서 코어를 처리하는 스레드를 생성합니다. 따라서 듀얼 코어 시스템의 경우 두 개의 스레드가 생성되고 쿼드 코어 시스템의 경우 네 개의 스레드가 생성됩니다. 기타 등등. 그런 다음 모든 스레드가 동시에 병렬 영역을 실행합니다. 각 스레드가 병렬 영역을 나갈 때 종료됩니다. OpenMP는 루프 병렬화를 포함하여 병렬로 코드 영역을 실행하기 위한 몇 가지 추가 지시문을 제공합니다.

병렬화에 대한 지시어를 제공하는 것 외에도 OpenMP를 사용하면 개발자가 여러 수준의 병렬화 중에서 선택할 수 있습니다. 예를 들어 스레드 수를 수동으로 설정할 수 있습니다. 또한 개발자는 데이터가 스레드 간에 공유되는지 또는 스레드에 대해 비공개인지 식별할 수 있습니다. OpenMP는 Linux, Windows 및 Mac OS X 시스템용 여러 오픈 소스 및 상용 컴파일러에서 사용할 수 있습니다.

그랜드 센트럴 디스패치(GCD)

Apple의 Mac OS X 및 iOS 운영 체제용 기술인 GCD(Grand Central Dispatch)는 응용 프로그램 개발자가 실행할 코드 섹션을 찾을 수 있도록 하는 C 언어, API 및 런타임 라이브러리에 대한 확장의 조합입니다. 평행한. OpenMP와 마찬가지로 GCD는 스레딩의 세부 정보도 대부분 관리합니다. 블록으로 알려진 C 및 C++ 언어의 확장을 식별합니다. 블록은 단순히 독립적인 작업 단위입니다. 한 쌍의 중괄호 { } 앞에 삽입된 캐럿 ˆ으로 지정됩니다. 블록의 간단한 예는 다음과 같습니다. -

{
   ˆprintf("This is a block");
}

디스패치 큐에 블록을 배치하여 런타임 실행을 위한 블록을 예약합니다. GCD가 큐에서 블록을 제거하면 GCD가 관리하는 스레드 풀에서 사용 가능한 스레드에 블록을 할당합니다. 직렬 및 동시의 두 가지 유형의 디스패치 대기열을 식별합니다. 직렬 큐에 배치된 블록은 FIFO 순서로 제거됩니다. 블록이 대기열에서 제거되면 다른 블록이 제거되기 전에 실행을 완료해야 합니다. 각 프로세스에는 고유한 직렬 큐(메인 큐라고 함)가 있습니다. 개발자는 특정 프로세스에 로컬인 추가 직렬 대기열을 생성할 수 있습니다. 직렬 대기열은 여러 작업의 순차적 실행을 보장하는 데 유용합니다. 동시 대기열에 배치된 블록도 FIFO 순서로 제거되지만 한 번에 여러 블록이 제거될 수 있으므로 여러 블록을 병렬로 실행할 수 있습니다. 세 가지 시스템 전체 동시 디스패치 대기열이 있으며 우선 순위에 따라 낮음, 기본값 및 높음으로 구분됩니다. 우선 순위는 블록의 상대적 중요도에 대한 추정치를 나타냅니다. 간단히 말해서, 우선순위가 더 높은 블록은 우선순위가 높은 디스패치 큐에 배치되어야 합니다. 다음 코드 세그먼트는 기본 우선 순위 동시 대기열을 얻고 디스패치 async() 함수를 사용하여 대기열에 블록을 제출하는 방법을 보여줍니다.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch async(queue, ˆ{ printf("This is a block."); });

내부적으로 GCD의 스레드 풀은 POSIX 스레드로 구성됩니다. GCD는 풀을 능동적으로 관리하여 애플리케이션 수요 및 시스템 용량에 따라 스레드 수를 늘리거나 줄일 수 있습니다.

객체로서의 스레드

대체 언어에서 고대 객체 지향 언어는 스레드를 객체로 사용하여 명시적인 멀티스레딩 지원을 제공합니다. 이러한 형태의 언어에서 클래스 영역은 스레드 클래스를 확장하거나 해당 인터페이스를 구현하기 위해 작성됩니다. 이 스타일은 코드가 명시적 스레드 관리로 작성되었기 때문에 Pthread 접근 방식과 유사합니다. 그러나 클래스 내부의 정보 캡슐화 및 추가 동기화 옵션은 작업을 수정합니다.

자바 스레드

Java는 사용할 수 있는 Thread 범주와 Runnable 인터페이스를 제공합니다. 각각은 스레드의 진입 목적을 정의하는 public void run() 기술을 구현해야 합니다. 개체의 인스턴스가 할당되면 해당 인스턴스에서 start() 기술을 호출하여 스레드가 시작됩니다. Pthread와 마찬가지로 스레드 시작은 비동기적이며 실행의 시간적 배열은 비결정적입니다.

파이썬 스레드

Python은 멀티스레딩을 위한 두 가지 메커니즘을 추가로 제공합니다. 한 가지 접근 방식은 함수 이름이 라이브러리 메서드 thread.start_new_thread()에 전달될 때마다 Pthread 스타일과 비슷합니다. 이 접근 방식은 스레드가 시작되면 스레드를 조인하거나 종료할 수 있는 유연성이 매우 부족합니다. 추가적인 유연한 기술은 스레딩 모듈을 사용하여 스레딩을 확장하는 클래스를 설명하는 것입니다. 실. Java 접근 방식과 거의 유사하게 카테고리에는 스레드의 진입 목적을 제공하는 run() 메소드가 있어야 합니다. 이 범주에서 개체가 인스턴스화되면 나중에 명시적으로 시작하고 결합할 수 있습니다.

언어 설계로서의 동시성

최신 프로그래밍 언어는 언어 스타일 자체에 직접 동시 실행 가정을 구축하여 경쟁 조건을 피했습니다. 예를 들어 Go는 간단한 암시적 스레딩 기술(고루틴)을 잘 정의된 메시지 전달 통신 스타일인 채널과 결합합니다. Rust는 Pthreads와 동일한 확실한 스레딩 접근 방식을 채택합니다. 그러나 Rust는 소프트웨어 엔지니어의 추가 작업이 필요 없는 매우 강력한 메모리 보호 기능을 가지고 있습니다.

고루틴

Go 언어에는 암시적 스레딩을 위한 간단한 메커니즘이 포함되어 있습니다. 즉, 호출 앞에 키워드 go를 배치합니다. 새 스레드는 메시지 전달 채널에 대한 연결을 전달합니다. 그런 다음 대부분의 스레드가 채널에 대한 간섭 스캔을 수행하는 success :=<-messages를 호출합니다. 사용자가 올바른 추측 7을 입력하면 키보드 감사 스레드가 채널에 기록하여 대부분의 스레드가 진행되도록 허용합니다.

채널과 고루틴은 Go 언어의 핵심 구성요소로, 거의 모든 프로그램이 다중 스레드될 것이라는 믿음 하에 설계되었습니다. 이 스타일 대안은 이벤트 모델을 간소화하여 언어 자체가 스레드 및 프로그래밍 관리에 대한 책임을 최신 상태로 유지하도록 합니다.

Rust 동시성

또 다른 언어는 동시성을 중심 디자인 기능으로 사용하여 최근 몇 년 동안 생성된 Rust입니다. 다음 예제는 thread::spawn()을 사용하여 새 스레드를 만드는 방법을 보여줍니다. 이 스레드는 나중에 해당 스레드에서 join()을 호출하여 결합할 수 있습니다. ||에서 시작하는 thread::spawn()에 대한 인수 익명 함수로 생각할 수 있는 클로저로 알려져 있습니다. 즉, 여기의 자식 스레드는 값을 인쇄합니다.

예시

use std::thread;
fn main() {
   /* Initialize a mutable variable a to 7 */
   let mut a = 7;
   /* Spawn a new thread */
   let child_thread = thread::spawn(move || {
      /* Make the thread sleep for one second, then print a */
      a -= 1;
      println!("a = {}", a)
   });
   /* Change a in the main thread and print it */
   a += 1;
   println!("a = {}", a);
   /* Join the thread and print a again */
   child_thread.join();
}

그러나 이 코드에는 Rust 디자인의 핵심인 미묘한 점이 있습니다. 새 스레드(클로저의 코드 실행) 내에서 a 변수는 이 코드의 다른 부분과 다릅니다. 여러 스레드가 동일한 메모리에 액세스하는 것을 방지하는 매우 엄격한 메모리 모델("소유권"이라고 함)을 시행합니다. 이 예에서 move 키워드는 생성된 스레드가 자체 사용을 위해 의 별도 사본을 수신함을 나타냅니다. 두 스레드의 일정에 관계없이 기본 스레드와 자식 스레드는 서로 다른 복사본이므로 의 수정을 방해할 수 없습니다. 두 스레드가 동일한 메모리에 대한 액세스를 공유하는 것은 불가능합니다.