5-bo‘lim

Oqimlar (threads)

Oqim nima, jarayondan farqi, pthread bilan ishlash, umumiy xotira, poyga holati va oqimlarni qachon ishlatish kerak.

🕑 16 daqiqa o‘qish 📄 953 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Jarayon va oqim
  2. Birinchi oqim
  3. Bir nechta oqim
  4. Poyga holati (race condition)
  5. Oqimlar nimani bo'lishadi?
  6. Oqimdan natija qaytarish
  7. Ajratilgan oqim (detached)
  8. Oqim va jarayon tezligi
  9. Qachon oqim, qachon jarayon?
  10. Oqimlar sonini tanlash
  11. Xulosa

Jarayon yaratish qimmat va ular bir-birining xotirasini ko'rmaydi. Oqim (thread) esa bitta jarayon ichida ishlaydi va xotirani bo'lishadi.

Jarayon va oqim #

Ikki jarayon va ikki oqim Ikkita jarayon Jarayon A Kod O'z xotirasi 1 oqim Jarayon B Kod O'z xotirasi 1 oqim Xotira ajratilgan - almashish qiyin Bitta jarayon, ikki oqim Jarayon Kod (umumiy) Umumiy xotira Oqim 1 (o'z steki) Oqim 2 (o'z steki) Xotira umumiy - almashish oson, lekin xavfli
Har bir oqimning faqat o'z steki alohida bo'ladi
JarayonOqim
XotiraAlohidaUmumiy
StekO'ziO'zi
Yaratish narxiYuqori (~100 mks)Past (~10 mks)
Almashinuv narxiYuqoriPast
Ma'lumot almashishQiyin (IPC kerak)Oson (o'zgaruvchi)
XavfsizlikBiri qulasa - boshqasi tirikBiri qulasa - hammasi
Xato xavfiPastYuqori (poyga holati)

Birinchi oqim #

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

void *ish(void *argument) {
    int raqam = *(int *) argument;

    printf("  Oqim %d ishlayapti\n", raqam);
    return NULL;
}

int main(void) {
    pthread_t oqim;
    int raqam = 1;

    if (pthread_create(&oqim, NULL, ish, &raqam) != 0) {
        printf("Oqim yaratilmadi\n");
        return 1;
    }

    pthread_join(oqim, NULL);       /* oqim tugashini kutamiz */
    printf("Asosiy oqim tugadi\n");

    return 0;
}
Natija
  Oqim 1 ishlayapti
Asosiy oqim tugadi
Kompilyatsiya uchun -pthread kerak
Terminal
gcc -std=c17 -Wall -Wextra -pthread dastur.c -o dastur

-pthread ni unutsangiz:

