Práce s vlákny v C

Práce s vlákny v C
« kdy: 18. 01. 2021, 18:33:07 »
Ahoj, chtel bych se trochu seznamit s programovanim vicevlaknovych programu v C na linuxu. Mam totiz napsany vlastni jednovlaknovy server a domnivam se, ze pokud mi client zacne delat nejakou slozitou operaci napr. pracovat s databazi (pouzivam SQLite v kombinaci s json-c), tak mi to blokuje dalsi klienty. Abych do stavajiciho jiz odladeneho programu co nejmene zasahoval, vymyslel jsem si tento postup...pokud bude chtit client pristupovat do databaze, tak se tento pozadavek otevre v novem vlakne a jakakoliv dalsi operace totoho clienta bude blokovana dokud vlakno neskonci.

Zjednodusene jsem se to pokusil napsat do kodu, tak jak bych to chtel realizovat a ten zde predkladam. Muj dotaz tedy zni, jestli takto napsany kod bude korektne fungovat a pokud ne, tak jak by to melo vypadat? Ani me nezajima jestli je to takto vhodne resit apod., jde mi jen o princip. Dik za radu.

Kód: [Vybrat]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

static int init_function(void *arg);
static void *handle_function(void *arg);

static pthread_t thread_var = -1;

int main(int argc, char *argv[]){
  int ret; 
   
  //printf("%ld, %ld\n", thread_var, (long)&thread_var); 
   
  while(1){
    ret = init_function((void *)&thread_var);
    if(ret == 1){
      printf("vytvoreno nove vlakno\n");
      }
    //else if(ret == -1){
    //  printf("vlakno se nepovedlo vytvorit\n");
    //  }
    //else if(ret == -2){
    //  printf("vlakno stale bezi\n");
    //  }
   
    //printf("%ld, %ld\n", thread_var, (long)&thread_var); 
    }
 
  return 0;
  }

static int init_function(void *arg){
  pthread_t *p_thread = (pthread_t*)arg;

  if(*p_thread != -1){
    return -2; 
    } 
   
  if(pthread_create(&thread_var, NULL, handle_function, arg)){
    return -1;
    }
 
  //printf("%ld, %ld\n", thread_var, (long)&thread_var);
 
  pthread_detach(thread_var); 
  return 1;
  }

static void *handle_function(void *arg){ 
  pthread_t *p_thread = (pthread_t*)arg; 
 
  //printf("%ld, %ld\n", *p_thread, (long)p_thread);
  sleep(5);
  printf("konec vlakna\n");
 
  *p_thread = -1;
 
  return NULL; 
  }
« Poslední změna: 18. 01. 2021, 21:38:57 od Petr Krčmář »


Re:prace s vlakny v C
« Odpověď #1 kdy: 18. 01. 2021, 19:06:20 »
Jednoznačne by som sa v takom prípade pozeral smerom https://github.com/libuv/libuv. Môžeš síce použiť pthreads a všetko robiť ručne, ale ak nemáš vyslovene dôvod prečo to tak robiť, určite by som skúsil to libuv, minimálne si prečítať čo to dokáže všetko.

Re:prace s vlakny v C
« Odpověď #2 kdy: 18. 01. 2021, 19:07:54 »
Vlakna tak funguju, ale musis si dat pozor na synchronizaciu. Pri kompletne oddelenych resourcoch to bude fungovat dobre, ale ked nieco zdielas alebo volas z viac vlaken tu istu kniznicu, tak to musis zamykat, ked nie je dokumentovany opak.

Urcite musis zamykat low level volania ako write(); inak sa vystupy mozu prekryvat. V knizniciach byvaju zamky, ale nie je to pravidlo.

