Vladislav Shpilevoy PRO
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.
Системное программирование
Лекция 6:
Потоки. Отличие от процессов. Атомарные операции. Синхронизация. Атрибуты. Особенности многопоточных процессов. Вид в ядре.
Version: 2
The presentation is outdated and not maintained anymore. Please, refer to the English version for the most actual slides.
Дедлайны:
0x0
0xffffffff
.text
.data
.bss
.heap
.stack
.env
kernel
0xc0000000
Файловые дескрипторы
Очередь сигналов
IPC
Задача
.text
.data
.bss
.stack
.heap
.stack
.heap
Translation Lookaside Buffer
Задача
.text
.data
.bss
.stack
.heap
Translation Lookaside Buffer
call()
call()
struct task_struct {
struct thread_info thread_info;
volatile long state;
void *stack;
atomic_t usage;
unsigned int cpu;
int prio;
struct mm_struct *mm;
int exit_state;
int exit_code;
int exit_signal;
pid_t pid;
struct task_struct *parent;
struct list_head children;
u64 start_time;
const struct cred *cred;
struct files_struct *files;
struct thread_struct thread;
};
В Linux потоки и процессы - одно и то же
Это потоки делят
int
clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...);
Создать новый struct task_struct из user space
#define CLONE_CHILD_CLEARTID
#define CLONE_CHILD_SETTID
#define CLONE_FILES
#define CLONE_FS
#define CLONE_IO
#define CLONE_NEWCGROUP
#define CLONE_NEWIPC
#define CLONE_NEWNET
#define CLONE_NEWNS
#define CLONE_NEWPID
#define CLONE_NEWUSER
#define CLONE_NEWUTS
#define CLONE_PARENT
#define CLONE_PARENT_SETTID
#define CLONE_PID
#define CLONE_PTRACE
#define CLONE_SETTLS
#define CLONE_SIGHAND
#define CLONE_STOPPED
#define CLONE_SYSVSEM
#define CLONE_THREAD
#define CLONE_UNTRACED
#define CLONE_VFORK
#define CLONE_VM
CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD
int
thread_create_clone(int (*func)(void *), void *arg, void **stack)
{
int stack_size = 65 * 1024;
*stack = malloc(stack_size);
void *stack_top = (char *) *stack + stack_size;
int flags = CLONE_VM | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_THREAD;
return clone(func, stack_top, flags, arg);
}
stack
stack_top
Чем делиться?
Принимает вершину нового стека
Создал отсюда
Но стек растет вниз - вершина выше
volatile bool is_finished = false;
int thread_f(void *arg)
{
pid_t thread_id = gettid();
pid_t pid = getpid();
printf("pid %d, tid %d: new thread, arg = %d\n",
(int)pid, (int)thread_id,
*((int *) arg));
is_finished = true;
return 0;
}
int main()
{
pid_t thread_id = gettid();
pid_t pid = getpid();
printf("pid %d, tid %d: main thread\n",
(int)pid, (int)thread_id);
void *stack;
int arg = 100;
int ret = thread_create_clone(thread_f,
(void *) &arg,
&stack);
printf("pid %d, tid %d: clone result = %d\n",
(int)pid, (int)thread_id, ret);
while (! is_finished)
sched_yield();
free(stack);
return 0;
}
Это будет новый поток
Печатаю идентификаторы потока и процесса в обоих потоках
Запускаю поток и жду его завершения
Не забываю освободить стэк
$> gcc 1_clone.c
$> ./a.out
pid 10194, tid 10194:
main thread
pid 10194, tid 10194:
clone result = 10195
pid 10194, tid 10195:
new thread, arg = 100
Одинаковый PID
Разные TID
Зачем ждать завершения?
volatile bool is_finished = false;
int thread_f(void *arg)
{
/* ... */
}
int main()
{
/* ... */
while (! is_finished)
sched_yield();
free(stack);
return 0;
}
Что будет, если не ждать завершения созданного потока?
1) стек удалится у него из под ног
2) завершение главного потока - завершение процесса
2 балла
Volatile память
Регистры
DRAM,
SRAM
Non-volatile память
Магнитная
Flash
bool value = true;
while (value) {
/*
* do not change
* 'value'
*/
}
gcc
while (true) {
/* ... */
}
struct object obj;
/* ... */
a = a + obj.value;
/* ... */
b = b + obj.value;
/* ... */
volatile bool value = true;
while (value) {
/*
* do not change
* 'value'
*/
}
volatile bool value = true;
while (value) {
/*
* do not change
* 'value'
*/
}
struct object obj;
int tmp = obj.value;
/* ... */
a = a + tmp;
/* ... */
b = b + tmp;
/* ... */
volatile struct object obj;
/* ... */
a = a + obj.value;
/* ... */
b = b + obj.value;
/* ... */
volatile struct object obj;
/* ... */
a = a + obj.value;
/* ... */
b = b + obj.value;
/* ... */
void *
thread_f(void *arg)
{
*((bool *)arg) = true;
return NULL;
}
int
main()
{
const bool is_finished = false;
pthread_t tid;
pthread_create(&tid, NULL,
thread_f,
&is_finished);
while (! is_finished)
sched_yield();
pthread_join(tid, NULL);
return 0;
}
$> clang 2_non_volatile.c
$> ./a.out
Программа зависает
$> gobjdump -d a.out
<_main>:
100000f40: push %rbp
100000f41: mov %rsp,%rbp
100000f44: sub $0x20,%rsp
100000f48: lea -0x10(%rbp),%rdi
100000f4c: xor %eax,%eax
100000f4e: mov %eax,%esi
100000f50: lea -0x37(%rip),%rdx
100000f57: lea -0x5(%rbp),%rcx
100000f5b: movl $0x0,-0x4(%rbp)
100000f62: movb $0x0,-0x5(%rbp)
100000f66: callq 100000f7c <_main+0x3c>
100000f6b: mov %eax,-0x14(%rbp)
100000f6e: callq 100000f82 <_main+0x42>
100000f73: mov %eax,-0x18(%rbp)
100000f76: jmpq 100000f6e <_main+0x2e>
Вот цикл
В нем безусловный jump
То есть переменную is_finished выкинули
Надо разобраться ...
void *
thread_f(void *arg)
{
*((bool *)arg) = true;
return NULL;
}
int
main()
{
const volatile bool
is_finished = false;
pthread_t tid;
pthread_create(&tid, NULL,
thread_f,
&is_finished);
while (! is_finished)
sched_yield();
pthread_join(tid, NULL);
return 0;
}
$> clang 3_volatile.c
$> ./a.out
$> # finished
$> gobjdump -d a.out
<_main>:
100000f00: push %rbp
...
100000f2e: mov -0x5(%rbp),%al
100000f31: xor $0xff,%al
100000f33: test $0x1,%al
100000f35: jne 100000f40 <_main+0x40>
100000f3b: jmpq 100000f4d <_main+0x4d>
100000f40: callq 100000f74 <_main+0x74>
100000f45: mov %eax,-0x18(%rbp)
100000f48: jmpq 100000f2e <_main+0x2e>
100000f4d: xor %eax,%eax
100000f4f: mov %eax,%esi
100000f51: mov -0x10(%rbp),%rdi
...
100000f66: retq
Цикл изменился
Начало цикла и проверка условия. Прошло - заходим в цикл
Если не прошла, то прыг из цикла
В конце прыг в начало цикла
check_condition:
if (! is_finished)
goto do_iteration;
goto end;
do_iteration:
sched_yield();
goto check_condition;
end:
volatile bool is_finished = false;
volatile int counter = 0;
int
thread_f(void *arg)
{
for (int i = 0; i < 100000; ++i)
counter = counter + 1;
is_finished = true;
return 0;
}
int
main()
{
void *stack;
thread_create_clone(thread_f, NULL,
&stack);
for (int i = 0; i < 100000; ++i)
counter = counter + 1;
while (! is_finished)
sched_yield();
printf("counter = %d\n", counter);
free(stack);
return 0;
}
$> gcc 4_not_atomic.c
$> ./a.out
counter = 133664
$> ./a.out
counter = 200000
$> ./a.out
counter = 127934
$> ./a.out
counter = 140386
counter = counter + 1;
gcc
mov counter,%eax
add $0x1,%eax
mov %eax,counter
Загрузить из памяти -
Увеличить -
Положить в память -
Поток 1
Поток 2
mov counter, $eax
mov counter,%eax
add $0x1,%eax
mov %eax,counter
add $0x1,%eax
mov %eax,counter
Увеличивает старое значение
__sync_add_and_fetch(&counter, 1);
gcc
lock addl $0x1,counter
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
void __atomic_load (type *ptr, type *ret, int memorder)
void __atomic_store (type *ptr, type *val, int memorder)
void __atomic_exchange (type *ptr, type *val, type *ret, int memorder)
bool __atomic_compare_exchange (type *ptr, type *expected, type *desired, ...)
type __atomic_add_fetch (type *ptr, type val, int memorder)
type __atomic_sub_fetch (type *ptr, type val, int memorder)
type __atomic_fetch_add (type *ptr, type val, int memorder)
type __atomic_fetch_sub (type *ptr, type val, int memorder)
volatile bool is_finished = false;
volatile int counter = 0;
int
thread_f(void *arg)
{
for (int i = 0; i < 100000; ++i)
__sync_add_and_fetch(&counter, 1);
is_finished = true;
return 0;
}
int
main()
{
void *stack;
thread_create_clone(thread_f, NULL,
&stack);
for (int i = 0; i < 100000; ++i)
__sync_add_and_fetch(&counter, 1);
while (! is_finished)
sched_yield();
printf("counter = %d\n", counter);
free(stack);
return 0;
}
$> gcc 5_atomic.c
$> ./a.out
counter = 200000
$> ./a.out
counter = 200000
$> ./a.out
counter = 200000
volatile bool is_finished = false;
volatile int counter = 0;
int
thread_f(void *arg)
{
for (int i = 0; i < 100000; ++i)
counter = counter + 1;
is_finished = true;
return 0;
}
int
main()
{
void *stack;
thread_create_clone(thread_f, NULL,
&stack);
for (int i = 0; i < 100000; ++i)
counter = counter + 1;
while (! is_finished)
sched_yield();
printf("counter = %d\n", counter);
free(stack);
return 0;
}
$> clang 6_bad_lock.c
struct complex_thing {
bool lock;
char string[128];
};
volatile struct complex_thing thing;
volatile bool start = false;
void *thread_f(void *arg)
{
int id = (int) arg;
while (! start) {};
if (! thing.lock) {
thing.lock = true;
for (int i = 0; i < 127; ++i)
thing.string[i] = 'a' + id;
}
return NULL;
}
int main()
{
pthread_t tid[6];
for (int i = 0; i < 6; ++i) {
pthread_create(&tid[i], NULL, thread_f,
(void *) i);
}
start = true;
for (int i = 0; i < 6; ++i)
pthread_join(tid[i], NULL);
printf("%s\n", thing.string);
return 0;
}
$> ./a.out
bbaaaaaaaaaaaaaaaaaaaaaa
bbbbbbaaabbaaaaaaaaabaaa
aabaaaabbbabbbbbbbbaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaa
Между этими строками может вклиниться другой поток
$> clang 6_bad_lock.c
struct complex_thing {
bool lock;
char string[128];
};
volatile struct complex_thing thing;
volatile bool start = false;
void *thread_f(void *arg)
{
int id = (int) arg;
while (! start) {};
if (! thing.lock) {
thing.lock = true;
for (int i = 0; i < 127; ++i)
thing.string[i] = 'a' + id;
}
return NULL;
}
int main()
{
pthread_t tid[6];
for (int i = 0; i < 6; ++i) {
pthread_create(&tid[i], NULL, thread_f,
(void *) i);
}
start = true;
for (int i = 0; i < 6; ++i)
pthread_join(tid[i], NULL);
printf("%s\n", thing.string);
return 0;
}
$> ./a.out
aaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaa
$> ./a.out
ffffffffffffffffffffffff
ffffffffffffffffffffffff
ffffffffffffffffffffffff
ffffffffffffffffffffffff
ffffffffffffffffffffffff
fffffff
struct complex_thing {
bool lock;
char string[128];
};
volatile struct complex_thing thing;
volatile bool start = false;
void *thread_f(void *arg)
{
int id = (int) arg;
while (! start) {};
if (__sync_bool_compare_and_swap(&thing.lock, 0, 1)) {
for (int i = 0; i < sizeof(thing.string) - 1; ++i)
thing.string[i] = 'a' + id;
__sync_bool_compare_and_swap(&thing.lock, 1, 0);
}
return NULL;
}
int main()
{
pthread_t tid[6];
for (int i = 0; i < 6; ++i) {
pthread_create(&tid[i], NULL, thread_f,
(void *) i);
}
start = true;
for (int i = 0; i < 6; ++i)
pthread_join(tid[i], NULL);
printf("%s\n", thing.string);
return 0;
}
void
spin_lock(volatile bool *lock)
{
while (! __sync_bool_compare_and_swap(lock, 0, 1))
{}
}
void
spin_unlock(volatile bool *lock)
{
__sync_bool_compare_and_swap(lock, 1, 0);
}
Спинлок - блокировка средствами процессора, без прерываний
Разблокируется быстро - не потеряются кеши и регистры
Не разблокируется быстро - впустую потрачено время процессора
Fast User-space Mutex
Mutex - не базовое понятие в Linux. Futex - базовое.
Оркестрация через ядро
int
futex(int *uaddr, int futex_op, int val,
const struct timespec *timeout,
int *uaddr2, int val3);
int
futex(int *uaddr, int futex_op, int val,
const struct timespec *timeout,
int *uaddr2, int val3);
Futex - это просто значение int
Поток изъявляет желание узнать, когда это значение станет != val
futex(&value, FUTEX_WAIT, wait_not_for);
Другой поток может уведомить об изменении 1 и более других потоков
futex(&value, FUTEX_WAKE, n_to_wake);
Поток 1
Поток 2
int value = 100;
futex(&value, FUTEX_WAIT, 100,
NULL, NULL, 0);
/* sleep ... */
Поток засыпает и хочет проснуться, когда value будет != 100
value = 0;
futex(&value, FUTEX_WAKE, 1,
NULL, NULL, 0);
Другой поток меняет значение на 0 и уведомляет ждущих
value != 100;
/* continue ... */
Ядро пробуждает поток
int
futex_wait(int *futex, int val)
{
return syscall(SYS_futex, futex, FUTEX_WAIT, val,
NULL, NULL, 0);
}
int
futex_wake(int *futex)
{
return syscall(SYS_futex, futex, FUTEX_WAKE, 1,
NULL, NULL, 0);
}
void
futex_lock(int *futex)
{
while (__sync_val_compare_and_swap(futex, 0, 1) != 0)
futex_wait(futex, 1);
}
void
futex_unlock(int *futex)
{
__sync_bool_compare_and_swap(futex, 1, 0);
futex_wake(futex);
}
Библиотечной обертки нет - нужно звать syscall напрямую
Futex сам значение не меняет - это нужно делать руками, причем атомарно
Fast User-space Mutex - потому что первая попытка не в ядре
Разблокировать так же
Будем хранить всего два значения - 0 и 1, как "свободно" и "занято"
volatile int counter = 0;
volatile bool start = false;
volatile int futex = 0;
void *thread_f(void *futex)
{
while (! start) {};
for (int i = 0; i < 100000; ++i) {
futex_lock((int *) futex);
counter = counter + 1;
futex_unlock((int *) futex);
}
return NULL;
}
int main()
{
pthread_t tid[6];
int futex = 0;
for (int i = 0; i < 6; ++i) {
pthread_create(&tid[i], NULL, thread_f,
(void *) &futex);
}
start = true;
for (int i = 0; i < 6; ++i)
pthread_join(tid[i], NULL);
printf("%d\n", counter);
return 0;
}
$> gcc 8_futex.c -pthread
$> ./a.out
600000
$> ./a.out
600000
$> ./a.out
600000
$> ./a.out
600000
void
futex_lock(int *futex)
{
while (__sync_val_compare_and_swap(futex, 0, 1) != 0)
futex_wait(futex, 1);
}
void
futex_unlock(int *futex)
{
__sync_bool_compare_and_swap(futex, 1, 0);
futex_wake(futex);
}
Как можно избежать futex_wake() в unlock(), если никто не ждет на futex_wait()?
Защитить весь futex спинлоком, и добавить счетчик ждущих. Если он 0 на unlock, то не делать futex_wake().
2 балла
read a
write b
read c
Исходный код:
Реальное выполнение:
write e
read a
write b
read c
write e
> code
Процессор может выполнять инструкции в произвольном порядке, если они независимы по данным
while (! is_ready)
usleep(10000);
printf("%d\n", a);
a = 200;
is_ready = true;
a = 100
Поток 1
Поток 2
На экран может быть выведено как 100, так и 200. Поток 1 может увидеть is_ready=true раньше, чем увидит a=200. Из-за переупорядочивания.
Без защиты потоки могут видеть друг друга абсолютно как угодно.
X = 1
r1 = Y
Поток 1
Y = 1
r2 = X
Поток 2
X = 0
Y = 0
На самом деле в конце r1 и r2 могут стать оба равны 0, даже на x86. Команды выше раскладываются на инструкции:
write(X, 1)
read(register, Y)
write(r1, register)
Поток 1
Поток 2
write(Y, 1)
read(register, X)
write(r2, register)
Эти чтения могут завершиться первыми
Логично, что в конце либо r1, либо r2, либо оба должны быть = 1
LoadLoad
LoadStore
StoreStore
StoreLoad
read(B); write(A);
A = B; C = D;
read(D); write(C);
read(B); write(A);
read(D); write(C);
read(B); write(A);
read(D); write(C);
read(B); write(A);
read(D); write(C);
Барьер
Барьер
Барьер
Барьер
StoreStore
StoreLoad
LoadLoad
LoadStore
Acquire
Release
Реальных барьеров 3:
Full
Acquire: LoadLoad + LoadStore - непреодолим "вверх"
Release: LoadStore + StoreStore - непреодолим "вниз"
while (! read_acquire(is_ready))
usleep(10000);
printf("%d\n", a);
a = 200;
write_release(&is_ready, true);
a = 100
Поток 1
Поток 2
Acquire не даст переменной a прочитаться до is_ready.
Release не даст переменной a записаться после is_ready.
Инструкции с полным барьером
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
void __atomic_load (type *ptr, type *ret, int memorder)
void __atomic_store (type *ptr, type *val, int memorder)
void __atomic_exchange (type *ptr, type *val, type *ret, int memorder)
bool __atomic_compare_exchange (type *ptr, type *expected, type *desired, ...)
type __atomic_add_fetch (type *ptr, type val, int memorder)
type __atomic_sub_fetch (type *ptr, type val, int memorder)
type __atomic_fetch_add (type *ptr, type val, int memorder)
type __atomic_fetch_sub (type *ptr, type val, int memorder)
Инструкции с выбором барьера
__ATOMIC_RELAXED, __ATOMIC_CONSUME, __ATOMIC_ACQUIRE,
__ATOMIC_RELEASE, __ATOMIC_ACQ_REL, __ATOMIC_SEQ_CST
memorder:
void
spin_lock(volatile bool *lock)
{
while (! __atomic_test_and_set(lock, __ATOMIC_ACQUIRE))
{}
}
void
spin_unlock(volatile bool *lock)
{
__atomic_clear(lock, __ATOMIC_RELEASE);
}
Оптимальная версия спинлока, без полных барьеров
Работает в ~1.5 раза быстрее предыдущей с
__sync_bool_compare_and_swap
clone();
futex();
Linux-specific
__sync_bool_compare_and_swap();
__sync_val_compare_and_swap();
__sync_add_and_fetch();
/* ... */
GCC-specific
Mac? FreeBSD? ...
clang? MinGW? MSVC? ...
pthread_create();
pthread_join();
pthread_exit();
pthread_mutex_lock();
pthread_mutex_unlock();
/* ... */
GCC на Linux
futex();
clone();
__sync_*();
GCC на Mac
?
MinGW на Windows
?
int
pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
pthread_t - дескриптор потока в библиотеке pthread
Функция для запуска в отдельном потоке
struct pthread_t {
int lock;
pid_t tid;
void *func_result;
void *stack;
size_t stack_size;
};
Защита структуры
Идентификатор потока
Сохраненный до востребования результат функции
Стек для clone(), который pthread сам освободит
volatile bool is_finished = false;
volatile pthread_t child_id;
int
thread_f(void *arg)
{
child_id = pthread_self();
is_finished = true;
return 0;
}
int
main()
{
void *stack;
thread_create_clone(thread_f, NULL, &stack);
while (! is_finished)
sched_yield();
pthread_join(child_id, NULL);
free(stack);
return 0;
}
$> gcc 9_clone_vs_pthread.c \
-pthread
$> ./a.out
$> # finished ok
Функция из pthread зовется в потоке, созданном не через pthread_сreate()
Ресурсы освобождаются через pthread_join() - уничтожается pthread_t
int
pthread_join(pthread_t thread, void **retval);
{
while (! thread.is_finished) {};
free(thread.stack);
*retval = thread.func_value;
return 0;
}
typedef int (*cthread_f)(void *);
struct cthread {
int returned_code;
cthread_f func;
void *arg;
void *stack;
bool is_finished;
};
int cthread_runner(void *arg)
{
struct cthread *thread =
(struct cthread *) arg;
thread->returned_code =
thread->func(thread->arg);
thread->is_finished = true;
return 0;
}
void cthread_create(struct cthread *result,
cthread_f func, void *arg)
{
result->returned_code = 0;
result->func = func;
result->arg = arg;
result->is_finished = false;
thread_create_clone(cthread_runner,
(void *) result,
&result->stack);
}
int cthread_join(volatile struct
cthread *thread)
{
while (! thread->is_finished)
sched_yield();
free(thread->stack);
return thread->returned_code;
}
Дескриптор потока
Стек и код возврата для cthread_join
Запускает поток, но с другой функцией - зачем?
Потому что пользовательская функция сама не сохранит результат в cthread, и не выставит флаг завершения
int
func(void *arg)
{
printf("arg = %d\n", *((int *) arg));
return 200;
}
int
main()
{
struct cthread thread;
int arg = 100;
cthread_create(&thread, func, (void *) &arg);
int retcode = cthread_join(&thread);
printf("thread is joined with retcode = %d\n", retcode);
return 0;
}
$> gcc 10_cthreads_1.c
$> ./a.out
arg = 100
thread is joined with retcode = 200
void
pthread_exit(void *retval);
Как реализован pthread_exit?
setjmp()/longjmp()
1 балл
typedef int (*cthread_f)(void *);
struct cthread {
int returned_code;
cthread_f func;
void *arg;
void *stack;
bool is_finished;
};
int cthread_runner(void *arg)
{
struct cthread *thread =
(struct cthread *) arg;
thread->returned_code =
thread->func(thread->arg);
thread->is_finished = true;
return 0;
}
void cthread_create(struct cthread *result,
cthread_f func, void *arg)
{
result->returned_code = 0;
result->func = func;
result->arg = arg;
result->is_finished = false;
thread_create_clone(cthread_runner,
(void *) result,
&result->stack);
}
int cthread_join(volatile struct
cthread *thread)
{
while (! thread->is_finished)
sched_yield();
free(thread->stack);
return thread->returned_code;
}
typedef int (*cthread_f)(void *);
struct cthread {
int returned_code;
cthread_f func;
void *arg;
void *stack;
bool is_finished;
jmp_buf jmp;
};
int cthread_runner(void *arg)
{
struct cthread *thread =
(struct cthread *) arg;
if (setjmp(thread->jmp) == 0) {
arg = thread->arg;
thread->returned_code =
thread->func(arg);
}
thread->is_finished = true;
return 0;
}
void cthread_exit(struct cthread *thread,
int retcode)
{
thread->returned_code = retcode;
longjmp(thread->jmp, 1);
}
Позиция стека запоминается в самом начале, и на нее можно прыгнуть в любой момент
int
pthread_detach(pthread_t thread);
Чтобы не нужно было делать pthread_join(). Ресурсы освободятся сами, когда поток завершится.
typedef int (*cthread_f)(void *);
struct cthread {
int returned_code;
cthread_f func;
void *arg;
void *stack;
bool is_finished;
bool is_detached;
jmp_buf jmp;
};
void cthread_destroy(struct cthread *thread)
{
printf("thread is destroyed\n");
free(thread->stack);
}
int cthread_runner(void *arg)
{
struct cthread *thread =
(struct cthread *) arg;
if (setjmp(thread->jmp) == 0) {
arg = thread->arg;
thread->returned_code =
thread->func(arg);
}
if (thread->is_detached)
cthread_destroy(thread);
thread->is_finished = true;
return 0;
}
void cthread_detach(struct cthread *thread)
{
if (thread->is_finished)
cthread_destroy(thread);
thread->is_detached = true;
}
void cthread_create(struct cthread *result,
cthread_f func, void *arg)
{
result->returned_code = 0;
result->func = func;
result->arg = arg;
result->is_finished = false;
result->is_detached = false;
thread_create_clone(cthread_runner,
(void *) result,
&result->stack);
}
Добавился флаг is_detached
void cthread_detach(struct cthread *thread)
{
if (thread->is_finished)
cthread_destroy(thread);
thread->is_detached = true;
}
void cthread_destroy(struct cthread *thread)
{
printf("thread is destroyed\n");
free(thread->stack);
}
int cthread_runner(void *arg)
{
/* ... */
if (thread->is_detached)
cthread_destroy(thread);
thread->is_finished = true;
return 0;
}
Какие здесь две ошибки?
1) Стек освобождается прямо в cthread_runner, который на этом же стеке и работает прямо сейчас.
2) Флаги is_detached и is_finished используются не атомарно.
1 балл за каждый
int
cthread_runner(void *arg)
{
struct cthread *thread = (struct cthread *) arg;
if (setjmp(thread->jmp) == 0) {
thread->returned_code =
thread->func(thread->arg);
}
if (thread->is_detached)
cthread_destroy(thread);
thread->is_finished = true;
return 0;
}
Здесь нигде нельзя удалить свой стек - даже return его использует
struct cthread_stack {
void *stack;
struct cthread_stack *next;
};
Можно удалить стек не сразу, а отложить в список, пока поток не завершится. Но как понять, завершился ли поток в ядре?
static inline int
thread_create_clone_tid(int (*func)(void *), void *arg, void **stack,
pid_t *tid)
{
int stack_size = 65 * 1024;
*stack = malloc(stack_size);
void *stack_top = (char *) *stack + stack_size;
int flags = CLONE_VM | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_THREAD |
CLONE_CHILD_CLEARTID | CLONE_CHILD_SETTID;
return clone(func, stack_top, flags, arg, NULL, NULL, tid);
}
В clone() можно попросить занулить значение по адресу, когда поток совсем завершится
struct cthread_stack {
pid_t tid;
void *stack;
struct cthread_stack *next;
};
С каждым стеком ассоциируем число, которое завершенный clone() будет занулять
struct cthread {
int returned_code;
cthread_f func;
void *arg;
void *stack;
bool is_finished;
bool is_detached;
jmp_buf jmp;
};
void cthread_destroy(struct cthread *thread)
{
printf("thread is destroyed\n");
free(thread->stack);
}
void cthread_detach(struct cthread *thread)
{
if (thread->is_finished)
cthread_destroy(thread);
sleep(1);
thread->is_detached = true;
}
int func(void *arg)
{
printf("thread started\n");
return 0;
}
int main()
{
struct cthread thread;
cthread_create(&thread, func, NULL);
cthread_detach(&thread);
while (! thread.is_finished)
sched_yield();
printf("detached thread finished\n");
return 0;
}
$> gcc 11_bad_detach.c
$> ./a.out
thread started
detached thread finished
Стек не освобожден
struct cthread_stack {
pid_t tid;
void *stack;
struct cthread_stack *next;
};
struct cthread_stack *stack_list = NULL;
volatile bool last_stack_lock = false;
struct cthread {
int returned_code;
cthread_f func;
void *arg;
struct cthread_stack *stack;
bool lock;
bool is_finished;
bool is_detached;
jmp_buf jmp;
};
void cthread_destroy(struct cthread *thread)
{
printf("thread is destroyed\n");
spin_lock(&last_stack_lock);
struct cthread_stack *iter = stack_list;
while (iter != NULL) {
if (iter->tid != 0)
break;
struct cthread_stack *next =
iter->next;
free(iter->stack);
free(iter);
iter = next;
printf("a stack is freed\n");
}
thread->stack->next = iter;
stack_list = thread->stack;
spin_unlock(&last_stack_lock);
}
Список стеков и лок для его защиты. У каждого cthread на отдельной памяти свой стек.
При уничтожении пытаемся чистить список стеков
Те, у кого tid != 0, еще используются - их не трогать
Положить свой стек в список
int cthread_runner(void *arg)
{
struct cthread *thread =
(struct cthread *) arg;
if (setjmp(thread->jmp) == 0) {
arg = thread->arg;
thread->returned_code =
thread->func(arg);
}
spin_lock(&thread->lock);
if (thread->is_detached)
cthread_destroy(thread);
thread->is_finished = true;
spin_unlock(&thread->lock);
return 0;
}
Операции с флагами is_detached, is_finished теперь под локом
void cthread_detach(struct cthread *thread)
{
spin_lock(&thread->lock);
if (thread->is_finished)
cthread_destroy(thread);
thread->is_detached = true;
spin_unlock(&thread->lock);
}
Тоже самое
void cthread_create(struct cthread *result, cthread_f func,
void *arg)
{
result->returned_code = 0;
result->func = func;
result->arg = arg;
result->is_finished = false;
result->is_detached = false;
result->lock = false;
result->stack =
malloc(sizeof(*result->stack));
result->stack->next = NULL;
void **stack_p = &result->stack->stack;
pid_t *tid_p = &result->stack->tid;
thread_create_clone_tid(cthread_runner,
result,
stack_p,
tid_p);
}
struct cthread_stack создается на куче, чтобы он мог пережить завершение потока. В нем ядро обновит tid, когда clone() завершится.
int func(void *arg)
{
printf("thread started\n");
return 0;
}
int main()
{
struct cthread thread[10];
for (int i = 0; i < 10; ++i) {
cthread_create(&thread[i], func,
NULL);
cthread_detach(&thread[i]);
}
for (int i = 0; i < 10; ++i) {
if (! thread[i].is_finished) {
i = 0;
sched_yield();
}
}
printf("detached threads finished\n");
return 0;
}
$> gcc 12_cthread_detach.c
$> ./a.out
thread started
thread is destroyed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
thread started
thread is destroyed
a stack is freed
detached threads finished
pthread_create();
pthread_join();
pthread_detach();
int
pthread_mutex_destroy(pthread_mutex_t *mutex);
int
pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
int
pthread_mutex_lock(pthread_mutex_t *mutex);
int
pthread_mutex_trylock(pthread_mutex_t *mutex);
int
pthread_mutex_unlock(pthread_mutex_t *mutex);
int
pthread_mutex_timedlock(pthread_mutex_t *mutex,
const struct timespec *abs_timeout);
На Linux все через futex и атомарные команды процессора
int
pthread_mutex_trylock(pthread_mutex_t *mutex);
Как реализован pthread_mutex_trylock?
{
return __sync_bool_compare_and_swap(mutex->lock, 0, 1);
}
1 балл
int
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int
pthread_rwlock_init(pthread_rwlock_t *rwlock,
const pthread_rwlockattr_t *attr);
int
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int
pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int
pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock,
const struct timespec *abs_timeout);
int
pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock,
const struct timespec *abs_timeout);
int
pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int
pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
Сколько угодно блокировок на чтение
Только одна на запись
На rdlock() и wrlock() один unlock()
int
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int
pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
Почему unlock() один? Почему нет rdunlock(), rwunlock()?
/* Possible implementation. */
{
if (mutex->readers > 0)
mutex->readers--;
else
mutex->writers = 0;
}
2 балла
struct condvar {
pthread_mutex_t event_lock;
bool is_event_set;
};
void condvar_signal(struct condvar *condvar)
{
printf("condvar signaled\n");
condvar->is_event_set = true;
pthread_mutex_unlock(&condvar->event_lock);
pthread_mutex_lock(&condvar->event_lock);
condvar->is_event_set = false;
printf("condvar is locked\n");
}
void condvar_wait(struct condvar *condvar)
{
pthread_mutex_t *lock =
&condvar->event_lock;
for (;;) {
pthread_mutex_lock(lock);
if (condvar->is_event_set) {
condvar->is_event_set =
false;
pthread_mutex_unlock(lock);
return;
}
pthread_mutex_unlock(lock);
}
}
void condvar_create(struct condvar *condvar)
{
pthread_t *lock =
&condvar->event_lock;
pthread_mutex_init(lock, NULL);
pthread_mutex_lock(lock);
condvar->is_event_set = false;
}
Простой condition variable - это флаг события, защищенный мьютексом
При инициализации лок захватывается генератором событий
Ожидающие тоже пытаются взять лок, но блокируются - лок уже взят
Генератор событий при появлении события отпускает лок, и один из ждущих просыпается
struct condvar condvar;
void *
thread_f(void *arg)
{
int id = (int) arg;
for (;;) {
condvar_wait(&condvar);
printf("%d processed the event\n",
id);
}
}
int
main()
{
condvar_create(&condvar);
const int thread_count = 10;
pthread_t tid[thread_count];
for (int i = 0; i < thread_count; ++i) {
pthread_create(&tid[i], NULL,
thread_f,
(void *) i);
}
while (getchar() != EOF)
condvar_signal(&condvar);
for (int i = 0; i < thread_count; ++i)
pthread_join(tid[i], NULL);
return 0;
}
$> # Mac
$> clang 13_simple_condvar.c
$> ./a.out
condvar signaled
0 processed the event
condvar is locked
condvar signaled
1 processed the event
condvar is locked
condvar signaled
1 processed the event
condvar is locked
$> # Linux
$> gcc 13_simple_condvar.c \
-pthread
$> ./a.out
condvar signaled
condvar is locked
condvar signaled
condvar is locked
condvar signaled
condvar is locked
condvar signaled
condvar is locked
struct condvar {
pthread_mutex_t event_lock;
bool is_event_set;
int wait_count;
};
void
condvar_signal(struct condvar *condvar)
{
int *ptr = &condvar->wait_count;
pthread_mutex_t *lock =
&condvar->event_lock;
int wait_count =
__sync_fetch_and_add(ptr,
0);
printf("wait_count = %d\n",
wait_count);
if (wait_count == 0) {
printf("no waiters\n");
return;
}
printf("condvar signaled\n");
condvar->is_event_set = true;
pthread_mutex_unlock(lock);
while (condvar->is_event_set) {}
pthread_mutex_lock(lock);
condvar->is_event_set = false;
printf("condvar is locked\n");
}
void
condvar_wait(struct condvar *condvar)
{
int *wc = &condvar->wait_count;
pthread_mutex_t *lock = &condvar->event_lock;
__sync_add_and_fetch(wc, 1);
for (;;) {
pthread_mutex_lock(lock);
if (condvar->is_event_set) {
condvar->is_event_set = false;
__sync_sub_and_fetch(wc, 1);
pthread_mutex_unlock(lock);
return;
}
pthread_mutex_unlock(lock);
}
}
void
condvar_create(struct condvar *condvar)
{
pthread_mutex_t *lock =
&condvar->event_lock;
pthread_mutex_init(lock, NULL);
pthread_mutex_lock(lock);
condvar->is_event_set = false;
condvar->wait_count = 0;
}
Добавился wait_count
struct condvar condvar;
void *
thread_f(void *arg)
{
int id = (int) arg;
for (;;) {
condvar_wait(&condvar);
printf("%d processed the"\
" event\n", id);
usleep(100000);
}
}
int
main()
{
condvar_create(&condvar);
const int thread_count = 10;
pthread_t tid[thread_count];
for (int i = 0; i < thread_count; ++i) {
pthread_create(&tid[i], NULL,
thread_f, i);
}
while (getchar() != EOF)
condvar_signal(&condvar);
for (int i = 0; i < thread_count; ++i)
pthread_join(tid[i], NULL);
return 0;
}
$> gcc 14_condvar.c \
-pthread
$> ./a.out
wait_count = 10
condvar signaled
2 processed the event
condvar is locked
asdfghjklqwert
wait_count = 10
condvar signaled
4 processed the event
condvar is locked
wait_count = 9
condvar signaled
3 processed the event
condvar is locked
wait_count = 8
...
wait_count = 2
condvar signaled
1 processed the event
condvar is locked
wait_count = 1
condvar signaled
condvar is locked
2 processed the event
wait_count = 0
no waiters
wait_count = 0
no waiters
int
pthread_cond_destroy(pthread_cond_t *cond);
int
pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *attr);
int
pthread_cond_broadcast(pthread_cond_t *cond);
int
pthread_cond_signal(pthread_cond_t *cond);
int
pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex,
const struct timespec *abstime);
int
pthread_cond_wait(pthread_cond_t *cond,
pthread_mutex_t *mutex);
int
pthread_barrier_destroy(pthread_barrier_t *barrier);
int
pthread_barrier_init(pthread_barrier_t *barrier,
const pthread_barrierattr_t *attr,
unsigned count);
int
pthread_barrier_wait(pthread_barrier_t *barrier);
N
static __thread int a = -1;
volatile int ready_to_print = 0;
volatile bool print = false;
void *
thread_f(void *arg)
{
a = (int) arg;
__sync_fetch_and_add(&ready_to_print, 1);
while (! print) {}
printf("a = %d\n", a);
return NULL;
}
int
main()
{
pthread_t t1, t2;
a = 0;
pthread_create(&t1, NULL, thread_f, 1);
pthread_create(&t2, NULL, thread_f, 2);
while (__sync_fetch_and_add(&ready_to_print,
0) != 2) {}
print = true;
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("main a = %d\n", a);
return 0;
}
$> gcc 15_gcc_thread.c \
-pthread
$> ./a.out
a = 2
a = 1
main a = 0
Копия этой переменной создается для каждого потока
.text
.data
.bss
.heap
.stack
.env
kernel
.tdata
+
$> gcc 15_gcc_thread.c \
-pthread
$> objdump -s a.out
...
Contents of section .tdata:
200d8c ffffffff
...
int
pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg)
{
copy_tdata();
/* ... */
void *
pthread_getspecific(pthread_key_t key);
int
pthread_setspecific(pthread_key_t key,
const void *value);
int
pthread_key_create(pthread_key_t *key,
void (*destructor)(void*));
volatile int ready_to_print = 0;
volatile bool print = false;
pthread_key_t key;
void *
thread_f(void *arg)
{
pthread_setspecific(key, arg);
__sync_fetch_and_add(&ready_to_print, 1);
while (! print) {}
int tmp = (int) pthread_getspecific(key);
printf("value = %d\n", tmp);
return NULL;
}
int
main()
{
pthread_t t1, t2;
pthread_key_create(&key, NULL);
pthread_setspecific(key, (const void *) 0);
pthread_create(&t1, NULL, thread_f, (void *) 1);
pthread_create(&t2, NULL, thread_f, (void *) 2);
while (__sync_fetch_and_add(&ready_to_print, 0) != 2) {}
print = true;
pthread_join(t1, NULL);
pthread_join(t2, NULL);
int tmp = (int) pthread_getspecific(key);
printf("main value = %d\n", tmp);
return 0;
}
$> gcc 16_pthread_key.c \
-pthread
$> ./a.out
value = 2
value = 1
main value = 0
Ключ у всех одинаковый
Но выставляют разные значения в разных потоках
У каждого потока своя версия значения
volatile bool finish = false;
volatile bool ptr_is_set = false;
volatile int *a_ptr = NULL;
void *
thread_f(void *arg)
{
int a = 100;
printf("child stack top = %p\n", &a);
a_ptr = &a;
ptr_is_set = true;
while (! finish) {}
return NULL;
}
int
main()
{
pthread_t t;
printf("main stack top = %p\n", &t);
pthread_create(&t, NULL, thread_f, NULL);
while (! ptr_is_set) {}
printf("foreign a = %d\n", *a_ptr);
finish = true;
pthread_join(t, NULL);
return 0;
}
$> # Linux
$> gcc 17_thread_stacks.c \
-pthread
$> ./a.out
main stack top = 0x7ffeffb1fe90
child stack top = 0x7f0c9de9cee4
foreign a = 100
$> # Mac
$> clang 17_thread_stacks.c
$> ./a.out
main stack top = 0x7ffee3a53aa0
child stack top = 0x70000521bee4
foreign a = 100
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
int
pthread_attr_init(pthread_attr_t *attr);
int
pthread_attr_destroy(pthread_attr_t *attr);
int
pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int
pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int
pthread_attr_setstack(pthread_attr_t *attr,
void *stackaddr, size_t stacksize);
int
pthread_attr_getstack(const pthread_attr_t *attr,
void **stackaddr, size_t *stacksize);
int
pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int
pthread_attr_getdetachstate(const pthread_attr_t *attr,
int *detachstate);
Атрибуты до старта потока
Обратная связь: goo.gl/forms/TAeraYrqJcil7GDt1
и на портале Техносферы
В следующий раз:
IPC. Pipe, FIFO. XSI и POSIX. Сокеты: доменные, обычные.
Файловая система
Есть интерфейс вида open/close/read/write. Нужно поверх него реализовать файловую систему в оперативной памяти.
Без директорий, по готовому шаблону, формата похожего на FAT.
Цена: 15 - 25 баллов.
Срок: 2 недели.
Положить на ваш гитхаб и сказать мне его. Сдавать как угодно - лично/удаленно.
enum ufs_error_code
ufs_errno();
int
ufs_open(const char *filename, int flags);
ssize_t
ufs_write(int fd, const char *buf, size_t size);
ssize_t
ufs_read(int fd, char *buf, size_t size);
int
ufs_close(int fd);
int
ufs_delete(const char *filename);
Интерфейс для реализации
struct block {
/** Block memory. */
char *memory;
/** How many bytes are occupied. */
int occupied;
/** Next block in the file. */
struct block *next;
/** Previous block in the file. */
struct block *prev;
/* PUT HERE OTHER MEMBERS */
};
struct file {
/** Double-linked list of file blocks. */
struct block *block_list;
/**
* Last block in the list above for fast access to the end
* of file.
*/
struct block *last_block;
/** How many file descriptors are opened on the file. */
int refs;
/** File name. */
const char *name;
/** Files are stored in a double-linked list. */
struct file *next;
struct file *prev;
/* PUT HERE OTHER MEMBERS */
};
Структуры, описывающие файл
By Vladislav Shpilevoy
Потоки в linux, представление в ядре. Отличие от процессов. POSIX. Синхронизация: mutex, rw-lock, condition variable, spin lock, barrier. Атомарные операции. Атрибуты потоков и объектов синхронизации.
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.