programing

C 코드를 "멀티 스레드"하는 방법

lastmoon 2023. 6. 6. 10:35
반응형

C 코드를 "멀티 스레드"하는 방법

저는 C로 작성된 숫자 크런치 애플리케이션을 가지고 있습니다.이것은 각 값에 대해 일부 계산을 수행하는 함수인 "i"의 값을 증가시키기 위해 호출되는 일종의 주 루프입니다.나는 멀티스레딩에 대해 읽었고, C에서 그것에 대해 조금 배울 것을 고려하고 있습니다.저는 저와 같은 일반적인 코드가 어떻게 자동으로 멀티 스레드화될 수 있는지 궁금합니다.

감사해요.

P.D. 내 코드에 대한 아이디어를 얻기 위해 다음과 같은 것이라고 가정해 보겠습니다.

main(...)
for(i=0;i<=ntimes;i++)get_result(x[i],y[i],result[i]);

...

void get_result(float x,float y,float result){
  result=sqrt(log (x) + log (y) + cos (exp (x + y));
(and some more similar mathematical operations)
}

작업이 고도로 병렬화 가능하고 컴파일러가 최신 버전이라면 OpenMP를 사용해 볼 수 있습니다. http://en.wikipedia.org/wiki/OpenMP

멀티스레드 코드에 대한 한 가지 대안은 pthread(OpenMP보다 더 정확한 제어를 제공)를 사용하는 것입니다.

을 가정하여x,y&result배열입니다.

#include <pthread.h>

...

void *get_result(void *param)  // param is a dummy pointer
{
...
}

int main()
{
...
pthread_t *tid = malloc( ntimes * sizeof(pthread_t) );

for( i=0; i<ntimes; i++ ) 
    pthread_create( &tid[i], NULL, get_result, NULL );

... // do some tasks unrelated to result    

for( i=0; i<ntimes; i++ ) 
    pthread_join( tid[i], NULL );
...
}

((으)로 gcc prog.c -lpthread)

당신은 이것을 위해 openMP를 봐야 합니다.이 페이지의 C/C++ 예제는 코드 https://computing.llnl.gov/tutorials/openMP/ #Sections와 유사합니다.

#include <omp.h>
#define N     1000

main ()
{

int i;
float a[N], b[N], c[N], d[N];

/* Some initializations */
for (i=0; i < N; i++) {
  a[i] = i * 1.5;
  b[i] = i + 22.35;
  }

#pragma omp parallel shared(a,b,c,d) private(i)
  {

  #pragma omp sections nowait
    {

    #pragma omp section
    for (i=0; i < N; i++)
      c[i] = a[i] + b[i];

    #pragma omp section
    for (i=0; i < N; i++)
      d[i] = a[i] * b[i];

    }  /* end of sections */

  }  /* end of parallel section */

}

열기를 사용하지 않으려면MP 당신은 pthreads 또는 직접 복제/대기를 사용할 수 있습니다.

어떤 경로를 선택하든 어레이를 각 스레드가 처리할 청크로 분할하는 것입니다.모든 처리가 순수하게 계산적인 경우(예: 함수에서 제안한 대로) 논리 프로세서 수만큼의 스레드만 갖는 것이 좋습니다.

병렬 처리를 위해 스레드를 추가할 때 약간의 오버헤드가 발생하므로 각 스레드에 이를 보완할 수 있는 충분한 작업을 제공해야 합니다.일반적으로 그러겠지만, 각 스레드가 하나의 계산만 수행하면 되고 계산이 그렇게 어렵지 않으면 실제로 속도가 느려질 수 있습니다.이 경우 프로세서보다 스레드 수가 항상 적을 수 있습니다.

작업 중에 IO가 어느 정도 진행 중인 경우 프로세서보다 스레드 수가 많은 것이 이득이라는 것을 알 수 있습니다. 한 스레드가 일부 IO가 완료되기를 기다리는 동안 다른 스레드가 계산을 수행할 수 있기 때문입니다.하지만 스레드에서 동일한 파일에 IO를 수행할 때는 주의해야 합니다.

만약 당신이 어떤 종류의 과학적 컴퓨팅이나 유사한 것에 대해 단일 루프에 대한 동시성을 제공하기를 원한다면, @Novikov의 OpenMP는 정말 최선의 방법이라고 말합니다. 이것이 바로 그것을 위해 설계된 것입니다.

C로 작성된 애플리케이션에서 보다 일반적으로 볼 수 있는 보다 고전적인 접근 방식을 배우려고 한다면... POSIXpthread_create()기타. 다른 언어의 동시성에 대한 배경이 무엇인지는 잘 모르겠지만, 너무 깊이 들어가기 전에 동기화 기본 요소(뮤텍스, 세마포어 등)를 잘 알고 사용해야 하는 시기를 이해하고 싶을 것입니다.그 주제는 전체 책일 수도 있고 SO 질문의 집합일 수도 있습니다.

glibc 2.28의 C11 스레드.

다음 소스에서 glibc를 컴파일하여 Ubuntu 18.04(glibc 2.27)에서 테스트됨:단일 호스트에서 여러 glibc 라이브러리

예: https://en.cppreference.com/w/c/language/atomic

#include <stdio.h>
#include <threads.h>
#include <stdatomic.h>

atomic_int acnt;
int cnt;

int f(void* thr_data)
{
    for(int n = 0; n < 1000; ++n) {
        ++cnt;
        ++acnt;
        // for this example, relaxed memory order is sufficient, e.g.
        // atomic_fetch_add_explicit(&acnt, 1, memory_order_relaxed);
    }
    return 0;
}

int main(void)
{
    thrd_t thr[10];
    for(int n = 0; n < 10; ++n)
        thrd_create(&thr[n], f, NULL);
    for(int n = 0; n < 10; ++n)
        thrd_join(thr[n], NULL);

    printf("The atomic counter is %u\n", acnt);
    printf("The non-atomic counter is %u\n", cnt);
}

GitHub 업스트림.

컴파일 및 실행:

gcc -std=c11 main.c -pthread
./a.out

가능한 출력:

The atomic counter is 10000
The non-atomic counter is 8644

비원자 계수기는 비원자 변수에 대한 스레드 간의 빠른 액세스로 인해 원자 계수기보다 작을 가능성이 매우 높습니다.

하여 TODO: 작업 내용을 합니다.++acnt;…로 옮기다

POSIX 스레드

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <stdlib.h>
#include <pthread.h>

enum CONSTANTS {
    NUM_THREADS = 1000,
    NUM_ITERS = 1000
};

int global = 0;
int fail = 0;
pthread_mutex_t main_thread_mutex = PTHREAD_MUTEX_INITIALIZER;

void* main_thread(void *arg) {
    int i;
    for (i = 0; i < NUM_ITERS; ++i) {
        if (!fail)
            pthread_mutex_lock(&main_thread_mutex);
        global++;
        if (!fail)
            pthread_mutex_unlock(&main_thread_mutex);
    }
    return NULL;
}

int main(int argc, char **argv) {
    pthread_t threads[NUM_THREADS];
    int i;
    fail = argc > 1;
    for (i = 0; i < NUM_THREADS; ++i)
        pthread_create(&threads[i], NULL, main_thread, NULL);
    for (i = 0; i < NUM_THREADS; ++i)
        pthread_join(threads[i], NULL);
    assert(global == NUM_THREADS * NUM_ITERS);
    return EXIT_SUCCESS;
}

컴파일 및 실행:

gcc -std=c99 pthread_mutex.c -pthread
./a.out
./a.out 1

첫 번째 실행은 정상적으로 작동하고 두 번째 실행은 동기화 누락으로 인해 실패합니다.

Ubuntu 18.04에서 테스트되었습니다.GitHub 업스트림.

운영체제에 따라, 당신은 posix 스레드를 사용할 수 있습니다.대신 상태 컴퓨터를 사용하여 스택 없는 멀티스레딩을 구현할 수 있습니다.키스 E의 "임베디드 멀티태스킹"이라는 제목의 정말 좋은 책이 있습니다.커티스스위치 케이스 문을 깔끔하게 정리한 것뿐입니다.잘 작동합니다. 애플 맥, 래빗 반도체, AVR, PC 등 모든 분야에서 사용했습니다.

발리

인텔의 C++ 컴파일러는 실제로 코드를 자동으로 병렬 처리할 수 있습니다.이것은 당신이 활성화해야 하는 컴파일러 스위치일 뿐입니다.그러나 OpenMP만큼 잘 작동하지는 않습니다(즉, 항상 성공하는 것은 아니거나 결과 프로그램이 더 느립니다).Intel 웹 사이트: "-parallel(Linux* OS 및 Mac OS* X) 또는 /Qparallel(Windows* OS) 옵션에 의해 트리거되는 자동 병렬화는 병렬화를 포함하는 루프 구조를 자동으로 식별합니다.컴파일 중에 컴파일러는 병렬 처리를 위해 코드 시퀀스를 별도의 스레드로 자동으로 분해하려고 시도합니다.프로그래머의 다른 노력은 필요하지 않습니다."

모든 언어로 동시 프로그래밍을 배우기 위한 좋은 연습은 스레드 풀 구현에서 일하는 것입니다.
이 패턴에서는 미리 몇 개의 스레드를 만듭니다.이러한 스레드는 리소스로 처리됩니다.스레드 풀 개체/구조는 사용자 정의 작업을 실행할 스레드에 할당하는 데 사용됩니다.작업이 완료되면 결과를 수집할 수 있습니다.스레드 풀을 동시성을 위한 범용 설계 패턴으로 사용할 수 있습니다.주요 아이디어는 다음과 유사하게 보일 수 있습니다.

#define number_of_threads_to_be_created 42
// create some user defined tasks
Tasks_list_t* task_list_elem = CreateTasks();
// Create the thread pool with 42 tasks
Thpool_handle_t* pool = Create_pool(number_of_threads_to_be_created);

// populate the thread pool with tasks
for ( ; task_list_elem; task_list_elem = task_list_elem->next) {
   add_a_task_to_thpool (task_list_elem, pool);
}
// kick start the thread pool
thpool_run (pool);

// Now decide on the mechanism for collecting the results from tasks list.
// Some of the candidates are:
// 1. sleep till all is done (naive)
// 2. pool the tasks in the list for some state variable describing that the task has
//    finished. This can work quite well in some situations
// 3. Implement signal/callback mechanism that a task can use to signal that it has 
//    finished executing.

작업에서 데이터를 수집하는 메커니즘과 풀에서 사용되는 스레드의 양은 하드웨어 및 런타임 환경의 요구 사항과 기능을 반영하도록 선택해야 합니다.
또한 이 패턴은 작업을 서로/외부 환경과 "동기화"하는 방법에 대해 설명하지 않습니다.또한 오류 처리가 다소 까다로울 수 있습니다(예: 한 작업이 실패할 경우 수행할 작업).이러한 두 가지 측면을 미리 고려해야 합니다. 스레드 풀 패턴의 사용을 제한할 수 있습니다.

스레드 풀 정보:
http://en.wikipedia.org/wiki/://en.wikipedia.org/wiki/Thread_pool_pattern
http://docs.oracle.com/cd//816-5137/ggedn/index.htmlhttp ://docs.oracle.com/cd/E19253-01/816-5137/ggedn/index.html

하기 위한 문헌pthreads 대한좋은자료:
http://www..com/alp-folder/alp-ch04-threads.pdfhttp ://www.advancedlinuxprogramming.com/alp-folder/alp-ch04-threads.pdf

OP 질문의 "자동 멀티 스레드" 부분을 구체적으로 설명하려면:

병렬 처리를 프로그래밍하는 방법에 대한 정말 흥미로운 관점 중 하나는 MIT가 발명한 Cilk Plus라는 언어로 설계되었고 현재는 Intel이 소유하고 있습니다.위키피디아를 인용하자면, 아이디어는

"프로그래머는 병렬로 안전하게 실행될 수 있는 요소를 식별하여 병렬성을 노출할 책임이 있습니다. 실행 중에 실제로 프로세서 간에 작업을 분할하는 방법을 결정하는 것은 런타임 환경, 특히 스케줄러에 맡겨져야 합니다."

Cilk Plus는 표준 C++의 상위 집합입니다. 몇 키워드를 하고 있습니다._Cilk_spawn,_Cilk_sync,그리고._Cilk_for프로그래머가 프로그램의 일부를 병렬로 태그할 수 있도록 합니다.프로그래머는 코드를 새 스레드에서 실행하도록 요구하지 않으며, 특정 런타임 조건에서 실제로 실행하는 것이 올바른 경우에만 경량 런타임 스케줄러가 새 스레드를 생성하도록 허용합니다.

Cilk Plus를 사용하려면 코드에 Cilk Plus의 키워드를 추가하고 인텔의 C++ 컴파일러로 빌드하기만 하면 됩니다.

C에서 pthread를 사용하여 다중 스레드를 수행할 수 있습니다. 여기 pthread를 기반으로 한 간단한 예가 있습니다.

#include <pthread.h>
#include <stdio.h>

void *mythread1();  //thread prototype
void *mythread2();

int main(){
    pthread_t thread[2];
    //starting the thread
    pthread_create(&thread[0],NULL,mythread1,NULL);
    pthread_create(&thread[1],NULL,mythread2,NULL);
    //waiting for completion
    pthread_join(thread[0],NULL);
    pthread_join(thread[1],NULL);
    
    
    return 0;
}

//thread definition
void *mythread1(){
    int i;
    for(i=0;i<5;i++)
        printf("Thread 1 Running\n");
}
void *mythread2(){
    int i;
    for(i=0;i<5;i++)
        printf("Thread 2 Running\n");
}

만약 그것이 당신의 질문이었다면 당신의 코드는 컴파일러에 의해 자동적으로 멀티 스레드화되지 않습니다.멀티스레딩을 사용할 수 있는지 여부는 코딩에 사용하는 언어가 아니라 코딩하는 대상 플랫폼에 따라 다르기 때문에 C 표준 자체는 멀티스레딩에 대해 아무것도 모른다는 점에 유의하십시오.C로 작성된 코드는 C 컴파일러가 존재하는 거의 모든 것에서 실행될 수 있습니다.C 컴파일러는 C64 컴퓨터(거의 완전히 ISO-99 준수)에도 존재하지만 여러 스레드를 지원하려면 플랫폼에 이를 지원하는 운영 체제가 있어야 하며 일반적으로 적어도 특정 CPU 기능이 있어야 합니다.운영 체제는 거의 전적으로 소프트웨어에서만 멀티스레딩을 수행할 수 있습니다. 이는 매우 느리고 메모리 보호 기능이 없을 것입니다. 하지만 가능한 경우에도 최소한 프로그래밍 가능한 인터럽트가 필요합니다.

따라서 멀티 스레드 C 코드를 작성하는 방법은 전적으로 대상 플랫폼의 운영 체제에 따라 다릅니다.POSIX 적합 시스템(OS X, FreeBSD, Linux 등)과 이를 위한 자체 라이브러리가 있는 시스템(Windows)이 있습니다.일부 시스템에는 라이브러리 이상이 있습니다(예: OS X에는 POSIX 라이브러리가 있지만 C에서 사용할 수 있는 Carbon Thread Manager도 있습니다).

물론 크로스 플랫폼 스레드 라이브러리가 존재하며 일부 최신 컴파일러는 컴파일러가 선택한 대상 플랫폼에서 스레드를 만들기 위해 자동으로 코드를 빌드하는 OpenMP와 같은 것을 지원합니다. 그러나 많은 컴파일러가 지원하지 않으며 지원하는 컴파일러는 일반적으로 완전한 기능을 제공하지 않습니다.일반적으로 "pthreads"라고 더 자주 불리는 POSIX 스레드를 사용하여 가장 광범위한 시스템 지원을 받을 수 있습니다.이를 지원하지 않는 유일한 주요 플랫폼은 Windows이며 여기에서 이와 같은 무료 타사 라이브러리를 사용할 수 있습니다.다른 포트도 여러 개 존재합니다(Cygwin은 확실히 하나를 가지고 있습니다).언젠가 UI를 사용하게 될 경우 wxWidgets 또는 SDL과 같은 교차 플랫폼 라이브러리를 사용할 수 있으며, 두 라이브러리 모두 지원되는 모든 플랫폼에서 일관된 멀티 스레드 지원을 제공합니다.

루프의 반복이 이전의 반복과 독립적이라면 매우 간단한 접근법이 있습니다. 멀티 스레드가 아닌 멀티 프로세싱을 시도해 보십시오.

의 코어와 2개의 코어를 있다고 하면,ntimes는 100, 그 다음에는 100/2=50이므로 첫 번째 버전이 0에서 49까지, 다른 버전이 50에서 99까지 반복되는 두 가지 버전의 프로그램을 만듭니다.둘 다 실행하면 코어가 상당히 많이 사용됩니다.

이는 매우 단순한 접근 방식이지만 스레드 생성, 동기화 등을 방해할 필요는 없습니다.

다른 기능에 걸쳐 스레드를 구현하고 매개 변수와 일부 벤치마크를 전달하는 구체적인 예는 모든 답변에 부족하다고 생각합니다.

// NB:  gcc -O3 pthread.c -lpthread && time ./a.out

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

#define bool    unsigned char
#define true    1
#define false   0

typedef struct my_ptr {
    long n;
    long i;
}   t_my_ptr;

void *sum_primes(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    if (my_ptr->n < 0 ) // handle misused of function
        return (void *)-1;
    bool isPrime[my_ptr->i + 1];
    memset(isPrime, true, my_ptr->i + 1);

    if (my_ptr->n >= 2) { // only one even number can be prime: 2
        my_ptr->n += 2;
    }
    for (long i = 3; i <= my_ptr->i ; i+=2) { // after what only odd numbers can be prime numbers
        if (isPrime[i]) {
            my_ptr->n += i;
        }
        for (long j = i * i; j <= my_ptr->i; j+=i*2) // Eratosthenes' Algo, sieve all multiples of current prime, skipping even numbers.
            isPrime[j] = false;
    }
    //printf("%s: %ld\n", __func__, my_ptr->n); // a) if both 'a' and 'b' activated you will notice that both functions are computed asynchronously.
}

void *sum_square(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    my_ptr->n += (my_ptr->i * my_ptr->i) >> 3;
    //printf("%s: %ld\n", __func__, my_ptr->n); // b) if both 'a' and 'b' activated you will notice that both functions are computed asynchronously.
}

void *sum_add_modulo_three(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    my_ptr->n += my_ptr->i % 3;
}

void *sum_add_modulo_thirteen(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    my_ptr->n += my_ptr->i % 13;
}

void *sum_add_twice(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    my_ptr->n += my_ptr->i + my_ptr->i;
}

void *sum_times_five(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    my_ptr->n += my_ptr->i * 5;
}

void *sum_times_thirteen(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    my_ptr->n += my_ptr->i * 13;
}

void *sum_times_seventeen(void *ptr) {
    t_my_ptr *my_ptr = ptr;
    my_ptr->n += my_ptr->i * 17;
}

#define THREADS_NB 8

int main(void)
{
    pthread_t thread[THREADS_NB];
    void *(*fptr[THREADS_NB]) (void *ptr) =  {sum_primes, sum_square,sum_add_modulo_three, \
    sum_add_modulo_thirteen, sum_add_twice, sum_times_five, sum_times_thirteen, sum_times_seventeen};
    t_my_ptr arg[THREADS_NB];
    memset(arg, 0, sizeof(arg));
    long  iret[THREADS_NB];

    for (volatile long i = 0; i < 100000; i++) {
        //print_sum_primes(&prime_arg);
        //print_sum_square(&square_arg);
        for (int j = 0; j < THREADS_NB; j++) {
            arg[j].i = i;
            //fptr[j](&arg[j]);
            pthread_create( &thread[j], NULL, (void *)fptr[j], &arg[j]); // https://man7.org/linux/man-pages/man3/pthread_create.3.html
        }

        // Wait till threads are complete before main continues. Unless we
        // wait we run the risk of executing an exit which will terminate
        // the process and all threads before the threads have completed.
        for (int j = 0; j < THREADS_NB; j++)
            pthread_join(thread[j], NULL);

        //printf("Thread 1 returns: %ld\n",iret1); // if we care about the return value
    }
    for (int j = 0; j < THREADS_NB; j++)
        printf("Function %d: %ld\n", j, arg[j].n);

    return 0;
}

출력:

Function 0: 15616893616113
Function 1: 41666041650000
Function 2: 99999
Function 3: 599982
Function 4: 9999900000
Function 5: 24999750000
Function 6: 64999350000
Function 7: 84999150000

결론(8개 스레드 사용)

  • pthread 없이 최적화 플래그 포함 -O3: 9.2sd
  • pthread 포함, 최적화 플래그 없음: 31.4sd
  • pthread 및 Optimization 플래그 포함 -O3: 17.8sd
  • pthread 및 Optimization 플래그 -O3 포함 및 pthread_join 제외: 2.0sd.하지만 서로 다른 스레드가 동시에 my_ptr->i에 액세스하려고 하기 때문에 올바른 출력을 계산하지 못합니다.

왜 멀티스레딩이 더 느릴까요?스레드를 시작하는 것은 매우 간단합니다. 주기 측면에서 비용이 많이 들기 때문에 기능이 다소 복잡한지 확인해야 합니다.이 첫 번째 벤치마크는 서로 다른 함수를 계산하기가 매우 쉽기 때문에 약간 편향되어 있습니다.


결론(8개의 스레드 사용), 각 함수의 내용을 sum_primes로 대체(고급 계산으로 이점을 벤치마킹하기 위해)

  • pthread는 사용하지 않지만 자동 벡터화(-O3) 사용: 1mn14.4sd
  • pthread는 있지만 최적화 플래그는 없는 경우: 2mn18.6sd
  • pthread 및 자동 벡터화(-O3) 포함: 54.7sd
  • pthread 사용 시, 자동 벡터화 및 pthread_join 사용 시: 2.8sd.하지만 서로 다른 스레드가 동시에 my_ptr->i에 액세스하려고 하기 때문에 올바른 출력을 계산하지 못합니다.

출력:

Function 0: 15616893616113
Function 1: 15616893616113
Function 2: 15616893616113
Function 3: 15616893616113
Function 4: 15616893616113
Function 5: 15616893616113
Function 6: 15616893616113
Function 7: 15616893616113

이것은 멀티스레딩의 진정한 힘을 조금 더 잘 보여줍니다!


마지막 단어

따라서 복잡한 계산 기능을 갖춘 멀티 스레드가 아니면 스레드를 시작하고 스레드를 결합할 필요가 없는 경우에는 스레드를 시작하는 비용과 결합하는 비용 때문에 가치가 없습니다.하지만 다시 한 번 벤치마킹해 보십시오!

자동 벡터화(-O3를 통해 수행)는 SIMD를 사용하는 데 비용이 들지 않으므로 항상 상당한 양의 결과를 산출합니다.

NB2: 사할수있다니를 할 수 있습니다.iret[j] =스레드의 결과를 저장하기 위해 성공 시 0을 반환합니다.

언급URL : https://stackoverflow.com/questions/3908031/how-to-multithread-c-code

반응형