Re:prace s vlakny v C
« Odpověď #3 kdy: 18. 01. 2021, 19:49:20 »
O tom zamykani jsem uz neco malo cetl, ale podle me v tomto pripade neni nutne. System je postaven tak, ze kazdy client je jednoznacne pojmenovany a ma ke svemu jmenu prirazenou jednoznacnou databazi. Nemuze do ni tedy vstoput nikdo jiny, protoze system neumoznuje dva stejne pojmenovane clienty zaroven. Zatim to tedy asi pouziju tak, jak jsem si usmyslel dokud se mi to nekde nezhrouti. Mozna misto
Kód: [Vybrat]
static pthread_t thread_var pouziju neco jako
Kód: [Vybrat]
atomic_long thread_varPokud spravne chapu princip atomickych promenych, tak by ten program mohl maximalne nefungovat.

qelurg

  • ****
  • 372
    • Zobrazit profil
    • E-mail
Re:prace s vlakny v C
« Odpověď #4 kdy: 18. 01. 2021, 20:43:08 »
To ze mas pro kazdeho klienta vlastni databazovy soubor jeste neznamena, ze ta knihovna nema treba nejaky spolecny buffer.


jouda2

Re:prace s vlakny v C
« Odpověď #5 kdy: 18. 01. 2021, 21:14:08 »
No vždycky je dobré něco času věnovat čtení dokumentace, keyword "thread safety", a ideálně i nějaké stack overflow etc. Ony i thread-safe knihovny mívají občas nějaká "ale" - třeba že handler může otevřít kdokoli, ale result musíte číst ze stejného threadu který dělal query. Nebo že některé inicializace safe nejsou a je potřeba je chránit mutexy.

Jinak ta proměnná by měla být pokud se nepletu volatile, jinak Vám do toho (někdy) hodí vidle už optimalizace kompileru, a druhák cokoli složitějšího než správně alignovaný int *) na intelu stejně musíte jet přes nějaké zamykání (jestli jde o rychlé přístupy, tak mutex/futex je dělo na vrabce a stačí spinlocky z rodiny __sync_bool_compare_and_swap() ).

Ale k pthreads - no fungují ale nic lepšího nemáme (ukončování zlobivých threadů je celkem nic moc když se spolehnete jen na to co funguje všude ale to brzo zjistíte sám)

*) Proboha tuhle platform dependent prasárnu neberte jako návod. Ono je to složitější, dost se plete C a C++, taky záleží jestli pro nějakou platformu máte jen starší céčko tak používat nové featury není správná cesta, takže konzervativní je minimalizovat přístup ke sdíleným objektům a tam kde je to nutné, tam ho zamykat.
« Poslední změna: 18. 01. 2021, 21:19:59 od J ouda »

jouda2

Re:prace s vlakny v C
« Odpověď #6 kdy: 18. 01. 2021, 21:31:13 »
Už mě to nenechá editnout.
Další věc, tenhle postup funguje na holém železe, u userspacu budete mít další problém (který pro teď klidně ignorujte, fungovat to bude, jen je třeba o tom vědět že to není úplně ok). Když si vezmete že jeden thread lockne resource, do toho mu kernel sebere kvantum, tak bude případný další thread viset než ho ten přerušený znova uvolní. Pro to co píšete to je jedno (desetiny sekundy za uherskej rok u příkladu na naučení se). Ale do budoucna je dobré mít pro lock/unlock makra a pak to časem udělat dobře (futexy pro linux, něco přenositelnějšího i jinam)

Re:prace s vlakny v C
« Odpověď #7 kdy: 18. 01. 2021, 21:44:22 »
Jinak ta proměnná by měla být pokud se nepletu volatile
Volatile použité na sychronizaci v multithread aplikaci je vždy chyba. Volatile zaručuje jen to, že se vykoná čtení/zápis z paměti a ne z registru, ale z hlediska sychronizace mezi vlákny je to prakticky k ničemu. Není tam žádná memory bariéra, není zaručena atomicita, CPU může udělat read/write reorediring na HW úrovni... Je třeba používat standardní sychronizační primitiva (mutex), které všechno tohle řeší, volatile určitě ne.

jouda2

Re:prace s vlakny v C
« Odpověď #8 kdy: 18. 01. 2021, 21:46:25 »
...sakra ten timeout je malý. Pro Vás je asi nejvhodnější používat na synchronizaci int pthread_mutex_lock(pthread_mutex_t *mutex); a co je obvykle pod tím je tady https://eli.thegreenplace.net/2018/basics-of-futexes/

