Приём заказов:
Круглосуточно
Москва
ул. Никольская, д. 10.
Ежедневно 8:00–20:00
Звонок бесплатный

Процессы, потоки, синхронизация

Диплом777
Email: info@diplom777.ru
Phone: +7 (800) 707-84-52
Url:
Логотип сайта компании Диплом777
Никольская 10
Москва, RU 109012
Содержание

Введение

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

Наиболее фундаментальным понятием является поток (thread). Под словом «поток» имеется в виду «поток команд», то есть последовательность инструкций, которые считывает и исполняет процессор. Все современные ОС позволяют запустить много потоков, которые будут исполняться параллельно. Таким образом, если вам нужно, чтобы ваша программа одновременно работала над несколькими задачами, проще всего поручить каждую задачу отдельному потоку. Конечно же, один процессор может одновременно исполнять лишь один поток инструкций. Поэтому «подлинная» много поточность, когда потоки команд исполняются действительно параллельно и независимо, возможна только в многопроцессорной машине. Обычно же число потоков в системе гораздо больше числа процессоров (которых, как правило всего-то один). Поэтому операционной системе приходится эмулировать много поточность, заставляя процессор поочередно переключаться между потоками. Время работы процессора разбивается на небольшие интервалы, обычно называемые квантами. Когда квант времени заканчивается, выполнение текущего потока приостанавливается, содержимое регистров процессора сохраняется в специальной области памяти, и он переключается на обработку следующего потока. Когда очередь снова дойдет до этого потока, содержимое регистров будет полностью восстановлено и работа потока продолжится так, как будто она вовсе и не прерывалась. Таким образом, переключение потоков происходит совершенно незаметно для них самих. Хочу особо обратить внимание, что программист не может напрямую управлять переключением потоков. У него нет возможности, скажем, дать команду «переключиться на исполнение вот этого потока». Механизмы синхронизации, о которых речь пойдет ниже, управляют потоками косвенным образом. Так задумано специально, чтобы скрыть от потоков, действительно ли они исполняются параллельно разными процессорами или по очереди одним. Именно это свойство, прозрачность механизма распределения процессорного времени, и позволяет легко и в полной мере использовать достоинства многопроцессорных систем. Рассмотрим свойства потока. С каждым из них связан свой набор значений регистров процессора, называемый контекстом потока (thread context). Кстати, его можно получить функцией Get Thread Context, впрочем, это можно понадобиться разве что при создании отладчика. Но кроме этого система создает еще ряд других объектов, однозначно связанных с каждым потоком. Важнейший из них (после контекста) – конечно же, стек (создание своего стека для каждого потока возможно благодаря тому, что в контексте потока сохраняется, в том числе, и регистр указателя стека, ESP). Для нас это важно, поскольку именно в стеке создаются и хранятся все локальные или автоматические переменные. Поскольку у каждого потока свой стек, то у каждого потока имеется свой собственный экземпляр локальной переменной, доступный только ему. Значит, синхронизация при работе с такими переменными не требуется. Можно, конечно, передать каким-либо образом другому потоку указатель на локальную переменную, но вряд ли это будет хорошим решением. Время жизни локальной переменной ограничено, значит, вам придется проследить, чтобы чужой поток закончил работу с переменной до того, как текущий завершит функцию, в которой эта переменная определена и автоматически удалит ее. В отличие от локальных переменных, у глобальной или статической переменной всегда имеется лишь один экземпляр, доступный сразу всем потокам. Поэтому их удобно использовать для обмена данными между потоками, хотя необходимость в синхронизации доступа к ним все равно необходима. Однако с ними связана и большая проблема. Если функцию, использующую только локальные переменные, можно использовать в многопоточной среде без каких-либо изменений, то с функциями, использующими глобальные переменные, так уже не получится. «Старый стиль» программирования, рассчитанный на однопоточную среду, часто предполагает использование глобальных переменных для связи между различными функциями. Например, стандартная библиотека C (C run-time library, RTL) использует глобальную переменную errno, куда заносится код ошибки в том случае, если системный вызов закончился неудачно. Теперь представим себе, что после того, как один поток вызвал системную функцию, но до того, как успел прочитать код ошибки, другой поток успел вызвать другую системную функцию. В результате, первый поток будет введен в заблуждение: первая функция могла завершиться успешно, а вторая нет, или в обеих произошли ошибки, но код их совершенно разный. В общем, результат непредсказуем. Обратите внимание и на то, что такая ошибка, очевидно, будет плавающей, то есть возникать не всегда, а от случая к случаю. Этим очень неприятным свойством обладают практически все ошибки, связанные с синхронизацией. Кстати, по этой причине отладку и особенно тестирование многопоточных приложений крайне желательно вести на многопроцессорной машине, где подобные ошибки обычно проявляются гораздо активней. Таким образом, разработка многопоточных приложений требует особого стиля программирования. Одно из основных правил при этом можно сформулировать следующим образом: избегайте по возможности глобальных переменных, используйте их лишь для связи между потоками. К сожалению, бывают случаи, когда без глобальной переменной все-таки не обойтись. Часто это случается, когда необходимо перенести какой-нибудь код, рассчитанный на однопоточную среду, в многопоточное приложение. Для таких ситуаций в Win32 предусмотрен механизм под названием «локальная память потока» (thread local storage, TLS). Он позволяет создать глобальную переменную, которая, тем не менее, подобно локальной, имеет отдельный экземпляр для каждого потока. Есть два способа работы с TLS: с помощью функций API (TlsAlloc, TlsGetValue, TlsSetValue, TlsFree) или используя поддержку компилятора (в Visual C++ для этого используется атрибут__declspec(thread)). К сожалению, и у того и другого подхода есть серьезные ограничения! Проблема первого состоит в том, что число TLS-слотов, выделяемых одному процессу, ограничено. Со вторым проблемы возникают при использовании таких переменных в DLL. Обратите внимание, что для иллюстрации того, как не надо проектировать программы, рассчитанные на работу в многопоточной среде, я взял пример не из какой-нибудь школьной поделки, а из стандартной библиотеки языка C, которая используется практически в каждой программе! И это является большой проблемой для разработчиков компиляторов. Дело в том, что язык C начал развиваться еще в те далекие времена, когда о много поточности и не слышали. Поэтому стандартная библиотека проектировалась в «старом» стиле. И теперь для того, чтобы приспособить ее к новым требованиям, системным программистам приходится использовать TLS и прочие уловки. В результате Visual C++, например, поставляется с двумя версиями RTL: для однопоточных и многопоточных приложений. Последняя универсальна, но и гораздо более объемна по сравнению с первой. У нее в свою очередь тоже два варианта: статическая библиотека и динамическая (та самая MSVCRT.DLL, знакомая не только программистам, но и многим пользователям). Теоретически, однопоточную библиотеку можно использовать и в многопоточном приложении при условии, что никакие другие потоки кроме главного не будут обращаться к ней. Однако я настоятельно рекомендую не пытаться так делать. Слишком легко проглядеть какое-нибудь неявное обращение к стандартной библиотеке и получить в результате непонятные ошибки. Все равно сэкономите вы не так много. В Windows с потоком связан еще ряд системных объектов, например очередь APC и очередь оконных сообщений. О последней мы поговорим более подробно в четвертой части, когда речь пойдет о синхронизации с использованием функции окна. В наследство от UNIX Windows достались также «волокна» (fiber). В UNIX они появились раньше настоящих потоков, а в Windows, наоборот, волокна были введены главным образом для облегчения переноса UNIX’овых приложений. Волокно – это в некотором роде «неполноценный» поток. В отличие от настоящих потоков, программист должен сам управлять переключением процессора с исполнения одного волокна на другое. Отсюда очевидно, что волокна не могут исполняться параллельно разными процессорами, то есть преимущества многопроцессорности сразу теряются. По сути, если поддержка много поточности осуществляется на аппаратном уровне, то волокна – это всего лишь программная эмуляция много поточности. Поэтому целесообразность использование их для каких-либо других целей, кроме как портирования UNIX-приложений, лично мне представляется сомнительной. Итак, мы вспомнили, а кто-то, может быть, и впервые узнал, что такое «поток». Но с этим понятием неразрывно связано другое: «процесс». Недаром соответствующий раздел в MSDN так и называется «Processes and Threads».