Natija
undefined reference to `pthread_create'

Bu bayroq kutubxonani bog'laydi va kerakli makroslarni yoqadi. -lpthread emas, aynan -pthread yozing.

Bir nechta oqim #

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

#define OQIMLAR 4

typedef struct {
    int raqam;
    long natija;
} Vazifa;

void *yigindi_hisobla(void *arg) {
    Vazifa *v = arg;
    long jami = 0;

    for (int i = 1; i <= 1000; i++) {
        jami += (long) i * v->raqam;
    }

    v->natija = jami;
    return NULL;
}

int main(void) {
    pthread_t oqimlar[OQIMLAR];
    Vazifa vazifalar[OQIMLAR];

    for (int i = 0; i < OQIMLAR; i++) {
        vazifalar[i].raqam = i + 1;
        vazifalar[i].natija = 0;

        if (pthread_create(&oqimlar[i], NULL, yigindi_hisobla, &vazifalar[i]) != 0) {
            return 1;
        }
    }

    for (int i = 0; i < OQIMLAR; i++) {
        pthread_join(oqimlar[i], NULL);
    }

    for (int i = 0; i < OQIMLAR; i++) {
        printf("Oqim %d natijasi: %ld\n", vazifalar[i].raqam, vazifalar[i].natija);
    }

    return 0;
}
Natija
Oqim 1 natijasi: 500500
Oqim 2 natijasi: 1001000
Oqim 3 natijasi: 1501500
Oqim 4 natijasi: 2002000
Har bir oqimga alohida ma'lumot bering
C
for (int i = 0; i < 4; i++) {
    pthread_create(&oqimlar[i], NULL, ish, &i);    /* XATO! */
}

Bu yerda barcha oqimlar bir xil i o'zgaruvchisiga ko'rsatadi. Sikl davom etgani sari i o'zgaradi va oqimlar noto'g'ri qiymat o'qiydi.

To'g'ri yo'l - yuqoridagi kabi har biriga alohida element berish:

C
pthread_create(&oqimlar[i], NULL, ish, &vazifalar[i]);

Poyga holati (race condition) #

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

#define OQIMLAR 4
#define QADAMLAR 100000

int umumiy_hisob = 0;               /* himoyalanmagan! */

void *oshir(void *arg) {
    (void) arg;

    for (int i = 0; i < QADAMLAR; i++) {
        umumiy_hisob++;             /* xavfli */
    }

    return NULL;
}

int main(void) {
    pthread_t oqimlar[OQIMLAR];

    for (int i = 0; i < OQIMLAR; i++) {
        pthread_create(&oqimlar[i], NULL, oshir, NULL);
    }

    for (int i = 0; i < OQIMLAR; i++) {
        pthread_join(oqimlar[i], NULL);
    }

    int kutilgan = OQIMLAR * QADAMLAR;

    printf("Kutilgan natija : %d\n", kutilgan);
    printf("Natija to'g'rimi? %s\n", umumiy_hisob == kutilgan ? "ha" : "yo'q");

    return 0;
}
Natija
Kutilgan natija : 400000
Natija to'g'rimi? yo'q
hisob++ aslida uch amaldan iborat umumiy_hisob++ 1. registr = xotiradan o'qi 2. registr = registr + 1 3. xotiraga yoz Oqim A o'qidi: 5 yozdi: 6 Oqim B o'qidi: 5 yozdi: 6 Ikki oshirish - lekin natija faqat bittaga oshdi
Yo'qolgan yangilanish (lost update) - eng klassik parallel xato
Poyga holati - eng qiyin xato turi

Yuqoridagi dastur ba'zan to'g'ri natija berishi ham mumkin. Bu uni eng xavfli xato turiga aylantiradi:

  • sinovda ishlaydi, ishlab chiqarishda buziladi;
  • kuchsiz kompyuterda chiqmaydi, kuchli serverda chiqadi;
  • nosozlik tuzatuvchi ostida yo'qoladi (Heisenbug).

Yechim - keyingi bo'limdagi sinxronizatsiya: mutex, atomik amallar yoki umumiy o'zgaruvchidan butunlay voz kechish.

Oqimlar nimani bo'lishadi? #

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

int global_ozgaruvchi = 10;

void *ish(void *arg) {
    (void) arg;

    int mahalliy = 20;              /* oqimning o'z stekida */
    global_ozgaruvchi += 5;         /* umumiy xotirada */

    printf("  Oqim: mahalliy=%d global=%d\n", mahalliy, global_ozgaruvchi);
    printf("  Oqim: PID = asosiy oqim bilan bir xilmi? %s\n",
           getpid() == getppid() ? "yo'q" : "ha");

    return NULL;
}

int main(void) {
    pthread_t oqim;

    pthread_create(&oqim, NULL, ish, NULL);
    pthread_join(oqim, NULL);

    printf("Asosiy: global = %d (oqim o'zgartirdi)\n", global_ozgaruvchi);

    return 0;
}
Natija
  Oqim: mahalliy=20 global=15
  Oqim: PID = asosiy oqim bilan bir xilmi? ha
Asosiy: global = 15 (oqim o'zgartirdi)
Bo'lishiladiHar oqimda alohida
Kod (text)Stek
Global va statik o'zgaruvchilarRegistrlar
Uyum (malloc bilan olingan)Dastur hisoblagichi
Ochiq fayl deskriptorlariOqim identifikatori
Joriy papka, PIDerrno
Signal ishlovchilariUstuvorlik

Oqimdan natija qaytarish #

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

void *kvadrat(void *arg) {
    int son = *(int *) arg;
    int *natija = malloc(sizeof(int));

    if (natija == NULL) {
        return NULL;
    }

    *natija = son * son;
    return natija;                  /* chaqiruvchi free qiladi */
}

int main(void) {
    pthread_t oqim;
    int son = 7;
    void *natija;

    pthread_create(&oqim, NULL, kvadrat, &son);
    pthread_join(oqim, &natija);

    if (natija != NULL) {
        printf("%d ning kvadrati: %d\n", son, *(int *) natija);
        free(natija);
    }

    return 0;
}
Natija
7 ning kvadrati: 49
Mahalliy o'zgaruvchi manzilini qaytarmang
C
void *ish(void *arg) {
    int natija = 42;
    return &natija;        /* XATO: oqim tugagach stek yo'qoladi */
}

Bu C darsligidagi «mahalliy o'zgaruvchi manzilini qaytarmang» qoidasining aynan o'zi.

Uch to'g'ri yo'l:

  1. malloc bilan uyumda joy olish (yuqoridagi misol);
  2. Natijani argument orqali berilgan tuzilmaga yozish;
  3. Global yoki statik o'zgaruvchi (lekin sinxronizatsiya kerak).

Ikkinchi usul eng xavfsiz - malloc/free juftligi ham kerak emas.

Ajratilgan oqim (detached) #

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

void *fon_ishi(void *arg) {
    (void) arg;
    printf("  Fon ishi bajarildi\n");
    return NULL;
}

int main(void) {
    pthread_t oqim;

    pthread_create(&oqim, NULL, fon_ishi, NULL);
    pthread_detach(oqim);           /* join kerak emas */

    sleep(1);                       /* oqim tugashiga vaqt beramiz */
    printf("Asosiy oqim tugadi\n");

    return 0;
}
Natija
  Fon ishi bajarildi
Asosiy oqim tugadi
pthread_join chaqirilmasa - resurs sizadi

join qilinmagan oqim tugaganda, uning resurslari bo'shatilmaydi - xuddi zombi jarayon kabi.

Ikki yechim:

  1. pthread_join - oqimni kutasiz va natijasini olasiz;
  2. pthread_detach - «bu oqim natijasi kerak emas» deysiz, tizim uni o'zi tozalaydi.

Har bir oqim uchun ikkalasidan bittasi bo'lishi shart.

main tugasa - hamma oqim o'ladi
C
int main(void) {
    pthread_create(&oqim, NULL, uzoq_ish, NULL);
    return 0;                       /* oqim yarim yo'lda o'ladi */
}

main funksiyasi tugashi butun jarayonni tugatadi. Barcha oqimlar darhol to'xtaydi - tozalanish imkoni berilmasdan.

Shuning uchun yuqoridagi misolda sleep(1) bor. Amalda esa sleep emas, pthread_join ishlatiladi - u ishonchli.

Oqim va jarayon tezligi #

C
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>

#define MARTA 300

void *bosh_ish(void *arg) {
    (void) arg;
    return NULL;
}

static double olchash_boshla(struct timespec *t) {
    clock_gettime(CLOCK_MONOTONIC, t);
    return 0.0;
}

static double olchash_tugat(const struct timespec *boshi) {
    struct timespec hozir;
    clock_gettime(CLOCK_MONOTONIC, &hozir);

    return (double) (hozir.tv_sec - boshi->tv_sec) +
           (double) (hozir.tv_nsec - boshi->tv_nsec) / 1e9;
}

int main(void) {
    struct timespec t;

    /* oqimlar */
    olchash_boshla(&t);
    for (int i = 0; i < MARTA; i++) {
        pthread_t oqim;
        pthread_create(&oqim, NULL, bosh_ish, NULL);
        pthread_join(oqim, NULL);
    }
    double oqim_vaqti = olchash_tugat(&t);

    /* jarayonlar */
    olchash_boshla(&t);
    for (int i = 0; i < MARTA; i++) {
        pid_t bola = fork();

        if (bola == 0) {
            _exit(0);
        }

        wait(NULL);
    }
    double jarayon_vaqti = olchash_tugat(&t);

    printf("%d marta yaratildi\n", MARTA);
    printf("Oqim jarayondan tezroqmi? %s\n",
           oqim_vaqti < jarayon_vaqti ? "ha" : "yo'q");

    /* aniq raqamlar uchun:
    printf("  oqim    : %.6f soniya\n", oqim_vaqti);
    printf("  jarayon : %.6f soniya\n", jarayon_vaqti);
    */

    return 0;
}
Natija
300 marta yaratildi
Oqim jarayondan tezroqmi? ha

Bir sinovda natija shunday chiqdi:

Natija
  oqim    : 0.010421 soniya
  jarayon : 0.087634 soniya

Oqim yaratish taxminan 8 barobar tezroq bo'ldi - chunki xotira xaritasini nusxalash shart emas.

Qachon oqim, qachon jarayon? #

Tanlov qoidalari
VaziyatTanlov
Umumiy ma'lumot ustida tez ishlashOqim
Bir vazifani ko'p yadroga bo'lishOqim
Grafik interfeys + fon ishiOqim
Ishonchsiz kodni ishga tushirishJarayon
Xatolik butun tizimni buzmasinJarayon
Turli huquqlar kerakJarayon
Turli dasturni ishga tushirishJarayon (fork + exec)

Brauzerlar jarayonni tanlaydi: har bir varaq alohida jarayon. Bir sayt qulasa, boshqa varaqlar tirik qoladi va saytlar bir-birining xotirasini o'qiy olmaydi.

Ma'lumotlar bazasi serverlari esa ko'pincha oqimni tanlaydi - umumiy kesh va tez almashinuv muhimroq.

Oqimlar sonini tanlash #

C
#include <stdio.h>
#include <unistd.h>

int main(void) {
    long yadrolar = sysconf(_SC_NPROCESSORS_ONLN);

    printf("Mavjud yadrolar aniqlandi: %s\n", yadrolar > 0 ? "ha" : "yo'q");
    printf("Kamida bitta yadro bormi? %s\n", yadrolar >= 1 ? "ha" : "yo'q");

    /* aniq sonni ko'rish uchun: */
    /* printf("Yadrolar soni: %ld\n", yadrolar); */

    return 0;
}
Natija
Mavjud yadrolar aniqlandi: ha
Kamida bitta yadro bormi? ha
Nechta oqim yaratish kerak?
Ish turiTavsiya
Protsessorga bog'liq (hisoblash)Yadrolar soniga teng
I/O ga bog'liq (disk, tarmoq)Yadrolardan ancha ko'p

Hisoblash ishida ortiqcha oqim foyda bermaydi - ular navbat kutadi va kontekst almashinuvi qo'shimcha yuk beradi.

I/O ishida esa oqimlar ko'pincha bloklangan holatda turadi, shuning uchun ularni ko'p yaratish mumkin.

Amalda esa har safar oqim yaratmasdan, oqimlar hovuzi (thread pool) ishlatiladi - oldindan yaratilgan oqimlar vazifalarni navbatdan oladi.

Amaliy topshiriq
  1. Bitta oqim yaratib, unga matn chiqartiring.
  2. To'rt oqim yaratib, har biriga alohida raqam bering.
  3. Sikldagi &i xatosini qilib, natijani kuzating.
  4. Poyga holatini takrorlang va natija har safar o'zgarishini ko'ring.
  5. QADAMLAR ni 100 ga kamaytiring - xato hali ham chiqadimi?
  6. Oqimdan malloc bilan natija qaytaring.
  7. Mahalliy o'zgaruvchi manzilini qaytarib, nima bo'lishini ko'ring.
  8. pthread_join siz oqim yaratib, main ni darhol tugating.
  9. pthread_detach bilan fon oqimini yarating.
  10. Oqim va jarayon yaratish tezligini o'z kompyuteringizda o'lchang.

Xulosa #

  • Oqim - bitta jarayon ichidagi mustaqil bajarilish yo'li.
  • Oqimlar xotirani bo'lishadi, faqat stek har birida alohida.
  • Oqim yaratish jarayon yaratishdan ancha tezroq.
  • Kompilyatsiyada -pthread bayrog'i kerak.
  • Har bir oqimga alohida ma'lumot bering - sikl o'zgaruvchisi manzilini emas.
  • Umumiy o'zgaruvchini himoyasiz o'zgartirish - poyga holati.
  • hisob++ aslida uch amal, shuning uchun u atomik emas.
  • Poyga holati ba'zan to'g'ri natija beradi - eng xavfli xato turi.
  • Har bir oqim uchun join yoki detach bo'lishi shart.
  • main tugasa barcha oqimlar darhol o'ladi.
  • Ishonchsiz kod uchun jarayon, umumiy ma'lumot uchun oqim.

Keyingi bo'limda protsessor vaqti qanday taqsimlanishini - rejalashtirish algoritmlarini ko'ramiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.