CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.
Niemniej jednak Python posiada stosowne API, które da się wykorzystać w innym interpreterze bez ograniczeń GIL, albo przy zadaniach typu I/O.
Queue (kolejka) zapewnia bezpieczny mechanizm do wymiany informacji pomiędzy wątkami.
Każde zawołanie Queue.put() zwiększa licznik, który jest dekrementowany w momencie zawołania Queue.task_done().
Główny wątek jest blokowany przez Queue.join(), do momentu, gdy wszystkie elementy z kolejki zostaną przetworzone.
Funkcja Queue.get() jest domyślnie blokująca, tzn. wątek zawiesza działania, jeżeli nie ma już danych do przetworzenia.
Thread.daemon - wszystkie wątki potomne zostaną zabite wraz z wątkiem głównym.
Często po wywołaniu programu na konsoli pojawia się informacja o wystąpieniu wyjątku. Jest to związane ustawieniem daemon=true. Interpreter nie zdążył z czystym zamknięciem wątku gdy kończony jest wątek główny. Rozwiązaniem byłby mechanizm w samum wątku, pozwalający na jego zakończenie, gdy nie jest on już potrzebny. W przypadku prostych skryptów, raczej nie trzeba się tym przejmować.
Dwie interesujące prezentacje na temat białek.
Wyjaśnienie czym są białka i jak są zbudowane. Paul Andersen "Proteins":
O człowieku, który buduje własne białka (z wykorzystaniem Lisp-a i LLVM) i chce budować jeszcze, oraz dlaczego C++ się do tego nie nadaje. Christian Schafmeister "Clasp: Common Lisp using LLVM and C++ for Molecular Metaprogramming":
start - ustawia się na pierwszej instrukcji naszego programu, w zależności od systemu punkt wejścia może mieć różną nazwę, gdb w takim przypadku zachowa się uniwersalnie.
save breakpoints mybbb - zapisanie breakpointów do pliku. Przydatne, gdy trzeba zamknąć gdb, ale nie chcemy niszczyć dorobku naszej sesji.
source mybbb - załadowanie breakpointów z pliku
Proste wykorzystanie pythona, do odczytania process id. Aby zakończyć interaktywny tryb należy wpisać end, podobnie jak to się dzieje w przypadku command.
Reading symbols from a.out...done.
(gdb) b 8
Breakpoint 1 at 0x400589: file main.cpp, line 8.
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400589 in main() at main.cpp:8
(gdb) r
Starting program: /home/user/cpp_gdb_test/a.out
is i now 7
Breakpoint 1, main () at main.cpp:8
8 i++;
(gdb) python
>print(gdb.breakpoints()[0].location)
>end
/home/user/cpp_gdb_test/main.cpp:8
Ustawienie braekpointa z poziomu pythona:
(gdb) python gdb.Breakpoint('11')
Breakpoint 2 at 0x4005a1: file main.cpp, line 11.
(gdb) c
Continuing.
is i now 8
Breakpoint 2, main () at main.cpp:11
11 printf("Hello world!!!\n");
Fajny lightning talk na zeszłorocznej konferencji CppCon 2015 traktujący o przydatnych trikach w procesie debugowania programu:
Postanowiłem przećwiczyć sobie kilka mechanizmów. Najciekawszą rzeczą dla mnie był record, który umożliwia odtworzenie wartości pamięci przy reverse debug-u. Szczególnie przydatny, gdy program pisze po stosie i nie można odtworzyć pechowej sekwencji instrukcji z core dumpa. Linki:
Przykładowy program, który w chwili, gdy zostanie wylosowana wartość 4, dla zmiennej v, zaczyna pisać po stosie.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void overflow(int v)
{
char t[10];
if (v == 4)
{
for (int n = 0; n < 10000; n++)
t[n] = 'A';
}
}
int main()
{
srand(time(0));
int i;
do
{
i = rand() % 10;
printf("i is now %d\n", i);
int v = rand() % 100;
overflow(v);
} while(i != 5);
return 0;
}
Kompilacja, konfiguracja i uruchomienie, trzeba pamiętać o ustawieniu rozmiaru ulimit dla core dump-ów, domyślnie jest to bowiem wartość 0.
ulimit -a
ulimit -c unlimited
ulimit -a
gcc -g main.cpp
while ./a.out ; do echo "OK"; done
gdb a.out -c core
Wynik:
i is now 3
i is now 2
Segmentation fault (core dumped)
Stos został uszkodzony, w tym przypadku widać ślad po ostatniej instrukcji i z jaką wartością została wykonana, ale w przypadku bardziej skomplikowanych struktur danych, może być to kłopotliwe.
Core was generated by `AAAAAAAA'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 __GI_getenv (name=0x7f15a332b3c2 "BC_FATAL_STDERR_", name@entry=0x7f15a332b3c0 "LIBC_FATAL_STDERR_") at getenv.c:84
84 getenv.c: No such file or directory.
(gdb) p i
No symbol "i" in current context.
(gdb) p v
No symbol "v" in current context.
(gdb) p t
No symbol "t" in current context.
(gdb) bt
#0 __GI_getenv (name=0x7f15a332b3c2 "BC_FATAL_STDERR_", name@entry=0x7f15a332b3c0 "LIBC_FATAL_STDERR_") at getenv.c:84
#1 0x00007f15a31d7ef2 in __GI___libc_secure_getenv (name=name@entry=0x7f15a332b3c0 "LIBC_FATAL_STDERR_")
at secure-getenv.c:29
#2 0x00007f15a321549a in __libc_message (do_abort=do_abort@entry=1,
fmt=fmt@entry=0x7f15a332cc7f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:80
#3 0x00007f15a32b689c in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7f15a332cc61 "stack smashing detected")
at fortify_fail.c:37
#4 0x00007f15a32b6840 in __stack_chk_fail () at stack_chk_fail.c:28
#5 0x00000000004006bb in overflow (v=4) at main.cpp:14
#6 0x4141414141414141 in ?? ()
#7 0x4141414141414141 in ?? ()
Próba odtworzenia problemu z wykorzystaniem record. gdb przechowa zawartość pamięci, z kolejnych kroków wykonania programu, dzięki czemu można się cofnąć, o jedną komendę (reverse-next), na sam początek nagrywania (reverse-continue), lub skorzystać z innych bliźniaczych komend (reverse-*).
Inne udogodnienia, z których skorzystałem to warunkowy breakpoint w linii 3 (b 26 if v == 4), który zachodzi, gdy zmienna v będzie miała ustawioną wartość na 4. Oraz command, który zostaje wykonany w momencie zatrzymania programu (linie 12-17). Pierwszym argumentem command jest numer porządkowy breakpoint-a, który można uzyskać z info break.
gdb a.out
(gdb) b 26 if v == 4
Breakpoint 1 at 0x400748: file main.cpp, line 26.
(gdb) b 11
Breakpoint 2 at 0x400686: file main.cpp, line 11.
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400748 in main() at main.cpp:26
stop only if v == 4
2 breakpoint keep y 0x0000000000400686 in overflow(int) at main.cpp:11
(gdb) command 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>record
>continue
>end
(gdb) r
Starting program: /home/user/cpp_gdb_test/a.out
i is now 9
i is now 7
i is now 2
i is now 3
i is now 3
i is now 9
i is now 9
Breakpoint 1, main () at main.cpp:26
26 overflow(v);
Breakpoint 2, overflow (v=4) at main.cpp:11
11 for (int n = 0; n < 10000; n++)
(gdb)
(gdb) bt
#0 overflow (v=4) at main.cpp:11
#1 0x0000000000400752 in main () at main.cpp:26
(gdb) reverse-next
9 if (v == 4)
(gdb)
6 {
(gdb)
No more reverse-execution history.
main () at main.cpp:26
26 overflow(v);
(gdb) p i
$5 = 9
Moja stara wersja korzystała ze zbyt wielu kroków. W dodatku nie mogłem zmusić programu convert do ustawienia mniejszego opóźnienia pomiędzy ramkami niż 10. W przykładzie poniżej zrezygnowałem ze skalowania.
Lepsza wersja (także bez skalowania). Kroków jest na tyle dużo, że zamknąłem wszystko w jednym skrypcie, ale convert nie jest już wykorzystywany, ponieważ sam ffmpeg dobrze sobie radzi z tworzeniem plików tego formatu:
Przez lata przyjmowałem za pewnik słowa, że "nie należy korzystać z goto" i tak właściwie, to nie pamiętam, żebym to kiedyś zrobił, ba nawet nie miałem okazji widzieć takiego kodu zbyt często. Takie też jest moje wewnętrzne przekonanie. Program korzystający z takiego wyrażenia, nie byłby przyjemny w utrzymaniu. Właściwie więc, po co te mechanizmy w językach nadal istnieją? Wyjątkiem kodu, który miałem okazję analizować były funkcje kryptograficzne, których działanie było zawiłe samo w sobie. Postanowiłem jednak sięgnąć do źródeł całego trendu. I tutaj, nie jest już tak czarno-biało. Ze spostrzeżeń Edsgera Dijkstry ("za") i Donalda Knutha ("przeciw") stworzono dogmat, a sami autorzy zgadzają się, że dla pewnych problemów optymalizacyjnych skorzystanie z goto jest konieczne, chociaż w większości przypadków będzie to symptom źle zaprojektowanego kodu. Odbiegając od specyficznych przypadków dotyczących małej części całej aplikacji, sam Knuth miał jednak nadzieję, że powstaną w przyszłości mechanizmy umożliwiające tłumaczenie wysokopoziomowego kodu, na niskopoziomowy, bez wymogu korzystania z tego wyrażenia. Po 40 latach kompilatory już całkiem nieźle sobie radzą, ale to jeszcze kiedyś sprawdzę.