Процесс – это объединение нескольких потоков. А объединяет эти потоки единое виртуальное адресное пространство. В этом пространстве размещаются код и данные приложения (обычно это один exe- и несколько dll-модулей). Именно единство этого пространства и делает обмен данными между потоками приложения предельно простым. Наоборот, адресные пространства различных процессов независимы и отделены друг от друга (хотя, используя проекции файла в память (memory mapped file), можно создать область памяти, которая будет доступна совместно нескольким процессам). Другими словами, один и тот же виртуальный адрес в разных процессах соответствует разным физическим адресам. Поэтому по одному и тому же виртуальному адресу в разных процессах могут находиться совершенно разные данные, при этом модификация данных в одном процессе никак не отразится на данных в другом. В первую очередь это сделано для повышения надежности всей системы, чтобы ошибка в одном приложении не могла привести к порче данных, принадлежащих другому. Таким образом, процесс – это несколько потоков (как минимум один) плюс единое виртуальное адресное пространство. Поскольку все потоки процесса работают в едином адресном пространстве, обмен данными между ними крайне прост, однако при этом требуется согласовывать их работу над совместными данными. Собственно, под термином «синхронизация», как правило, имеют в виду именно согласование работы потоков, принадлежащих одному процессу. Этому и будут посвящены следующие части данной статьи. Хотя некоторые из описанных далее приемов можно использовать и для синхронизации потоков принадлежащих разным процессам, в основном согласование их работы связано с «механизмами взаимосвязи процессов» (inter-process communications, IPC). Действительно, трудно представить ситуацию, когда нам потребовалось бы согласовывать движение потоков без необходимости обмена данными между ними. А для этого, если потоки работают в разных адресных пространствах, требуются специальные механизмы, носящие обобщенное название IPC (проекции файлов в память – один из них). Любопытно, что идея запустить несколько потоков в едином адресном пространстве пришла в голову программистам далеко не сразу. В UNIX’ах, например, до самого недавнего времени понятия потока вообще не было, притом, что многозадачность была заложена в эту систему с самого начала! Изначально у них один процесс – один поток. С этим связаны и проблемы стандартной библиотеки C, о которых я упомянул, ведь долгое время язык C развивался именно в UNIX’овой однопоточной среде. Впрочем, современные версии этой системы поддерживают много поточность. Хотя, по-видимому, эта идея у них до конца так и не прижилась, юниксовые программисты по-прежнему предпочитают запустить несколько процессов, нежели несколько потоков в одном процессе. Именно так, например, работает знаменитый web-сервер Apache. Итак, процесс – это набор потоков, работающих в едином адресном пространстве. Очевидно, что само по себе адресное пространство без потоков смысла не имеет. Поэтому процесс считается завершенным, как только завершатся все его потоки.

