Czemu kolejność definicji danych w polu klasy ma tak duże znaczenie?

0

Szok. Coś mi chyba umknęło. Piszę sobie klasę, która zawiera jakieś tam sobie pola i boost::asio::io_context. Podczas kompilacji przy próbie zrobienia użytku z tej klasy rzuca mi wyjątek, że ioc jest nullptrem. Ja tu kurdę kminię o co może chodzić, aż w końcu jakimś sposobem pozmieniałem kolejność pól w klasie, tak aby ioc było pierwsze:

private:
    //bla bla
    //bla bla
    asio::io_context m_ioc;

zmieniłem na:

private:
    asio::io_context m_ioc;
    //bla bla
    //bla bla

i działa XD
WTF!!! Żadnego nullptra już nie ma, wszystko żyje i jest w pytę.
Dlaczego to tak się dzieje? O co w tym wszystkim chodzi?

Dla kontekstu dodam, że po konstrukcji klasy od razu wołam metodę, która odpala oddzielny wątek, który wtedy konstruuje inny obiekt (miejsce w którym leciał wyjątek) - czyżby metoda z wątkiem była szybciej wołana niż inicjalizowane były pola? Bez sensu.

2

Pokaż co masz w "bla bla", co jest w definicji konstruktora i jak odpalasz ten wątek. Bez tego jedyne co można powiedzieć, to przypomnieć że kolejność inicjalizacji pól w klasie zależy od kolejności deklaracji.

0

Dobra, wygląda na to, że zbyt optymistycznie podszedłem do tego "działa", bo nie działa. Teraz krzaczy się mi gdzie indziej -.-

0

Ok, bardzo dziwna sprawa. Problem okazał się być totalnie gdzie indziej - mianowicie przekazałem this do wątka, którego detachowałem i stamtąd chciałem się dostać do pola klasy, które (prawdopodobnie) już nie istniało, albo nie zostało zainicjalizowane, w końcu kod to był jakiś read access violation (0xDDDDDDE1) :

class X {
public:
    X() = default;
    void doStuff() {
        std::thread th{
	        [this]() {
	            doSomethingElse();
            }};
        th.detach();
    }

    void doSomethingElse() {
        stuff = makeStuff(); //NOPE! nie ma stuff albo coś :(
    }

private:
    std::string stuff;
};

Jestem nieco zniesmaczony, bo teraz kolejność jaką zachowam przy pisaniu asio::io_context nie ma już najmniejszego znaczenia. Czyli wcześniejszy wyjątek już na to wskazywał, ale w nieco pokraczny sposób, bo wskazywał na ioc w nadrzędnej klasie, a nie konkretną linijkę w klasie z przykładu X (a po zamianie kolejności pól już tak!).

Problem jako tako rozwiązałem po prostu przenosząc stuff do lokalnego zakresu funkcji. Brzydko tylko teraz wygląda przekazywanie tego wszędzie jako parametr - mogę w jakiś sposób to obejść? W sensie, że mogę jakoś mieć te prywatne pole i nie martwić się, że zostało już usunięte/niezainicjalizowane?

2
    void doStuff() {
        std::thread th{
            [this]() {
                doSomethingElse();
            }};
        th.detach();
    }

To mi wygląda na zaproszenie na undefined behavior.
Jeśli czas życia instancji X się skończy zanim wątek się zakończy, to będzie crash.
I pewnie to jest problem (pewności nie mam bo kod nie wygląda na kompletny).

0

@MarekR22:
Miałem praktycznie to samo napisać. Próbowałem na moim komputerze doprowadzić do crasha, ale albo inny kompilator albo coś w reszcie programu się jeszcze dzieje i u mnie "działa".

@Cyberah
Tak z ciekawości - jakbyś wywalił detach i do ~X() dodał join - to czy nie rozwiązałoby to problemu "crashowania" ?

0

@MarekR22:

Jeśli czas życia instancji X się skończy zanim wątek się zakończy, to będzie crash.

I właśnie tak się działo. Tak swoją drogą, co uważasz za UB? ten detach()?

