Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

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.