Технологии многопоточности

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

Одно из решений данной проблемы связано с реализацией концепции “параллелизма на уровне тредов (потоков)” – TLP (Thread Level Parallelism). Если программные коды не в состоянии загрузить работой все или даже большинство функциональных устройств, то можно разрешить процессору выполнять более чем одну задачу (тред, или поток), чтобы дополнительные потоки загрузили простаивающие устройства. Здесь нетрудно усмотреть аналогию с многозадачной операционной системой: чтобы процессор не простаивал, когда задача оказывается в состоянии ожидания (например, завершения ввода-вывода), операционная система переключает процессор на выполнение другой задачи. Более того, некоторые механизмы диспетчеризации в операционной системе (например, квантование) имеют аналоги в много потоковой архитектуре (MTA – MultiThreading Architecture). Очевидно, архитектура, поддерживающая параллелизм на уровне потоков (TLP), должна гарантировать, что треды не будут использовать одновременно одни и те же ресурсы, для чего требуются дополнительные аппаратные средства (например, дублирование регистровых файлов). Однако оказалось, что можно реализовать МТА на базе современных суперскалярных процессоров, и это требует лишь относительно небольших аппаратных доработок, что резко повышает привлекательность МТА в глазах проектировщиков.

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

Архитектура с одновременным выполнением тредов – SMT (Simultaneous Multi-Threading) допускает одновременное выполнение нескольких потоков. В этом случае на каждом новом такте на выполнение в какое-либо исполнительное устройство может направляться команда любого потока. Одна из основных особенностей SMT у многих современных процессоров – переименование регистров, когда логические (архитектурные) регистры отображаются в физические, с которыми и ведется реальная работа. Техника переименования регистров может, очевидно, применяться для того, чтобы избежать прямого дублирования файлов регистров как аппаратной принадлежности потока. Анонсированная в 2002 году компанией Intel технология Hyper-Threading – пример многопоточной обработки команд. Данная технология является чем-то средним между многопоточной обработкой, реализованной в мультипроцессорных системах, и параллелизмом на уровне инструкций, реализованном в однопроцессорных системах. Фактически технология Hyper-Threading позволяет организовать два логических процессора в одном физическом. Таким образом, с точки зрения операционной системы и запущенного приложения в системе существует два процессора, что даёт возможность распределять загрузку задач между ними.

