Wątki i mutex-y

Wątki i mutex-y
OM
  • Rejestracja:około 10 lat
  • Ostatnio:ponad 9 lat
  • Postów:3
1

Witam

Chciałem napisać prosty demon TCP w C++, który służyłby mi jako prosty chat. W tym celu chciałem skorzystać z wielowątkowości biblioteki standardowej, lecz już w prostym przypadku natrafiam na problemy.

Moja koncepcja jest taka:
UserDispatcher : std::thread - klasa która będzie zarządzać połączeniami od nowych użytkowników.
User : std::thread - klasa obsługująca danego użytkownika

Na tę chwilę, celem uproszczenia, w wątku dispatchera nowy użytkownik tworzony jest co 2s, i każdy z nich wyświetla na standardowe wyjście w swoim imieniu swoje id. Poza tym nie dzieje się nic.
Teraz w przypadku gdy w funkcji run() dispatchera nowego użytkownika stworzę jawnie wołając "users.push_back(User(&mutex));" to program działa zgodnie z moim oczekiwaniem, natomiast zamknięcie tego fragmentu kodu w funkcję addUser() powoduje iż na standardowe wyjście zgłaszają się użytkownicy z id=0. Cóż robię nie tak?

Tzn. output w pierwszym przypadku:

Kopiuj
New user
User thread entered, id: 0
New user
User thread entered, id: 1
New user
User thread entered, id: 2

W przypadku drugim:

Kopiuj
New user
User thread entered, id: 0
New user
User thread entered, id: 0
New user
User thread entered, id: 0

Klasa UserDispatcher:

Kopiuj
class UserDispatcher: public std::thread {
    std::mutex mutex;
    std::list<User> users;

    void run(void);
    void addUser(void);

public:
    UserDispatcher(void);
};

void UserDispatcher::run(void) {
    while(true) {
        mutex.lock();
        std::cout << "New user" << std::endl;
//        users.push_back(User(&mutex));     //pierwszy przypadek
//        addUser();                        //drugi przypadek
        mutex.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }
}

void UserDispatcher::addUser(void) {
    users.push_back(User(&mutex));
}

UserDispatcher::UserDispatcher(void)
        : std::thread(&UserDispatcher::run, this) {
}

Klasa User:

Kopiuj
class User: public std::thread {
    static int nextID;
    std::mutex *mutex;
    int id;

    void run(void);

public:
    User(std::mutex *mutex);
};

void User::run(void) {
    mutex->lock();
    std::cout << "User thread entered, id: " << id << std::endl;
    mutex->unlock();
    while(true) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

User::User(std::mutex *mutex)
        : mutex(mutex), id(nextID++), std::thread(&User::run, this) {
}
edytowany 3x, ostatnio: omicronns
gośćabc
  • Rejestracja:prawie 11 lat
  • Ostatnio:ponad 3 lata
  • Lokalizacja:Szczecin
  • Postów:500
0

oba przypadki są złe, zauważ że robisz dual lock na tym samym mutexie, a potem zwalniasz go w User pozwalająć wątkowi, w którym jest dispatcher, jechać dalej (to jest jeden z use casów)

masz zły design

edytowany 2x, ostatnio: gośćabc
OM
  • Rejestracja:około 10 lat
  • Ostatnio:ponad 9 lat
  • Postów:3
0

Robię tak, bo tak zrozumiałem działanie mutexów. Robię lock w UserDispatcher, po to, by nowo tworzony wątek w User również zrobił lock na tym mutexie, co powinno spowodować jego zatrzymanie w tym miejscu, do momentu, gdy UserDispatcher wykona unlock na mutexie.

several
  • Rejestracja:ponad 15 lat
  • Ostatnio:minuta
1

Nie sądzę żeby projektanci std::thread projektowali tą klasę by po niej dziedziczyć.


hauleth
Więcej, na pewno tego tak nie robili, bo pisałem do komitetu w sprawie zmiany na to by wątki tworzyło się właśnie przez dziedziczenie. Dostałem odpowiedź, że za późno zgłosiłem RFC.
satirev
@winerfresh dlaczego postulowałeś o tworzenie wątków przez dziedziczenie? Jeśli skrobnąłeś jakąś propozycję to podrzuć link do niej.
hauleth
Bo IMHO to jest prostszy sposób i bardziej naturalny w środowisku obiektowym (łatwiej i logiczniej jest rozwiązane przekazywanie parametrów). Dawno temu to było i już nawet gdybym chciał to się nie dokopię do tego maila.
gośćabc
  • Rejestracja:prawie 11 lat
  • Ostatnio:ponad 3 lata
  • Lokalizacja:Szczecin
  • Postów:500
0
omicronns napisał(a):

Robię tak, bo tak zrozumiałem działanie mutexów. Robię lock w UserDispatcher, po to, by nowo tworzony wątek w User również zrobił lock na tym mutexie, co powinno spowodować jego zatrzymanie w tym miejscu, do momentu, gdy UserDispatcher wykona unlock na mutexie.

true ale chodzi o to, że sharujesz mutex między 2 totalnie rózne obiekty, albo zrób osobnego mutexa dla Userów, albo nadawaj im ID w dispatcherze i bez lockowania i unlockowania w userze

satirev
  • Rejestracja:prawie 14 lat
  • Ostatnio:około 4 lata
2

Przede wszystkim @omicronns +1 za ciekawy, techniczny wątek, o które na tym forum bardzo ciężko ;)

