Vlákna v C na Linuxu - 2.
3.12.2022
V minulém příspěvku o vláknech jsme si ukázali základy práce s vlákny v jazyce C v systému Linux. Nyní si ukážeme trochu více, zejména jak vlákna "ovládat zvenku", konkrétně předčasně ukonči a také jak ho "zapauzovat" a později pokračovat v provádění tam kde jsme ho zastavili.
Nejprve si pro vyzkoušení připravíme funkci vlákna, které bude v sekundovém intervalu vypisovat inkrementované číslo. Aby nám výpis do terminálu "neutíkal pryč", napíšeme si pomocné funkce pro vymazání řádky a celého terminálu a výpis textu na požadovanou pozici
static void term_erase(void)
{
(void)fprintf(stdout, "\033[1;1H\033[2J");
}
static void term_goto(int row, int col)
{
(void)fprintf(stdout, "\033[%d;%df", row, col);
}
static void term_erase_cur_line(void)
{
(void)fprintf(stdout, "\033[2K");
}
A takto bude vypadat funkce vlákna, které standardně poběží cca 40 sekund, a které budeme ovládat zvenku.
static void* thread_func_counter(void* arg)
{
if (0 != pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL))
{
perror("pthread_setcancelstate");
exit(EXIT_FAILURE);
}
for (size_t i = 0; i < 40; i++)
{
term_goto(2, 1);
term_erase_cur_line();
printf("%d", (int)i);
fflush(stdout);
sleep(1);
if (20 == i)
{
if (0 != pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL))
{
perror("pthread_setcancelstate");
exit(EXIT_FAILURE);
}
}
}
return (void*)0;
}
Ve výchozím stavu lze vlákno ukončit zvenku zavoláním funkce int pthread_cancel(pthread_t thread);. Pokud bychom chtěli z nějakého důvodu zmenožnit toto ukončení zvenku, můžeme tuto vlastnost nastavit vláknu pomocí funkce int pthread_setcancelstate(int state, int *oldstate);. Jak je vidět v předchozím výpisu, vyzkoušíme zakázat zrušení vlákna až do hodnoty počítadla 20.
Pokud jde o "zapauzování" vlákna, využijeme signály a funkci int pause(void); která přeruší vlákno, v rámci jehož funkce je zavolána a vlákno pokračuje až po přijetí nějakého signálu. Pro poslání signálu specifickému vláknu použijeme funkci int pthread_kill(pthread_t thread, int sig);.
Nadefinujeme si vlastní hodnoty signálů a obslužnou funkci signálu:
#define MY_SIGNAL_PAUSE SIGRTMIN
#define MY_SIGNAL_CONTINUE SIGRTMIN+1
static void on_signal(int nsig)
{
term_goto(3, 1);
term_erase_cur_line();
printf("signal: %d", (int)nsig);
fflush(stdout);
if (MY_SIGNAL_PAUSE == nsig)
{
term_goto(3, 1);
term_erase_cur_line();
printf("signal pause");
fflush(stdout);
pause();
}
else if (MY_SIGNAL_CONTINUE == nsig)
{
term_goto(3, 1);
term_erase_cur_line();
printf("signal continue");
fflush(stdout);
}
}
Ve funkci main si ještě nastavíme terminál tak abychom četli ze vstupu jednotlivé ovládací znaky bez nutnosti zadávat Enter a zavoláme vlastní funkcí pro vyzkoušení zrušení, zapauzování a pokračování v běhu vlákna. Vše je vidět v následujícím výpisu celého programu
/* gcc -Wall -std=c99 -pthread main.c */
#define _DEFAULT_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>
#define MY_SIGNAL_PAUSE SIGRTMIN
#define MY_SIGNAL_CONTINUE SIGRTMIN+1
static struct termios _termios;
pthread_t _thread_counter;
static void term_erase(void)
{
(void)fprintf(stdout, "\033[1;1H\033[2J");
}
static void term_goto(int row, int col)
{
(void)fprintf(stdout, "\033[%d;%df", row, col);
}
static void term_erase_cur_line(void)
{
(void)fprintf(stdout, "\033[2K");
}
static void on_signal(int nsig)
{
term_goto(3, 1);
term_erase_cur_line();
printf("signal: %d", (int)nsig);
fflush(stdout);
if (MY_SIGNAL_PAUSE == nsig)
{
term_goto(3, 1);
term_erase_cur_line();
printf("signal pause");
fflush(stdout);
pause();
}
else if (MY_SIGNAL_CONTINUE == nsig)
{
term_goto(3, 1);
term_erase_cur_line();
printf("signal continue");
fflush(stdout);
}
}
static void* thread_func_counter(void* arg)
{
if (0 != pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL))
{
perror("pthread_setcancelstate");
exit(EXIT_FAILURE);
}
for (size_t i = 0; i < 40; i++)
{
term_goto(2, 1);
term_erase_cur_line();
printf("%d", (int)i);
fflush(stdout);
sleep(1);
if (20 == i)
{
if (0 != pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL))
{
perror("pthread_setcancelstate");
exit(EXIT_FAILURE);
}
}
}
return (void*)0;
}
static void test_pause(void)
{
int iret = pthread_create(&_thread_counter, NULL,
&thread_func_counter, NULL);
if (0 != iret)
exit(EXIT_FAILURE);
term_erase();
int ch;
while ((ch = getchar()) != 'q')
{
if ('c' == ch)
{
pthread_cancel(_thread_counter);
}
else if ('p' == ch)
{
pthread_kill(_thread_counter, MY_SIGNAL_PAUSE);
}
else if ('r' == ch)
{
pthread_kill(_thread_counter, MY_SIGNAL_CONTINUE);
}
}
}
int main(int argc, char** argv)
{
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = on_signal;
sigaction(MY_SIGNAL_CONTINUE, &sa, NULL);
sigaction(MY_SIGNAL_PAUSE, &sa, NULL);
/* nastaveni terminalu */
tcgetattr(STDIN_FILENO, &_termios);
struct termios ti;
if (tcgetattr(STDIN_FILENO, &ti) == -1)
return errno;
ti.c_lflag &= ~ECHO;
ti.c_lflag &= ~ICANON;
if (0 != tcsetattr(STDIN_FILENO, TCSAFLUSH, &ti))
return errno;
cfmakeraw(&ti);
test_pause();
/* reset terminalu */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &_termios);
term_erase();
printf("konec\n");
return EXIT_SUCCESS;
}
Program sestavíme pomocí GCC například příkazem:
$ gcc -Wall -std=c99 -pthread main.c
Funkčnost si každý může vyzkoušet spuštěním vytvořeného souboru a.out a ovládání znaky c, p, r, q jak je zřejmé ze zdrojového kódu.