Посредством реализованного в технологии Hyper-Threading принципа параллельности можно обрабатывать инструкции в параллельном (а не в последовательном) режиме, то есть для обработки все инструкции разделяются на два параллельных потока. Это позволяет одновременно обрабатывать два различных приложения или два различных потока одного приложения и тем самым увеличить IPC процессора (количество инструкций, выполняемых процессором в секунду), что сказывается на росте его производительности.

В конструктивном плане процессор с поддержкой технологии Hyper-Threading состоит из двух логических процессоров, каждый из которых имеет свои регистры и контроллер прерываний (Architecture State, AS), а значит, две параллельно исполняемые задачи работают со своими собственными независимыми регистрами и прерываниями, но при этом используют одни и те же ресурсы процессора для выполнения своих задач. После активизации каждый из логических процессоров может самостоятельно и независимо от другого процессора выполнять свою задачу, обрабатывать прерывания либо блокироваться. Таким образом, от реальной двухпроцессорной конфигурации новая технология отличается только тем, что оба логических процессора используют одни и те же исполняющие ресурсы, одну и ту же разделяемую между двумя потоками кэш-память и одну и ту же системную шину. Использование двух логических процессоров позволяет усилить процесс параллелизма на уровне потока, реализованный в современных операционных системах и высокоэффективных приложениях. Команды от исполняемых параллельно потоков одновременно посылаются для обработки ядру процессора. Используя технологию out-of-order (исполнение командных инструкций не в порядке их поступления), ядро процессора способно параллельно обрабатывать оба потока за счёт использования нескольких исполнительных модулей.

Идея технологии Hyper-Threading тесно связана с микроархитектурой NetBurst процессора Pentium 4 и является в каком-то смысле её логическим продолжением.