Teraz to meritum.
Na początek trzeba cię skarcić za dziedziczenie po std::thread (wspomniał już o tym @several). Nie chodzi tutaj tylko o to, że std::thread nie ma wirtualnego destruktora (więc w przypadku trzymania obiektów zdefiniowanych przez ciebie klas w referencji/wskaźniku otrzymasz wyciek pamięci). Główny problem to kolejność inicjalizacji na liście inicjalizacyjnej klasy. W tym miejscu odsyłam do dokumentacji/standardu. W każdym razie w twoim przypadku najpierw utworzy się funkcja wątku (nowy wątek), kolejno nastąpi skopiowanie wskaźnika na std::mutex, a na końcu dojdzie do postrinkrementacji statycznej zmiennej i skopiowania jej wartości do pola id. Dodam, że to wszystko mógłbyś wykryć dodając flagę kompilacji -Wreorder. Ergo, twój wątek będzie działał na niezainicjowanych zmiennych. Co więcej użycie this na liście inicjalizacyjnej w przypadku, gdy pola danej klasy nie są jeszcze zainicjowane to UB. Tyle z teorii, rzućmy okiem na działanie tego kodu w praktyce (pamiętając o tym, że to UB).

Jeśli używamy bezpośrednio users.push_back(User(&mutex)) to nie wiedzieć dlaczego ten kod działa xd. A po przeniesieniu tej instrukcji do osobnej metody dostajemy segfault. Co jest bardzo dziwne, bo w obu przypadkach używany jest ten sam, defaultowy move constructor. Jedyna różnica jest taka, że w tym drugim przypadku mutex nie jest jeszcze zainicjowany i przez to dostajemy segfault. Sprawdzałem to na g++ 4.9.2 i clang++ 3.5.0 i w obu przypadkach zachowanie było jak to opisane powyżej. Tak, tak wiem, że to UB więc równie dobrze mógłbym się spodziewać tego, że spali mi się karta graficzna, komputer odegra Hejnał Maryjacki, etc. Mimo wszystko ciekawe zagadnienie ;)

Jeszcze jedna sprawa. @omicronns z tego co widzę chcesz zapewnić atomość dostępu do io (w tym wypadku tylko stdout). Polecam w tym celu użycie rozwiązanie zaproponowanego przez Howarda Hinnant, które poza tym, że działa to jeszcze zachowuje się zgodnie z zasadą pojedynczej odpowiedzialności bytów.

Kopiuj
#include <iostream>
#include <mutex>

std::ostream&
print_one(std::ostream& os)
{
    return os;
}

template <class A0, class ...Args>
std::ostream&
print_one(std::ostream& os, const A0& a0, const Args& ...args)
{
    os << a0;
    return print_one(os, args...);
}

template <class ...Args>
std::ostream&
print(std::ostream& os, const Args& ...args)
{
    return print_one(os, args...);
}

template <class ...Args>
std::ostream&
print(const Args& ...args)
{
    static std::mutex m;
    std::lock_guard<std::mutex> _(m);
    return print(std::cout, args...);
}

// Użycie

void exec()
{
    print("Hello ", std::this_thread::get_id(), '\n');
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

Edit: @omicronns zastanów się jeszcze nad samym rozwiązaniem problemu. Tip, wątek per klient to nie jest dobre rozwiązanie.

edytowany 3x, ostatnio: satirev
gośćabc
czemu nie dasz odpowiedzi realnej, takiej, której on nie zignoruje przez to, że jest skomplikowana? tak tylko pytam z ciekawości :D
satirev
Dlaczego zakładasz z góry, że zignoruje? Skoro używa w swoim kodzie std::thread i std::mutex to równie dobrze może użyć variadic template. Jeśli czegoś nie zrozumie/będzie chciał to inaczej rozwiązać to przypuszczam, że zada kolejne pytanie.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)