C Thread - это что такое? Создание и ожидание выполнения потоков Недужный thread.

Модуль threading впервые был представлен в Python 1.5.2 как продолжение низкоуровневого модуля потоков. Модуль threading значительно упрощает работу с потоками и позволяет программировать запуск нескольких операций одновременно . Обратите внимание на то, что потоки в Python лучше всего работают с операциями I/O, такими как загрузка ресурсов из интернета или чтение файлов и папок на вашем компьютере.

Если вам нужно сделать что-то, для чего нужен интенсивный CPU, тогда вам, возможно, захочется взглянуть на модуль multiprocessing , вместо threading . Причина заключается в том, что Python содержит Global Interpreter Lock (GIL), который запускает все потоки внутри главного потока. По этой причине, когда вам нужно запустить несколько интенсивных операций с потоками, вы заметите, что все работает достаточно медленно. Так что мы сфокусируемся на том, в чем потоки являются лучшими: операции I/O.

Небольшое интро

Поток позволяет вам запустить часть длинного кода так, как если бы он был отдельной программой. Это своего рода вызов наследуемого процесса, за исключением того, что вы вызываете функцию или класс, вместо отдельной программы. Я всегда находил конкретные примеры крайне полезными. Давайте взглянем на нечто совершенно простое:

Import threading def doubler(number): """ A function that can be used by a thread """ print(threading.currentThread().getName() + "\n") print(number * 2) print() if __name__ == "__main__": for i in range(5): my_thread = threading.Thread(target=doubler, args=(i,)) my_thread.start()

Здесь мы импортируем модуль threading и создаем обычную функцию под названием doubler. Наша функция принимает значение и удваивает его. Также она выводи название потока, который вызывает функцию и выводит бланк-строчку в конце. Далее, в последнем блоке кода, мы создаем пять потоков, и запускаем каждый из них по очереди.

Используя многопоточность можно решить много рутинных моментов. Например загрузка видео или другого материала в социальные сети, такие как Youtube или Facebook. Для развития своего Youtube канала можно использовать https://publbox.com/ru/youtube который возьмет на себя администрирование вашего канала. Youtube отличный источник заработка и чем больше каналов тем лучше. Без Publbox вам не обойтись.

Обратите внимание на то, что когда мы определяем поток, мы устанавливаем его целью на нашу функцию doubler , и мы также передаем аргумент функции. Причина, по которой параметр args выглядит немного непривычно, заключается в том, что нам нужно передать sequence функции doubler, и она принимает только один аргумент, так что нужно добавить запятую в конце, чтобы создать sequence одной из них. Обратите внимание на то, что если вы хотите подождать, пока поток определится, вы можете вызвать его метод join (). Когда вы запустите этот код, вы получите следующую выдачу:

Thread-1 0 Thread-2 2 Thread-3 4 Thread-4 6 Thread-5 8

Конечно, вам скорее всего не захочется выводить вашу выдачу в stdout. Это может закончиться сильным беспорядком. Вместо этого, вам нужно использовать модуль Python под названием logging . Это защищенный от потоков модуль и он прекрасно выполняет свою работу. Давайте немного обновим указанный ранее пример и добавим модуль logging , и заодно назовем наши потоки:

Import logging import threading def get_logger(): logger = logging.getLogger("threading_example") logger.setLevel(logging.DEBUG) fh = logging.FileHandler("threading.log") fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s" formatter = logging.Formatter(fmt) fh.setFormatter(formatter) logger.addHandler(fh) return logger def doubler(number, logger): """ A function that can be used by a thread """ logger.debug("doubler function executing") result = number * 2 logger.debug("doubler function ended with: {}".format(result)) if __name__ == "__main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina"] for i in range(5): my_thread = threading.Thread(target=doubler, name=thread_names[i], args=(i,logger)) my_thread.start()

Самое большое изменение в этом коде – это добавление функции get_logger . Эта часть кода создаст логгер, который настроен на дебаг. Это сохранит log в нынешнюю рабочую папку (другими словами, туда, откуда запускается скрипт) и затем мы настраиваем формат каждой линии на логированный. Формат включает временной штамп, название потока, уровень логгирования и логгированое сообщение. В функции doubler мы меняем наши операторы вывода на операторы логгирования .

Обратите внимание на то, что мы передаем логгер в функцию doubler, когда создаем поток . Это связанно с тем, что если вы определяет объект логгирования в каждом потоке, вы получите несколько singletons и ваш журнал будет содержать множество повторяющихся строк. В конце мы называем наши потоки, создав список наименований, и затем устанавливаем каждый поток на особое наименование, использую параметр name. Когда вы запустите этот код, вы должны получить лог файл со следующим содержимым:

Эта выдача достаточно понятная, так что давайте пойдем дальше. Я хочу разобрать еще один вопрос в этой статье. Мы поговорим о наследовании класса под названием threading.Thread . Давайте снова рассмотрим предыдущий пример, только вместо вызова потока напрямую, мы создадим свой собственный подкласс. Вот обновленный код:

Import logging import threading class MyThread(threading.Thread): def __init__(self, number, logger): threading.Thread.__init__(self) self.number = number self.logger = logger def run(self): """ Run the thread """ logger.debug("Calling doubler") doubler(self.number, self.logger) def get_logger(): logger = logging.getLogger("threading_example") logger.setLevel(logging.DEBUG) fh = logging.FileHandler("threading_class.log") fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s" formatter = logging.Formatter(fmt) fh.setFormatter(formatter) logger.addHandler(fh) return logger def doubler(number, logger): """ A function that can be used by a thread """ logger.debug("doubler function executing") result = number * 2 logger.debug("doubler function ended with: {}".format(result)) if __name__ == "__main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina"] for i in range(5): thread = MyThread(i, logger) thread.setName(thread_names[i]) thread.start()

В этом примере мы только что унаследовали класс threading.Thread . Мы передали число, которое хотим удвоить, а также передали объект логгированмя, как делали это ранее. Но на этот раз, мы настроим название потока по-другому, вызвав функцию setName в объекте потока. Нам все еще нужно вызвать старт в каждом потоке, но запомните, что нам не нужно определять это в наследуемом классе. Когда вы вызываете старт, он запускает ваш поток, вызывая метод run . В нашем классе мы вызываем функцию doubler для выполнения наших вычислений. Выдача сильно похожа на ту, что была в примере ранее, за исключением того, что я добавил дополнительную строку в выдаче. Попробуйте сами и посмотрите, что получится.

Замки и Синхронизация

Когда у вас в распоряжении более одного потока, тогда вам, возможно, понадобится понять, как избежать конфликтов . Под этим я имею ввиду то, что вы можете использовать случай, где более одного потока нуждаются в доступе к одном и тому же ресурсу в одно и то же время. Если вы не думаете о таких проблемах и соответственном планировании, тогда вы можете столкнуться с самыми худшими проблемами в крайне неудобное время, и, как правило, в момент выпуска кода.

Решение проблемы – это использовать замки . Замок предоставлен модулем Python threading и может держать один поток, или не держать поток вообще. Если поток пытается acquire замок на ресурсе, который уже закрыт, этот поток будет ожидать до тех пор, пока замок не откроется. Давайте посмотрим на практичный пример одного кода, который не имеет никакого замочного функционала, но мы попробуем его добавить:

Import threading total = 0 def update_total(amount): """ Updates the total by the given amount """ global total total += amount print (total) if __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Мы можем сделать этот пример еще интереснее, добавив вызов time.sleep . Следовательно, проблема здесь в том, что один поток может вызывать update_total и перед тем, как он обновится, другой поток может вызвать его и тоже попытается обновить его. В зависимости от порядка операций, значение может быть добавлено единожды. Давайте добавим замок к функции. Существует два способа сделать эта. Первый – это использование try/finally , если мы хотим убедиться, что замок снят. Вот пример:

Import threading total = 0 lock = threading.Lock() def update_total(amount): """ Updates the total by the given amount """ global total lock.acquire() try: total += amount finally: lock.release() print (total) if __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Здесь мы просто вешаем замок, перед тем как сделать что-либо другое. Далее, мы пытаемся обновить total и finally , мы снимаем замок и выводим нынешний total . Мы можем упростить данную задачу, используя оператор Python под названием with :

Import threading total = 0 lock = threading.Lock() def update_total(amount): """ Updates the total by the given amount """ global total with lock: total += amount print (total) if __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

Как вы видите, нам больше не нужны try/finally , так как контекстный менеджер, предоставленный оператором with , сделал все это за нас. Конечно, вы можете обнаружить, что пишите код там, где необходимы несколько потоков с доступом к нескольким функциям. Когда вы впервые начнете писать конкурентный код , вы можете сделать что-нибудь на подобии следующего:

Import threading total = 0 lock = threading.Lock() def do_something(): lock.acquire() try: print("Lock acquired in the do_something function") finally: lock.release() print("Lock released in the do_something function") return "Done doing something" def do_something_else(): lock.acquire() try: print("Lock acquired in the do_something_else function") finally: lock.release() print("Lock released in the do_something_else function") return "Finished something else" if __name__ == "__main__": result_one = do_something() result_two = do_something_else()

Этот код хорошо работает в данном случае, но подразумевается, что у вас есть несколько потоков , вызывающих обе эти функции. Пока один поток работает над функциями, второй может, в свою очередь, обновлять данные и вы получите некорректный результат. Проблема в том, что вы сначала можете не заметить, что с результатами что-то неладное. Как найти решение этой проблеме? Давайте в ней разберемся. Первое, к чему можно прийти, это повесить замок на двух вызовах функций. Давайте попробуем обновить указанный ранее пример, чтобы получить что-то вроде следующего:

Import threading total = 0 lock = threading.RLock() def do_something(): with lock: print("Lock acquired in the do_something function") print("Lock released in the do_something function") return "Done doing something" def do_something_else(): with lock: print("Lock acquired in the do_something_else function") print("Lock released in the do_something_else function") return "Finished something else" def main(): with lock: result_one = do_something() result_two = do_something_else() print (result_one) print (result_two) if __name__ == "__main__": main()

Когда вы запустите этот код, вы увидите, что он просто висит. Причина в том, что мы просто указываем модулю threading повесить замок. Так что когда мы вызываем первую функцию, она видит, что замок уже висит и блокируется. Это будет длиться до тех пор, пока замок не снимут, что никогда и не случится, так как это не предусмотрено в коде. Хорошее решение в данном случае – использовать re-entrant замок. Модуль threading предоставляет такой, в виде функции RLock . Просто замените строку lock = threading.Lock () на lock = threading.RLock () и попробуйте перезапустить код. Теперь он должен заработать. Если вы хотите попробовать код выше но добавить в него потоки, то мы можем заменить call на main следующим образом:

If __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=main) my_thread.start()

Так мы запустим основную функцию в каждом потоке, что в свою очередь приведет к вызову остальных двух функций. В конце вы получите достаточно крупную выдачу.

Таймеры

Модуль threading включает в себя один очень удобный класс, под названием Timer , который вы можете использовать запуска действия, спустя определенный отрезок времени. Данный класс запускает собственный поток и начинают работу с того же метода start (), как и обычные потоки. Вы также можете остановить таймер, используя метод cancel. Обратите внимание на то, что вы можете отменить таймер еще до того, как он стартовал. Однажды у меня был случай, когда мне нужно было наладить связь с под-процессом, который я начал, но мне нужен было обратный отсчет. Несмотря на существования ряда различных способов решения этой отдельной проблемы, моим любимым решением всегда было использование класса Timer модуля threading. Для этого примера мы взглянем на применение команды ping. В Linux, команда ping будет работать, пока вы её не убьете. Так что класс Timer становится особенно полезным для мира Linux. Вот пример:

Import subprocess from threading import Timer kill = lambda process: process.kill() cmd = ["ping", "www.google.com"] ping = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) my_timer = Timer(5, kill, ) try: my_timer.start() stdout, stderr = ping.communicate() finally: my_timer.cancel() print (str(stdout))

Здесь мы просто настраиваем лямбду, которую мы можем использовать, чтобы убить процесс. Далее мы начинаем нашу работу над ping и создаем объект Timer. Обратите внимание на то, что первый аргумент – это время ожидания в секундах, затем – функция, которую нужно вызвать и аргумент, который будет передан функции. В нашем случае, наша функция – это лямбда, и мы передаем её список аргументов, где список содержит только один элемент. Если вы запустите этот код, он будет работать примерно 5 секунд, после чего выведет результат пинга.

Другие Компоненты Потоков

Модуль threading также включает в себя поддержу других объектов. Например, вы можете создать семафор , который является одним из древнейших синхронизационных примитивов в компьютерной науке. Семафор управляет внутренним счетчиком , который будет декрементируется когда вы вызываете acquire и увеличивается, когда вы вызываете release . Счетчик разработан таким образом, что он не может падать ниже нуля. Так что если так вышло, что вы вызываете acquire когда он равен нулю, то он заблокируется.

Еще один полезный инструмент, который содержится в модуле, это Event . С его помощью вы можете получить связь между двумя потоками, используя сигналы. Мы рассмотрим примеры применения Event в следующей статье. Наконец-то, в версии Python 3.2 был добавлен объект Barrier . Это примитив, который управляет пулом потока, при этом не важно, где потоки должны ждать своей очереди. Для передачи барьера, потоку нужно вызвать метод wait (), который будет блокировать до тех пор, пока все потоки не сделают вызов. После чего все потоки пройдут дальше одновременно.

Связь потоков

Существует ряд случаев, когда вам нужно сделать так, чтобы потоки были связанны друг с другом. Как я упоминал ранее, вы можете использовать Event для этой цели. Но более удобный способ – использовать Queue . В нашем примере мы используем оба способа! Давайте посмотрим, как это будет выглядеть:

Import threading from queue import Queue def creator(data, q): """ Creates data to be consumed and waits for the consumer to finish processing """ print("Creating data and putting it on the queue") for item in data: evt = threading.Event() q.put((item, evt)) print("Waiting for data to be doubled") evt.wait() def my_consumer(q): """ Consumes some data and works on it In this case, all it does is double the input """ while True: data, evt = q.get() print("data found to be processed: {}".format(data)) processed = data * 2 print(processed) evt.set() q.task_done() if __name__ == "__main__": q = Queue() data = thread_one = threading.Thread(target=creator, args=(data, q)) thread_two = threading.Thread(target=my_consumer, args=(q,)) thread_one.start() thread_two.start() q.join()

Давайте немного притормозим. Во первых, у нас есть функция creator (также известная, как producer ), которую мы используем для создания данных, с которыми мы хотим работать (или использовать). Далее мы получаем еще одну функцию, которую мы используем для обработки данных, под названием my_consumer . Функция creator использует метод Queue под названием put, чтобы добавить данные в очередь, затем потребитель, в свою очередь, будет проверять, есть ли новые данные и обрабатывать их, когда такие появятся. Queue обрабатывает все закрытия и открытия замков, так что лично вам эта участь не грозит.

В данном примере мы создали список значений, которые мы хотим дублировать. Далее мы создаем два потока, один для функции creator/producer , второй для consumer (потребитель). Обратите внимание на то, что мы передаем объект Queue каждому потоку, что является прямо таки магией, учитывая то, как обрабатываются замки. Очередь начнется с первого потока, который передает данные второму. Когда первый поток передает те или иные данные в очередь, он также передает их к Event , после чего дожидается, когда произойдет события, чтобы закончить. Далее, в функции consumer, данные обрабатываются, и после этого вызывается метод настройки Event , который указывает первому потоку, что второй закончил обработку, так что он может продолжать. Последняя строка кода вызывает метод join объекта Queue, который указывает Queue подождать, пока потоки закончат обработку. Первый поток заканчивает, когда ему больше нечего передавать в Queue.

Подведем итоги

Мы рассмотрели достаточно много материала. А именно:

  1. Основы работы с модулем threading
  2. Как работают замки
  3. Что такое Event и как его можно использовать
  4. Как использовать таймер
  5. Внутрипотоковая связь с использованием Queue/Event

Теперь вы знаете, как использовать потоки, и в чем они хороши. Надеюсь, вы найдете им применение в своем собственном коде!

Теги: pthreads, pthread_create, pthread_join, EINVAL, ESRCH, EDEADLK, EDEADLOCK, EAGAIN, EPERM, PTHREAD_THREADS_MAX, передача аргументов потоку, возврат аргументов из потока, ошибки pthread_create, ошибки pthread_join, ожидание потока, объединение потоков, идентификатор потока, пример pthreads.

Создание и ожидание потока

Р ассмотрим простой пример

#include #include #include #include #define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define SUCCESS 0 void* helloWorld(void *args) { printf("Hello from thread!\n"); return SUCCESS; } int main() { pthread_t thread; int status; int status_addr; status = pthread_create(&thread, NULL, helloWorld, NULL); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); } printf("Hello from main!\n"); status = pthread_join(thread, (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); } printf("joined with address %d\n", status_addr); _getch(); return 0; }

В данном примере внутри основного потока, в котором работает функция main, создаётся новый поток, внутри которого вызывается функция helloWorld. Функция helloWorld выводит на дисплей приветствие. Внутри основного потока также выводится приветствие. Далее потоки объединяются.

Новый поток создаётся с помощью функции pthread_create

Int pthread_create(*ptherad_t, const pthread_attr_t *attr, void* (*start_routine)(void*), void *arg);

Функция получает в качестве аргументов указатель на поток, переменную типа pthread_t, в которую, в случае удачного завершения сохраняет id потока. pthread_attr_t – атрибуты потока. В случае если используются атрибуты по умолчанию, то можно передавать NULL. start_routin – это непосредственно та функция, которая будет выполняться в новом потоке. arg – это аргументы, которые будут переданы функции.

Поток может выполнять много разных дел и получать разные аргументы. Для этого функция, которая будет запущена в новом потоке, принимает аргумент типа void*. За счёт этого можно обернуть все передаваемые аргументы в структуру. Возвращать значение можно также через передаваемый аргумент.

В случае успешного выполнения функция возвращает 0. Если произошли ошибки, то могут быть возвращены следующие значения

  • EAGAIN – у системы нет ресурсов для создания нового потока, или система не может больше создавать потоков, так как количество потоков превысило значение PTHREAD_THREADS_MAX (например, на одной из машин, которые используются для тестирования, это магическое число равно 2019)
  • EINVAL – неправильные атрибуты потока (переданные аргументом attr)
  • EPERM – Вызывающий поток не имеет должных прав для того, чтобы задать нужные параметры или политики планировщика.

Пройдём по программе

#define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define SUCCESS 0

Здесь мы задаём набор значений, необходимый для обработки возможных ошибок.

Void* helloWorld(void *args) { printf("Hello from thread!\n"); return SUCCESS; }

Это функция, которая будет работать в отдельном потоке. Она не будет получать никаких аргументов. По стандарту считается, что явный выход из функции вызывает функцию pthread_exit, а возвращаемое значение будет передано при вызове функции pthread_join, как статус.

Status = pthread_create(&thread, NULL, helloWorld, NULL); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); }

Здесь создаётся и сразу же исполняется новый поток. Поток не получает никаких атрибутов или аргументов. После создания потока происходит проверка на ошибку.

Status = pthread_join(thread, (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); }

Приводит к тому, что основной поток будет ждать завершения порождённого. Функция

Int pthread_join(pthread_t thread, void **value_ptr);

Откладывает выполнение вызывающего (эту функцию) потока, до тех пор, пока не будет выполнен поток thread. Когда pthread_join выполнилась успешно, то она возвращает 0. Если поток явно вернул значение (это то самое значение SUCCESS, из нашей функции), то оно будет помещено в переменную value_ptr. Возможные ошибки, которые возвращает pthread_join

  • EINVAL – thread указывает на не объединяемый поток
  • ESRCH – не существует потока с таким идентификатором, который хранит переменная thread
  • EDEADLK – был обнаружен дедлок (взаимная блокировка), или же в качестве объединяемого потока указан сам вызывающий поток.

Пример создания потоков с передачей им аргументов

П усть мы хотим передать потоку данные и вернуть что-нибудь обратно. Скажем, передавать потоку будем строку, а возвращать из потока длину этой строки.

Так как функция может получать только указатель типа void, то все аргументы следует упаковать в структуру. Определим новый тип структуру:

Typedef struct someArgs_tag { int id; const char *msg; int out; } someArgs_t;

Здесь id – это идентификатор потока (он в общем-то не нужен в нашем примере), второе поле это строка, а третье длина строки, которую мы будем возвращать.

Внутри функции приводим аргумент к нужному типу, выводим на печать строку и засовываем в структуру обратно вычисленную длину строки.

Void* helloWorld(void *args) { someArgs_t *arg = (someArgs_t*) args; int len; if (arg->msg == NULL) { return BAD_MESSAGE; } len = strlen(arg->msg); printf("%s\n", arg->msg); arg->out = len; return SUCCESS; }

В том случае, если всё прошло удачно, то в качестве статуса возвращаем значение SUCCESS, а если была допущена ошибка (в нашем случае, если передана нулевая строка), то выходим со статусом BAD_MESSAGE.

В этом примере создадим 4 потока. Для 4-х потоков понадобятся массив типа pthread_t длинной 4, массив передаваемых аргументов и 4 строки, которые мы и будем передавать.

Pthread_t threads; int status; int i; int status_addr; someArgs_t args; const char *messages = { "First", NULL, "Third Message", "Fourth Message" };

Первым делом заполняем значения аргументов.

For (i = 0; i < NUM_THREADS; i++) { args[i].id = i; args[i].msg = messages[i]; }

For (i = 0; i < NUM_THREADS; i++) { status = pthread_create(&threads[i], NULL, helloWorld, (void*) &args[i]); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); } }

Затем ждём завершения

For (i = 0; i < NUM_THREADS; i++) { status = pthread_join(threads[i], (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); } printf("joined with address %d\n", status_addr); }

Под конец ещё выводим аргументы, которые теперь хранят возвращённые значения. Заметьте, что один из аргументов «плохой» (строка равна NULL). Вот полный код

#include #include #include #include #include #define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define BAD_MESSAGE -13 #define SUCCESS 0 typedef struct someArgs_tag { int id; const char *msg; int out; } someArgs_t; void* helloWorld(void *args) { someArgs_t *arg = (someArgs_t*) args; int len; if (arg->msg == NULL) { return BAD_MESSAGE; } len = strlen(arg->msg); printf("%s\n", arg->msg); arg->out = len; return SUCCESS; } #define NUM_THREADS 4 int main() { pthread_t threads; int status; int i; int status_addr; someArgs_t args; const char *messages = { "First", NULL, "Third Message", "Fourth Message" }; for (i = 0; i < NUM_THREADS; i++) { args[i].id = i; args[i].msg = messages[i]; } for (i = 0; i < NUM_THREADS; i++) { status = pthread_create(&threads[i], NULL, helloWorld, (void*) &args[i]); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); } } printf("Main Message\n"); for (i = 0; i < NUM_THREADS; i++) { status = pthread_join(threads[i], (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); } printf("joined with address %d\n", status_addr); } for (i = 0; i < NUM_THREADS; i++) { printf("thread %d arg.out = %d\n", i, args[i].out); } _getch(); return 0; }

Выполните его несколько раз. Заметьте, что порядок выполнения потоков не детерминирован. Запуская программу, можно каждый раз получить другой порядок выполнения.

Что общего у футболки и компьютерной программы? Они обе состоят из многих потоков! В то время как нитки в футболке держат ткань в виде единого полотна, C Thread (в буквальном смысле — «нити» или «потоки») операционной системы объединяют все программы, для того чтобы выполнить последовательные или параллельные действия одновременно. Каждый поток в программе идентифицирует процесс, который запускается, когда система (system Thread C) запрашивает его. Это оптимизирует работу такого сложного устройства как персональный компьютер и положительно сказывается на его быстродействии и производительности.

Определение

В информатике С Thread, или поток выполнения, представляет собой наименьшую последовательность команд, которые управляются независимым планировщиком, который обычно является составным элементом операционной системы.

Для потоков обычно задается определенный приоритет, что означает, что некоторые потоки имеют приоритет над другими. Как только процессор завершит обработку одного потока, он может запустить следующий, ожидающий очереди. Как правило, ожидание не превышает нескольких миллисекунд. Компьютерные программы, реализующие «многопоточность», могут выполнять сразу несколько потоков. Большинство современных ОС поддерживают С Thread на системном уровне. Это означает, что когда одна программа пытается забрать все ресурсы процессора, система принудительно переключается на другие программы и заставляет программу поддержки процессора разделять ресурсы равномерно.

Термин «поток» (С Thread) также может ссылаться на серию связанных сообщений в онлайн-обсуждении. Веб-доски объявлений состоят из множества тем или веток. Ответы, отправленные в ответ на исходную публикацию, являются частью одного и того же потока. В электронной почте поток может ссылаться на серию ответов в виде команд «назад» и «вперед», относящихся к определенному сообщению, и структурировать дерево беседы.

Многопоточность C Thread в Windows

В компьютерном программировании однопоточность — это обработка одной команды за раз. Противоположность однопоточности — многопоточность. Оба термина широко используются в сообществе функционального программирования.

Многопоточность похожа на многозадачность, но позволяет обрабатывать за раз несколько потоков, но не несколько процессов. Поскольку потоки меньше и управляются более простыми инструкциями, многопоточность может возникать также и внутри процессов.

Примеры работы инструмента C Thread task

Многопоточная операционная система может одновременно выполнять несколько фоновых задач, таких как ведение журнала изменений файлов, индексирование данных и управление окнами. Веб-браузеры, поддерживающие многопоточность, могут открывать несколько окон с одновременной работой JavaScript и Flash. Если программа полностью многопоточная, разные процессы не должны влиять друг на друга, если у процессора достаточно мощности для их обработки.

Подобно многозадачности, многопоточность также улучшает стабильность программ. Многопоточность может помешать сбою программы и не допустить сбоя в работе компьютера. Поскольку каждый поток обрабатывается отдельно, то ошибка в одном из них не в состоянии нарушить работу ПК. Таким образом, многопоточность может привести к меньшему количеству сбоев в работе операционной системы в целом.

Многозадачность

Многозадачность обрабатывает несколько задач в параллельном режиме и также характеризует принципы работы компьютера. Процессор может обрабатывать сразу несколько процессов с абсолютной точностью. Тем не менее он обрабатывает только инструкции, отправленные ему программным обеспечением. Поэтому чтобы в полной мере использовать возможности ЦП, программное обеспечение должно иметь возможность обрабатывать более одной задачи за раз и также отличаться многозадачностью.

Историческая ретроспектива

Ранние операционные системы могли запускать сразу несколько программ, но не полностью поддерживали многозадачность. Одна программа могла потреблять все ресурсы процессора при выполнении определенной операции. Основные задачи операционной системы, такие как копирование файлов, не позволяли пользователю выполнять другие задачи (открытие или закрытие окон, например).

Современные операционные системы включают в себя полную поддержку многозадачности — несколько программных решений могут работать одновременно, не мешая функциональности друг друга.

Многозадачность также повышает стабильность работы компьютера. Например, если один из процессов выйдет из строя, это не повлияет на другие запущенные программы, поскольку компьютер обрабатывает каждый процесс отдельно. Это можно сравнить с процессом написания письма: если вы находитесь в середине листа и уже написали часть текста, но ваш веб-браузер неожиданно завершает работу, то вы не потеряете уже выполненную работу.

Одиночные и многопроцессорные системы

Реализация потоковых и процессорных технологий отличается, в зависимости от операционной системы, но чаще всего поток является компонентом процесса. Одновременно несколько потоков способны существовать в едином процессе, выполняя и разделяя ресурсы. В частности, C Thread потоки процесса используют в совместном режиме исполняемый код и значения переменной в любой момент времени.

Системы с одним процессором реализуют многопоточность по времени: центральный процессор (CPU) переключается между различными потоками программного обеспечения. В многопроцессорной, а также в многоядерной системе, некоторое количество потоков выполняется в параллельном режиме, причем каждый процессор или ядро ​​выполняют отдельный поток одновременно.

Виды потоков

Планировщики процессов большинства современных ОС напрямую поддерживают как временную, так и многопроцессорную потоковую обработку, в то время как ядро ​​операционной системы позволяет разработчикам управлять потоками, предоставляя нужные функции через интерфейс системного вызова. Некоторые потоковые реализации называются потоками ядра, тогда как легкие процессы (LWP) — это тип потока, который имеет одно и то же информационное состояние. Также программные решения могут иметь потоки пространства пользователя, когда они используются с таймерами (Thread timer C), сигналами или другими методами, чтобы прервать их собственное выполнение, выполняя своего рода временную привязку ad hoc.

Потоки и процессы: отличия

Потоки отличаются от классических процессов многозадачной ОС следующими характеристиками:

    процессы обычно независимы, тогда как потоки существуют как подмножества процесса;

    процессы несут гораздо больше информации, чем потоки;

    процессы имеют выделенные адресные пространства;

    процессы взаимодействуют только через системные механизмы коммуникации;

    переключение контекста между потоками в процессе происходит оперативнее, чем переключение контекста между процессами.

Превентивное и совместное планирование

В многопользовательских операционных системах превентивная многопоточность является более широко используемым подходом для контроля над временем выполнения посредством переключения контекста. Тем не менее превентивное планирование может привести к неконтролируемой программистами приоритезации и сбоям. Напротив, совместная многопоточность опирается на потоки, чтобы отказаться от контроля выполнения. Это может создавать проблемы, если совместный многозадачный поток блокируется путем ожидания ресурса.

Эволюция технологии

До начала 2000-х гг. на большинстве настольных компьютеров был только один одноядерный процессор, который не поддерживал аппаратные потоки. В 2002 г. компания Intel реализовала поддержку одновременной многопоточности на процессоре Pentium 4, который носит имя Hyper-Threading. В 2005 г. был представлен двухъядерный процессор и двухъядерный процессор AMD Athlon 64 X2.

Процессоры в интегрированных системах с более высокими требованиями к действиям в режиме реального времени способны поддерживать многопоточность, уменьшая время переключения потоков, и применяют выделенный файл регистров для каждого потока.

Модели

Перечислим основные модели реализации.

1: 1 (поток на уровне ядра) — темы, созданные пользователем в ядре, являются простейшей возможной реализацией потоков. OS/2 и Win32 используют этот подход изначально, тогда как в Linux Thread join реализует данный подход через NPTL или более старые LinuxThreads. Этот подход также используется Solaris, NetBSD, FreeBSD, macOS и iOS.

N: 1 (пользовательский поток) — эта модель предусматривает, что все потоки уровня приложения сопоставляются с одним запланированным объектом уровня ядра. При таком подходе контекстное переключение может быть выполнено очень быстро, и, кроме того, его можно даже реализовать на тех ядрах, которые не поддерживают потоковую обработку. Однако один из основных недостатков заключается в том, что он не выигрывает от аппаратного ускорения на многопоточных процессорах или компьютерах. Например: если один из потоков необходимо выполнить при запросе ввода-вывода, весь процесс блокируется и потоковая передача не может быть использована. В GNU Portable C Thread exception используется как потоковая обработка пользовательского уровня.

M: N (гибридная реализация) — модель отображает некоторое количество потоков приложений для некоторого N числа ячеек ядра, или «виртуальных процессоров». Это компромисс между потоками уровня ядра («1: 1») и пользователя («N: 1»). Системы потоковой передачи «M: N» более сложны, поскольку требуются изменения как кода ядра, так и кода пользователя. В реализации M: N библиотека потоковой обработки отвечает за планирование потоков в доступных планируемых сущностях. Это делает контекст самым оптимальным, поскольку позволяет избежать вызовов системы. Однако это увеличивает сложность и вероятность инверсии, а также субоптимальное планирование без обширной (и дорогостоящей) координации между планировщиком пользовательской среды и планировщиком ядра.

Примеры гибридной реализации — активация планировщика, используемая встроенной реализацией библиотеки POSIX NetBSD (для модели M: N, в отличие от модели реализации ядра 1: 1, или модели пользовательского пространства).

Легкие процессы, используемые более старыми версиями операционной системы Solaris (инструментарий Std Thread C).

Поддержка языков программирования

Многие формальные системы поддерживают функциональность потоков. Реализации C и C++ реализуют эту технологию и обеспечивают доступ к собственным API-интерфейсам для операционной системы. Некоторые языки программирования более высокого уровня, такие как языки Java, Python и.NET Framework, выставляют потоки разработчикам при абстрагировании специфических различий в реализации потоков в среде выполнения. Другие языковые расширения также пытаются абстрагировать концепцию параллелизма и потоковой передачи от разработчика. Некоторые языки предназначены для последовательного параллелизма с использованием графических процессоров.

Ряд интерпретируемых языков имеет реализации, которые поддерживают потоковую и параллельную обработку, но не параллельное выполнение потоков из-за глобальной блокировки интерпретатора (GIL). GIL — это блокировка взаимного исключения, выполняемая интерпретатором, которая может помешать одновременно интерпретировать код приложения на двух или более потоках одновременно, что ограничивает параллелизм в многоядерных системах.

Другие реализации программирования, такие как Tcl, используют расширение Thread sleep C. Это позволяет избегать максимального предела GIL, используя модель, где контент и код должны быть явным образом «разделены» между потоками.

Языки программирования приложений, ориентированные на события, такие как Verilog, и расширение Thread sleep C, имеют иную модель потоков, которая поддерживает максимальное их число для моделирования оборудования.

Практическая многопоточность

Многопоточные библиотеки инициируют вызов функции для генерации нового потока, который принимает функциональное значение в качестве параметра. Затем создается новый параллельный поток и запускается обработка запущенной функции с последующим возвратом. Языки программирования содержат библиотеки потоков, включающие функции глобальной синхронизации, которые позволяют создавать и успешно реализовать многопоточность с условием отсутствия ошибок, используя мьютексы, переменные условия, критические разделы, мониторы и другие виды синхронизации.


Подробное описание

Протопотоки - это тип облегченных потоков без использования стека, разработанный для систем с малым количеством памяти, таких как встраиваемые (embedded) системы на микроконтролллерах или сетевые узлы датчиков.

Протопотоки предоставляют линейное выполнение кода для событийно-управляемых систем, реализованных на языке C. Протопотоки могут использоваться как совместно с RTOS, так и без нее.

Протопотоки предоставляют блокируемый на ожидании контекст поверх системы, управляемой событиями, без затрат на стек для отдельного потока. Назначение потопотоков - реализовать последовательное выполнение кода без использования сложных машин состояния или многопоточности. Протопотоки предоставляют блокрование выполнения кода по заданному условию внутри тела функций C.

Достоинство протопотоков в том, что с ними достигается чистый событийный механизм, в котором линейное выполнение кода функции может быть заблокировано по нужному условию. В чисто событийных системах блокирование должно быть реализовано вручную, путем разделения функции на 2 части - одна часть для кода до блокирующего вызова, и вторая часть кода после блокирующего вызова. Это делает сложным использование структур управления, таких как оператор условия if() и циклы while().

Достоинство протопотоков по сравнению с обычными потоками в том, что протопоток не требует для себя отдельный стек. В системах, где память является дефицитным ресурсом, выделение нескольких стеков может привести к чрезмерным затратам памяти. В отличие от обычного потока, протопоток требует от 2 до 12 байт для хранения состояния, в зависимости от используемой архитектуры.

Заметки: Поскольу протопотоки не сохраняют контекст в стеке между блокироующим вызовом, локальные переменные не сохранятся, когда протопоток заблокировался . Это означает, что локальные переменные должны использоваться с осторожностью - если у Вас есть сомнения, то не используйте в локальные переменные в протопотоке!

Главные особенности:

  • Нет кода, привязанного к ассемблеру - protothread-библиотека написана на чистом языке C.
  • Не используются подверженные ошибкам функции, такие как longjmp().
  • Очень малый расход памяти RAM - только 2 байта на протопоток.
  • Может использоваться как операционной системой (где есть многопоточность), так и без нее.
  • Предоставляет блокирующее ожидание без многопоточности или задействования стека.

Примеры приложений, где можно использовать:

  • Системы с ограниениями по памяти.
  • Стеки протоколов, управляемые событиями.
  • Очень маленькие встраиваемые системы.
  • Сетевые узлы для датчиков.

API протопотоков состоит из 4 базовых операций. Это инициализация PT_INIT() , выполнение PT_BEGIN() , блокирование на условии PT_WAIT_UNTIL() и выход PT_END() . Кроме того для удобства есть еще 2 функции блокировка по обратному условию PT_WAIT_WHILE() и блокировка на протопотоке PT_WAIT_THREAD() .

См. также: Документация Protothread API

Авторы

Protothread-библиотека была написана Adam Dunkels с поддержкой Oliver Schmidt .

Protothread-ы

Протопотоки яляются экстремально облегченными, безстековыми потоками, которые предоставляют блокируемый контекст поверх системы, управляемой событиями, без издержек на стек для каждого потока. Назначение протопотоков - реализация последовательного потока выполнения без сложных машин состояния или полного мультитрединга. Протопотоки предоставляют блокрование выполнения кода по заданному условию внутри тела функций C.

В системах с ограниченной памятью (наподобие систем на микроконтроллерах) традиционный мультитрединг приводит к слишком большим расходам памяти. В традиционном мультитрединге для каждого потока требуется отдельный стек, который может занимать большой объем памяти.

Главное достоинство протопотоков по сравнению с обычными потоками в том, что протопоток очень облегчен, и не требует для себя отдельный стек. Вместо этого все протопотоки используют один и тот же стек системы, и переключение контекста происходит методом перемотки стека. Это является достоинством в системах, где память - дефицитный ресурс, потому что выделение нескольких стеков для потоков может привести к чрезмерным затратам памяти. Протопоток требует только 2 байта на один протопоток. Кроме того, протопотоки реализованы на чистом C, и не требуют специфического кода ассемблера, привязанного к архитектуре.

Протопоток работает в пределах единственной функции C и не может охватывать другие функции. Протопоток может вызывать обычные функции C, но внутри вызванной функии блокировка невозможна. Вместо блокировки внутри вызываемой функции делается порождение отдельного протопотока для каждой потенциально блокируемой функции. Достоинство этого подхода в явном блокировании: программист точно знает, какие функции блокируют выполнение, а какие нет.

Протопотоки похожи на асимметричные сопрограммы (co-routines). Главное отличие от сопрограмм в том, что сопрограммы используют стек для каждой сопрограммы, а протопотоки не используют для себя отдельный стек. Наиболее похожий механизм, как у протопотоков, есть у генераторов Python. У них тоже безстековая конструкция, только другое предназначение. Протопотоки предоставляют блокировки контекста внутри функции C, а генераторы Python предоставляют несколько точек выхода из функции генератора.

Локальные переменные

Заметки: Поскольку протопотоки не сохраняют контекст в стеке между блокирующими вызовами, то локальные переменные не будут сохранены, когда протопоток делает блокировку. Это означает, что локальные переменные должны использоваться с осторожностью - если у Вас есть сомнения, то не используйте в локальные переменные в протопотоке!

Шедулинг (планировщик задач) протопотоков

Протопоток управляется с помощью повторяющихся вызовов функции, в которой протопоток работает. Каждый раз, когда функция вызывается, протопоток будет работать, пока не сделает блокировку или не завершится. Таким образом, шедулинг выполняется приложением, которое использует протопотоки.

Реализация

Протопотоки реализованы с использованием локальных продолжений . Локальное продолжение представляет текущее состояние выполнения в отдельном месте программы, но не предоставляет какую-либо историю вызовов или локальные переменные. Локальное продолжение может быть установлено в отдельной функции для захвата состояния функции. После того, как локальное продолжение установлено, оно может быть продолжено в виде восстановления состояния функции в той точке, где локальное продолжение было установлено. Прим. переводчика: звучит конечно как бред, но кое-что станет понятно, если посмотрите код макросов протопотоков, и как они используются - например в сетевом приложении hello-world, которое построено на протопотоке.

Локальное продолжение может быть реализовано несколькими способами:

  1. с помощью привязанного к архитектуре кода на ассемблере,
  2. с помощью стандартных конструкций C, или
  3. с помощью расширений компилятора.

Первый способ работает путем сохранения и восстановления состояния процессора, за исключением указателей стека, и требует от 16 до 32 байт на протопоток. Точное количество памяти зависит от используемой архитектуры процессора.

Реализация на стандартном C требует только 2 байта на протопоток под сохранение состояния, и задействует оператор C switch() неочевидным способом. Эта реализация вводит, однако, небольшое ограничение для кода, который использует протопотоки - сам код не может использовать операторы switch().

У определенных компиляторов есть расширения C, которые можно использовать для реализации протопотоков. GCC поддерживает указатели-метки, которые могут использоваться для этой цели. С такой реализацией протопотоки потребуют 4 байта RAM на один протопоток.

Макросы

Примеры: dhcpc.c .

Примеры: dhcpc.c .

См. также: PT_SPAWN() Примеры: dhcpc.c .

См. определение в файле