@Bartłomiej Golenko
Nie mogę, bo bawię się serwerami współbieżnymi i po "odczepieniu" wątku (odpowiedzialnego za wymianę informacji), w tym wypadku klienta, mogę przyjąć kolejnego. Czekanie na skończenie wykonywania się wątku spowoduje, że mój serwer stanie się iteracyjny ;(

A z resztą bardzo musiałbym się przyjrzeć tej konstrukcji, bo generalnie mi się nie crashuje i jest ok. Wiadomo, sceptycznie do tego trzeba podchodzić, bo mimo wszystko jest minimalna szansa, że jednak program się przez to sypnie. Musiałbym pomyśleć właśnie nad skutecznym zabezpieczeniem tej klasy.

0

@Cyberah:
UB wynika z użycia this w sytuacji, gdy obiekt na który ten wskaźnik pokazuje może już nie istnieć. Dodatkowo dobierasz się do stuff bez jakiejkolwiek synchronizacji, ale to już osobna sprawa - być może w Twoim programie nikt inny w tym samym czasie do tego pola nie będzie sięgać.

W tym konkretnie przypadku potrzebujesz detach(), bo bez niego po wyjściu z zakresu widoczności th program się wywali (std::terminate wywołany w destruktorze std::thread). Ale pomijając ten drobiazg - czy z detach czy bez niego wątek i tak będzie się wykonywać współbieżnie (z praktycznego punktu widzenia detach() powoduje tylko to, że OS nie będzie po zakończeniu wątku trzymał jego pozostałości w nadziei, że ktoś wywoła dla niego join() )

0

Kurczę, bo jak zaimplementowałem Twoją propozycję, to serwer mógł obsługiwać tylko jednego klienta (pewnie musiałbym coś jeszcze gdzieś zmienić - na razie nie umiem), a z detach() już ile chcę. O to mi bardziej chodziło. Nie mniej też wolę joina używać, bo niby wiem czym się obydwa różnią, ale nie mam tego jeszcze we krwi i o wiele rzadziej korzystam z detach(). Być może stąd są te problemy.

Dodam jeszcze, że uczę się na podstawie pewnej książki i autor nie synchronizuje kodu nigdzie poza asynchronicznymi przykładami.

0

Pokaż fragment kodu w którym tworzysz X.

0

mój kod:

void accept() {
		try {
			auto sock{ std::make_shared<asio::ip::tcp::socket>(m_ioc) };
			m_acceptor.accept(*sock);
			
			auto const service{ std::make_unique<Service>() };
			service->startHandlingClient(sock);
		}
		catch (system::error_code& ec) {
			std::cout << "Error occured: " << ec.message() << '\t' << ec.value() << '\n'; 
		}
	}

przykłady z książki:

std::shared_ptr<asio::ip::tcp::socket>
			sock(new asio::ip::tcp::socket(m_ios));
		m_acceptor.accept(*sock.get());
		(new Service)->StartHandligClient(sock);
void StartHandligClient(
		std::shared_ptr<asio::ip::tcp::socket> sock) {

		std::thread th(([this, sock]() {
			HandleClient(sock);
			}));

		th.detach();
	}

	void HandleClient(std::shared_ptr<asio::ip::tcp::socket> sock) {
                //...

		// Clean-up.
		delete this;
	}
0

Twój Service znika po wyjściu z bloku try. W żaden sposób nie sięgasz potem do "stuff" - do czego on jest w ogóle potrzebny ?

W HandleClient wywołujesz destruktor dla potencjalnie już nieistniejącego Service, do którego wskaźnik "wyciekłeś sobie" przekazując go do wątku. Brr...

0

Właśnie o to mi chodzi. Nie mam pojęcia jak sięgnąć po stuff - przez co muszę definicję przenieść z klasy do funkcji i wtedy oczywiście działa, w przeciwnym przypadku crash. I to jest po prostu zwykły string trzymający nazwę użytkownika, niemniej problem zacznie się bardzo rozrastać w miarę dodawania funkcji do programu. Dlatego zapytałem czy jakoś dam radę się do zawartości klasy dostać, ale wygląda na to, że nie, a wręcz, że to bez sensu.

W HandleClient wywołujesz destruktor dla potencjalnie już nieistniejącego Service, do którego wskaźnik "wyciekłeś sobie" przekazując go do wątku. Brr...

no właśnie dlatego tu eksperymentuję ze smart pointerami, bo też mi się to bardzo nie podoba :P

1 użytkowników online, w tym zalogowanych: 0, gości: 1