Re:prace s vlakny v C
« Odpověď #9 kdy: 18. 01. 2021, 21:50:59 »
Naozaj treba prejst vsetky kniznice. Zavislosti to cele este komplikuju - treba ked pouzivas kniznicu A a potom kniznicu B, ktora pouziva A, tak je treba rovnakymi zamkami zamykat pouzitia A a B.
V tvojom pripade precitaj https://sqlite.org/threadsafe.html a https://stackoverflow.com/questions/26374323/is-the-json-c-library-thread-safe a pripadne podobne pre vsetky ostatne implementacie, ktore pouzivas.

Volatile nepouzivaj ked nepracujes s HW - garantuje to veci, ktore nutne nechces (premenna sa nedrzi v registroch) a negarantuje to veci, ktore chces - negarantuje to atomicitu zapisu premennej, reordering na urovni HW atd. Ked musis nieco zdielat, tak pouzivaj bud atomic builtins (https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html) alebo memory barriers. Alebo v najlepsom pripade zamky.

U threadov sa tazko hladaju chyby, takze skusit to nestaci. Na niektore bugy pouzi kompilaciu cez Clang s -fsanitize=thread alebo to zbehni cez valgrind --tool=helgrind.
« Poslední změna: 18. 01. 2021, 21:53:42 od branchman »

Re:Práce s vlákny v C
« Odpověď #10 kdy: 19. 01. 2021, 09:35:11 »
Diky za pripominky. Pokusim se to shnout...
1. muj jednoduchy priklad nebyl vylozene vyvracen ani pohanen a lze ho pouzit
2. pokud budou pouzity nejake knihovny, musi byt bud "thread safety" nebo je potreba pouzit nejakou pokrocilejsi techniku spravy vlaken aby se nestalo to, ze vlakna budou prustupovat k jednomu zdroji, ktery si budou navzajem menit napr. vyrovnavaci bufer nebo nejaky statusovy priznak. Zde zminim napr. mutex.
3. pri hledani problemu muze pomoci najeky ladici nastroj napr. valgrind apod.

Overil jsem si, ze knihovna pro SQlite je "thread safety", protoze kod:
Kód: [Vybrat]
ret = sqlite3_config(SQLITE_CONFIG_SERIALIZED);

vraci:
Kód: [Vybrat]
SQLITE_OK

coz podle dokumentace znamena, ze je mozne pouzit tento mod pracujici s vice vlakny. Tato knihovna se tedy zda byt OK.

U json-c si nejsem vubec jistej, protoze mi asi chybi zkusenosti s pochopenim principu. Tam je ale situace trochu jina, protoze json se pouziva pouze jednou pri uplne prvnim pripojeni klienta o krerem system nema zadne informace. Jakmile si informace vytvori, uz se pro tohoto clienta nikdy nepouzije. Volani funkci teto knihovny tedy neni potreba do jineho vlakna davat.

Zaverem mi tedy vyplyva, ze je pokud se mi to povede odladit a bude to fungovat, tak bych na vetsi problemy (nez uz ted mam) nemel narazit.

anonacct

Re:Práce s vlákny v C
« Odpověď #11 kdy: 19. 01. 2021, 10:22:25 »
Mě by zajímalo, co konkrétně čeká autor dotazu že se tady dozví?

  - Lze zpracovávat požadavky klientů v samostatném vlákně? Ano, ale vytvářet nové vlákno per request může být drahé.
  - Musí si hlídat přístup ke zdrojům v MT aplikaci? Ano.
  - Existuje i jiný způsob? Existuje, třeba dělat to asynchronně např. s pomocí libuv.

Jediná možnost, jak se dozvědět zda autorovo konkrétní řešení funguje je napsat testy. Pokud budou testy modelovat veškeré podporované scénáře a budou fungovat, tak je to řešení asi funkční. No a pokud už budou testy, tak není problém použít třeba thread sanitizer a další nástroje na detekci chyb.