Микроархитектура Intel NetBurst позволяет получить максимальный выигрыш в производительности при выполнении одиночного потока инструкций, то есть при выполнении одной задачи. Однако даже в случае специальной оптимизации программы не все исполнительные модули процессора оказываются задействованными на протяжении каждого тактового цикла. В среднем при выполнении кода, типичного для набора команд IA-32, реально используется только 35% исполнительных ресурсов процессора, а 65% исполнительных ресурсов процессора простаивают, что означает неэффективное использование возможностей процессора. Было бы логично реализовать работу процессора таким образом, чтобы в каждом тактовом цикле максимально использовать его возможности. Именно эту идею и реализует технология Hyper-Threading, подключая незадействованные ресурсы процессора к выполнению параллельной задачи.

Средства синхронизации потоков и процессов

поток процессор микроархитектура синхронизация

Взаимоисключения (mutex, мьютекс) — это объект синхронизации, который устанавливается в особое сигнальное состояние, когда не занят каким-либо потоком. Только один поток владеет этим объектом в любой момент времени, отсюда и название таких объектов (от английского mutually exclusive access — взаимно исключающий доступ) — одновременный доступ к общему ресурсу исключается. После всех необходимых действий мьютекс освобождается, предоставляя другим потокам доступ к общему ресурсу. Объект может поддерживать рекурсивный захват второй раз тем же потоком, увеличивая счетчик, не блокируя поток, и требуя потом многократного освобождения. Таков, например, mutex в Win32 и KMUTEX в ядре Windows. Тем не менее есть и такие реализации, которые не поддерживают такое и приводят к взаимной блокировке потока при попытке рекурсивного захвата. Это FAST_MUTEX в ядре Windows и критическая секция в Win32.

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

События. Объект, хранящий в себе 1 бит информации «просигнализирован или нет», над которым определены операции «просигнализировать», «сбросить в не просигнализированное состояние» и «ожидать». Ожидание на просигнализированном событии есть отсутствие операции с немедленным продолжением исполнения потока. Ожидание на не просигнализированном событии приводит к приостановке исполнения потока до тех пор, пока другой поток (или же вторая фаза обработчика прерывания в ядре ОС) не просигнализирует событие. Возможно ожидание нескольких событий в режимах «любого» или «всех». Возможно также создания события, автоматически сбрасываемого в не просигнализированное состояние после пробуждения первого же — и единственного — ожидающего потока (такой объект используется как основа для реализации объекта «критическая секция»). Активно используются в MS Windows, как в режиме пользователя, так и в режиме ядра. Аналогичный объект имеется и в ядре Linux под названием kwait_queue.

Критические секции обеспечивают синхронизацию подобно мьютексам за исключением того, что объекты, представляющие критические секции, доступны в пределах одного процесса. События, мьютексы и семафоры также можно использовать в одно процессном приложении, однако реализации критических секций в некоторых ОС (например, Windows NT) обеспечивают более быстрый и более эффективный[1][2] механизм взаимно-исключающей синхронизации — операции «получить» и «освободить» на критической секции оптимизированы для случая единственного потока (отсутствия конкуренции) с целью избежать любых ведущих в ядро ОС системных вызовов. Подобно мьютексам объект, представляющий критическую секцию, может использоваться только одним потоком в данный момент времени, что делает их крайне полезными при разграничении доступа к общим ресурсам.

Условные переменные (condvars). Сходны с событиями, но не являются объектами, занимающими память — используется только адрес переменной, понятие «содержимое переменной» не существует, в качестве условной переменной может использоваться адрес произвольного объекта. В отличие от событий, установка условной переменной в просигнализированное состояние не влечет за собой никаких последствий в случае, если на данный момент нет потоков, ожидающих на переменной. Установка события в аналогичном случае влечет за собой запоминание состояния «просигнализировано» внутри самого события, после чего следующие потоки, желающие ожидать события, продолжают исполнение немедленно без остановки. Для полноценного использования такого объекта необходима также операция «освободить mutex и ожидать условную переменную атомарно». Активно используются в UNIX-подобных ОС. Дискуссии о преимуществах и недостатках событий и условных переменных являются заметной частью дискуссий о преимуществах и недостатках Windows и UNIX.

Описание программы

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

Листинг программы

