Linux-бум, Програмування, Статті, Бібліотека Лінуксцентру, – експерт з Linux та вільного

Останнім часом багато провідних виробників ПО оголошують про перенесення своїх продуктів на систему Linux - вільно розповсюджувану операційну систему. В інтернеті постійно з'являються нові сайти, присвячені цій системі. В наявності бум навколо Linux, який не тільки не замовкає, а стає все сильнішим і потужнішим.

Linux, як клон Unix, зараз підтримує багатозадачність і многопотоковость, тобто. в системі одночасно може працювати кілька завдань (процесів), і кожна із завдань може виконуватися у кілька потоків.

Спочатку розглянемо, що таке потік: потік виконання - це елемент коду програми, що виконується послідовно. Більшість програм - однопотокові програми. Багатопотокова програма одночасно може виконуватися у кількох окремих потоках.

Якщо завдання виконується на багатопроцесорній машині, то всі її потоки можуть виконуватися одночасно, підвищуючи таким чином продуктивність виконання завдання. Продуктивність багатопотокової програми можна покращити навіть на однопроцесорній системі. Наприклад, якщо один із потоків програми блокується якимось системним викликом або чекає надходження даних, в цей час виконується інший потік.

Між процесами та потоками існують відмінності. Під процесами розуміється програма що у стадії виконання. Скажімо, shell в UNIX - це процес, який створюється при вході користувача в систему. Кожна команда створює новий процес. Відповідно до термінології UNIX - це породжений процес, який виконує команду від імені користувача. Потоки - це частина процесу, і вони використовують сегменти даних та коду спільно.

Для багатопотокового програмування існує два основних стандарти: багатопотокові API Solaris (Sun Microsystems) таAPI POSIX.1c У Linux використовується API POSIX.1c. Але якщо бути абсолютно точним, то в Linux є системний виклик clone(), на основі якого і побудовано API для роботи з потоками, що відповідають стандарту POSIX.1c з незначними винятками.Постановка задачі

Досить часто в інженерних розрахунках необхідно зробити обчислення матриці, елементами якої є функції, а точніше значення функції з певними параметрами. Розглянемо наступну матрицю A розмірністю 4x4: f(X11) f(X12) f(X13) f(X14) f(X21) f(X22) f(X23) f(X24) f(X31) f(X32) f(X33) f(X34) f(X41) f(X42) f(X43) f(X44) де f(x) - обчислювана функція Xij - аргументСтандартний підхід до обчислень: переваги та недоліки

Для обчислення елементів даної матриці зазвичай використовують наступний фрагмент коду: Прийнявши, що вихідні дані (Xij) зберігаються в масиві X, а вихідні в S:

int SIZE_I = 4; int SIZE_J = 4; double X[SIZE_I][SIZE_J]; double S[SIZE_I][SIZE_J]; . double f(double x) . //якісь обчислення > main_evalution() for (int i=0;i for (int z=0; z // обчислюємо елемент матриці X[i][z] = f(S [i][z]); > >

Після виконання цього коду матриця X буде заповнена обчисленими даними. Перевага цього підходу - простота реалізації. Недолік - під час роботи на потужній машині (особливо з кількома процесорами) неповне використання обчислювальних ресурсів.

Багатострумові обчислення: переваги та недоліки

Для перетворення попереднього прикладу в багатопотокове завдання, необхідно зробити маленькі зміни - функція, що обчислює значення, має працювати окремим потоком.

int SIZE_I = 4; int SIZE_J = 4; doubleX[SIZE_I][SIZE_J]; double S[SIZE_I][SIZE_J]; struct DATA_ double x; int i; int z; > typedef struct DATA_ DATA; double f(double x) //якісь обчислення > void *thread_f(void *arg) //функція для обчислення елемента матриці DATA* a = (DATA*)arg; //перетворюємо дані X[a->i][a->z] = f([a->x]); // обчислюємо > main_evalution() pthread_t thread; //ідентифікатор потоку DATA * arg; //дані передачі у потік for (int i=0;i for (int z=0; z // створюємо arg = new DATA; // ініціалізуємо дані arg->i = i; arg->z = z;arg->x = S[i][z]; , thread_f, (void *) arg); //переводимо в від'єднаний стан pthread_detach(thread); > > >

В результаті змін, обчислення кожного елемента буде відбуватися в окремому потоці. Недоліком такого методу є складність - необхідно завжди враховувати те, що дві нитки можуть звернутися до тих самих даних - одна для читання, інша для запису, і в такому разі не можна гарантувати достовірність даних. Тобто. необхідно встановлювати/перевіряти блокування, забезпечувати синхронізацію виконання тощо. Гідність – підвищення продуктивності. Так було в нашому прикладі, процес чекає виконання всіх ниток, тобто. він не чекає коли всі елементи матриці заповнюватимуться, а продовжує свою роботу. Якщо подальша робота програми залежить від отриманих обчислень, можна призупинити основний процес до завершення всіх ниток.Функції для роботи з потоками