Re:Práce s vlákny v C
« Odpověď #12 kdy: 19. 01. 2021, 15:37:34 »
Jsem presvedecn, ze dotaz znel v podstate jasne...Ptal jsem se na to jestli ten muj prilozenej kod bude fungovat korektne. A cekal jsem ze dostanu odpoved. Take jsem presvedecn, ze jsem jednoznacnou odpoved nedostal, ale urcite ted vim neco vice o konfiguraci SQLite. Rada, ze existuje jiny zpusob napr. libuv neni odpoved na moji otazku. Tim se samozrejme nechci nikoho kdo prispival dotknout, na druhou stranu by me zajimalo kam uzivatel anonacct miri?

Re:Práce s vlákny v C
« Odpověď #13 kdy: 19. 01. 2021, 16:54:13 »
Ptal jsem se na to jestli ten muj prilozenej kod bude fungovat korektne.
Nebude fungovat korektně, máš tam data race a helgrind ti to řekne. K proměnné thread_var přistupuješ z více vláken bez synchronizace.
Kód: [Vybrat]
g++ -lpthread -o test -g main.cc
valgrind --tool=helgrind ./test

==5579== Possible data race during read of size 8 at 0x10C048 by thread #1
==5579== Locks held: none
==5579==    at 0x1091AF: init_function(void*) (main.cc:37)
==5579==    by 0x10917F: main (main.cc:17)
==5579==
==5579== This conflicts with a previous write of size 8 by thread #2
==5579== Locks held: none
==5579==    at 0x109232: handle_function(void*) (main.cc:58)
==5579==    by 0x483C8B6: ??? (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_helgrind-amd64-linux.so)
==5579==    by 0x4886FA2: start_thread (pthread_create.c:486)
==5579==    by 0x4CBA4CE: clone (clone.S:95)
==5579==  Address 0x10c048 is 0 bytes inside data symbol "_ZL10thread_var"

Re:Práce s vlákny v C
« Odpověď #14 kdy: 19. 01. 2021, 18:44:14 »
no super...snad se konecne pohnu kupredu (rad bych pouzil spravnym smerem, ale do toho je jeste daleko :))
Pridal jsem do kodu mutex a zkusil jak se to bude chovat. Po spusteni pres:
Kód: [Vybrat]
valgrind --tool=helgrind ./mainjsem dostal po skonceni programu tuto hlasku
Kód: [Vybrat]
==3853== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 50221857 from 38)
Vyslednu kod je zde:
Kód: [Vybrat]
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>

static int init_function(void);
static void *handle_function(void *arg);

static uint8_t thread_var = 0x00;
static pthread_t thread;
static pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;

int main(int argc, char *argv[]){
  int ret;
  int c_run = 100;
   
  while(1){
    ret = init_function();
    if(ret == 1){
      //printf("vytvoreno nove vlakno\n");
      if(c_run != 0){ c_run--;}
      else{ break;}
      }
    }
 
  return 0;
  }

static int init_function(void){
  uint8_t var;
 
  pthread_mutex_lock(&mymutex); 
  var = thread_var;   
  if(var == 0x00){ thread_var = 0xFF;}
  pthread_mutex_unlock(&mymutex);

  if(var == 0xFF){     
    return -2;
    }   
   
  if(pthread_create(&thread, NULL, handle_function, NULL)){
    //vlakno nebylo vytvoreno, musim shodit thread_var 
    pthread_mutex_lock(&mymutex);       
    thread_var = 0x00;
    pthread_mutex_unlock(&mymutex); 
     
    return -1;
    }
 
  pthread_detach(thread);
  return 1;
  }

static void *handle_function(void *arg){   
  sleep(1);
 
  pthread_mutex_lock(&mymutex);
  thread_var = 0x00;
  pthread_mutex_unlock(&mymutex);
 
  return NULL;
  }

Ze zacatku mi to nefungovalo. Po chvili mi doslo, ze problem bude asi v puvodnim volani funkce printf na konci vlakna. Kdyz jsem ji odstranil uz to zadne chyby nehlasilo. Uz mi jsou ty dotazy skoro trapny, ale je toto jiz korektni?