unit Unit1;

interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs, ExtCtrls, ComCtrls, StdCtrls, Unit2 ;

type

TForm1 = class(TForm)

Edit1: TEdit;

Edit2: TEdit;

TrackBar1: TTrackBar;

TrackBar2: TTrackBar;

Timer1: TTimer;

Label1: TLabel;

Label2: TLabel;

Label3: TLabel;

Label4: TLabel;

Label5: TLabel;

Label6: TLabel;

procedure FormCreate(Sender: TObject);

procedure Timer1Timer(Sender: TObject);

procedure TrackBar1Change(Sender: TObject);

private

{ Private declarations }

public

Thread1, Thread2: TSimpleThread;

{ Public declarations }

end;

var

Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);

begin

Thread1:=TSimpleThread.Create(False);

Thread1.Priority:=tpLowest;

Thread2:=TSimpleThread.Create(False);

Thread2.Priority:=tpLowest;

end;

procedure TForm1.Timer1Timer(Sender: TObject);

begin

Edit1.Text:=IntToStr(Thread1.Count);

Edit2.Text:=IntToStr(Thread2.Count);

Thread1.Count:=0;

Thread2.Count:=0;

end;

procedure TForm1.TrackBar1Change(Sender: TObject);

Var

I: Integer;

Priority : TThreadPriority;

begin

Priority:=tpLowest;

For I:=0 To (Sender as tTrackBar).Position – 1 Do

inc(Priority);

If Sender=TrackBar1 Then Thread1.Priority:=Priority

Else Thread2.Priority:=Priority;

end;

end.

unit Unit2;

interface

uses

Classes;

type

TSimpleThread = class(TThread)

private

{Private declarations }

protected

procedure Execute; override;

public

Count: integer;

end;

implementation

{Important: Methods and properties of objects in visual components can only be

used in a method called using Synchronize, for example,

Synchronize(UpdateCaption);

and UpdateCaption could look like,

procedure TSimpleThead.UpdateCaption;

begin

Form1.Caption := ‘Updated in a thread’;

end; }

{TSimpleThead }

procedure TSimpleThread.Execute;

var

i, total, avg: integer;

begin

While true do begin

total:=0;

For i:=1 to 10 do

inc(total, random(Maxint));

avg:=avg Div 10;

inc(Count);

End;

{ Place thread code here }

End.

Заключение

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

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

Список использованной литературы

1. Архангельский А.Я. Программирование в Delphi 6 — М.: ЗАО «Издательство БИНОМ», 2002 г. – 1200 с.

2. Программирование в среде Delphi: Лабораторный практикум для студентов всех специальностей. Под общей редакцией Синицына А.К. – Мн.; БГУИР, 1998. – 94 с.

Леонид Федотов
Леонид Федотов
Окончил НИУ ВШЭ факультет компьютерных наук. Сам являюсь кандидатом наук. По специальности работаю 13 лет, за это время создал 8 научных статей и 2 диссертации. В компании подрабатываю в свободное от работы время уже более 5 лет. Нравится помогать школьникам и студентам в решении контрольных работ и написании курсовых проектов. Люблю свою профессию за то, что это направление с каждым годом становится все более востребованным и актуальным.
Поделиться курсовой работой:
Поделиться в telegram
Поделиться в whatsapp
Поделиться в skype
Поделиться в vk
Поделиться в odnoklassniki
Поделиться в facebook
Поделиться в twitter
Похожие статьи
Раздаточный материал для дипломной работы образец

Когда студент выходит на защиту перед экзаменационной комиссией, ему требуется подготовить все необходимые материалы, которые могут повысить шансы на получение высокого балла. Один из таких

Читать полностью ➜
Задание на дипломную работу образец заполнения

Дипломная — это своеобразная заключительная работа, которая демонстрирует все приобретенные студентом знания во время обучения в определенном вузе. В зависимости от специализации к исследовательским работам

Читать полностью ➜