Для роботи з потоками існують такі основні функції: pthread_create(pthread_t *tid, const pthread_attr_t *attr, void*(*function)(void*), void* arg) - створює потік длявиконання функції функції. Як параметр потокової функції передається покажчик arg. Індентифікатор нового потоку повертається через TID. Потік створюється із параметрами attr. · pthread_mutex_init(pthread_mutex_t* lock, pthread_mutexattr_t *attr) - ініціалізує взаємовиключне блокування. attr - містить атрибути для взаємовиключного блокування. Якщо attr == NULL, використовуються установки за замовчуванням. · pthread_mutex_destroy(pthread_mutex_t* lock) - видаляє взаємовиключне блокування. · pthread_mutex_lock(pthread_mutex_t* lock) - встановлює блокування. Якщо блокування було встановлене іншим процесом, поточний процес зупиняється до зняття блокування іншим процесом. · pthread_mutex_unlock(pthread_mutex_t* lock) - знімає блокування. · pthread_join(pthread_t tid, void **statusp) - очікує завершення невід'єднаного процесу, результат повертається функцією зберігається в statusp. · pthread_detach (pthread_t tid) - від'єднує процес. Це можна задати під час створення процесу, встановивши атрибут detachstate викликом pthread_attr_setdetachstate. · pthread_exit(void *status) - завершує процес, статус передається виклику pthread_join, подібний до exit(). Але виклик exit() у процесі призведе до завершення всієї програми. Процес завершується двома шляхами - викликом pthread_exit() або завершенням потокової функції. Якщо процес невід'єднаний, то при його завершенні ресурси, виділені процесу, не звільняються до виклику pthread_join(). Якщо процес від'єднаний - ресурси звільняються після її завершення.Приклад програми

Ця програма запитує користувача параметри матриці аргументів, і використовуючи потоки, заповнює матрицю результатами обчислень. Цю програму необхіднокомпілювати з бібліотекою pthread (саме в ній знаходяться всі функції для роботи з потоками) і задавши _REENTRANT: g++ -D_REENTRANT -o threads threads.c -lpthread Цей код перевірявся на RedHat Linux 6.0

* threads.c * simple pthread API demo * autor: Tarasenko Volodymyr * e-mail: [email protected] * Компілювати: * g++ -D_REENTRANT -o threads threads .c -lpthread */ #include #include #include #include #define SIZE_I 2 #define SIZE_J 2 float X[SIZE_I ][SIZE_J]; float S[SIZE_I][SIZE_J]; int all = 0; struct DATA_ double x; int i; int z; >; typedef struct DATA_ DATA;

pthread_mutex_t lock; //Виключає блокування

// Функція для обчислень double f(float x) if (x>0) return log(x); else return x; > // Потокова функція для обчислень void *thread_f(void *arg) DATA* a = (DATA*) arg;

X[a->i][a->z] = f(a->x); // встановлюємо блокування pthread_mutex_lock(&lock); // змінюємо глобальну змінну ++all; // знімаємо блокування pthread_mutex_unlock(&lock);

delete a; // видаляємо дані return NULL; > // Потокова функція для введення void *input_thr(void *arg) DATA* a = (DATA*) arg; //pthread_mutex_lock(&lock); printf("S[%d][%d]:", a->i, a->z); scanf("%f", &S[a->i][a->z]); //pthread_mutex_unlock(&lock); delete a; return NULL; > int main() //масив ідентифікаторів потоків pthread_t thr [SIZE_I + SIZE_J]; //ініціалізація виключає блокування pthread_mutex_init(&lock, NULL); DATA *arg; // Введення for (int i = 0; i for (int z = 0; z arg = new DATA; arg-> i = i; arg-> ;z = z; // створюємо потік для введення pthread_create(&ththr[i+z], NULL, input_thr, (void *)arg); > > //Очікуємо завершення всіх потоків //ідентифікатори потоків зберігаються в масиві for(int i = 0; i pthread_join(thr[i], NULL); > //Обчислення printf("Start calculation\n"); arg->i = i;arg->z = z;arg->x = S[i][z]; pthread_t thread; &thread, NULL, thread_f, (void *)arg); // переводимо в від'єднаний режим pthread_detach(thread); > > / Основний процес "засинає" на 1с sleep(1); // Чи всі завершилися? printf("finished %d threads. (all //Друк результатів for (int i=0;i for (int z=0; z printf("X[%d][%d] = %f\) t", i, z, X[i][z]); > printf("\n"); > &lock); return 0; >

Після запуску програма ініціалізує виключне блокування та починає введення даних. В даному випадку, як приклад введення зроблено з потоків, без будь-яких блокувань вводу/виводу, щоб показати, що потоки працюють одночасно і коли один зупиняється, інші продовжують працювати. Основний процес очікує завершення всіх потоків викликом pthread_join(). Тільки після завершення всіх потоків відбувається перехід до другої частини програми - обчислень. Для обчислень використовуються від'єднані потоки, від'єднання відбувається викликом pthread_detach(). Після завершення обчислень в потоці відбувається збільшення змінної all на одиницю, і потік завершує роботу. Для гарантування правильності змін застосовується блокування, що виключає. Після затримки основного процесу на 1 сек., перевіряємокількість завершених потоків, і якщо всі потоки завершили обчислення, виводимо результат роботи. Показаний приклад буде корисним при вирішенні багатьох завдань. Особливо при розрахунках у галузі обробки металів тиском, при вирішенні яких часто використовуються методи кінцевих елементів або методи граничних елементів. Ці методи характеризуються великими обчисленнями, що з матрицями та його заповненням. У більшості випадків елементом матриці є результат складних обчислень, таких як розв'язання інтегральних рівнянь. Застосування багатострумового підходу дозволить збільшити швидкість та продуктивність обчислень. Але, як показано, це призводить до ускладнення реалізації обчислень.