Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лаб4_вер2.doc
Скачиваний:
0
Добавлен:
10.08.2019
Размер:
124.93 Кб
Скачать

Лабораторная работа №4

Задание

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

Цель

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

Описание

Многопоточное приложение

Операционные системы семейства Win32 являются многозадачными системами, то есть, в них одновременно могут выполняться несколько различных задач – процессов. Дополнительно каждый процесс может состоять из нескольких потоков (threads), также выполняющихся одновременно. Одновременность в данном случае подразумевается видимая пользователем; на самом же деле в единый момент времени активен только один поток одного процесса; ОС через определенный интервал времени передает управление другому потоку (возможно, другого процесса).

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

Доступ к общим ресурсам

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

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

Простейшим объектом синхронизации в Win32 является критическая секция (critical section). Она действует следующим образом: в программе выделяются некоторые участки кода (критические секции), которые могут выполняться одновременно только одним потоком. Эти участки обрамляются вызовами функций EnterCriticalSection в начале и LeaveCriticalSection в конце с указанием используемого объекта критической секции (структура CRITICAL_SECTION). Поток, первым зашедший в критическую секцию, захватывает её. Соответственно, если другой поток дойдет до входа в критическую секцию, то его выполнение будет автоматически приостановлено до тех пор, пока секция не будет освобождена первым потоком. Перед использованием критическая секция должна быть проинициализирована функцией InitializeCriticalSection.

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

Критическая секция имеет важное ограничение – она может применяться только для синхронизации потоков одного процесса. Данное ограничение можно обойти, используя мутекс (mutex). Этот объект синхронизации аналогичен критической секции, за исключением того, что он может быть использован для синхронизации потоков различных процессов. Для этого используются мутексы с именами. Для создания мутекса используется функция CreateMutex, для открытия уже существующего мутекса по имени – OpenMutex. Для обозначения места начала действия мутекса используется функция WaitForSingleObject, места окончания действия – ReleaseMutex.

Еще одним объектом синхронизации является семафор (semaphore). Семафор представляет собой мутекс со счетчиком потоков-владельцев. При создании семафора задается максимальное количество потоков, одновременно владеющих семафором. При заходе нового потока в блок семафора счетчик семафора уменьшается на единицу, соответственно, при выходе потока из семафора счетчик увеличивается на единицу. Если при попытке входа потока в семафор его счетчик равен нулю, выполнение этого потока приостанавливается. Для создания семафора используется функция CreateSemaphore, для открытия уже существующего по имени – OpenSemaphore. Для обозначения места начала действия семафора используется функция WaitForSingleObject, места окончания действия – ReleaseSemaphore.

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

Описание приложения

Приложение должно по команде пользователя (например, по нажатию кнопки в окне) запустить n потоков (threads), где n может задаваться пользователем (но не меньше 3). При этом каждый поток должен обращаться к общему ресурсу – строке (массиву символов). Обращения эти должны иметь следующий вид – поток добавляет в конец строки 1 символ (причем каждый поток – свой уникальный символ). Если длина строки достигла заданного значения  (или превысила его), содержимое строки отображается в области вывода окна приложения и строка обнуляется. Каждое такое обращение должно выполняться с синхронизацией доступа к строке. Далее поток, ничего не делая, ожидает время, необходимое операционной системе для переключения потока (достаточно ожидать 1 миллисекунду). Сформировав таким образом заданное количество строк, поток завершается.

 

Пользователь должен иметь возможность выбирать способ синхронизации доступа к строке. Должны быть доступны следующие способы:

       Отсутствие синхронизации;

       Синхронизация критической секцией или мутексом;

       Синхронизация семафором.

Для случая синхронизации семафором пользователь должен иметь возможность задать максимальное значение счетчика семафора.

 

Описания функций и структур

InitializeCriticalSection

Функция InitializeCriticalSection инициализирует объект критической секции.

Void InitializeCriticalSection(

  LPCRITICAL_SECTION lpCriticalSection

);

Параметры

lpCriticalSection

Указатель на объект критической секции.

Возвращаемые значения

Функция не возвращает значений.

В случае недостатка памяти InitializeCriticalSection может вызвать исключение STATUS_NO_MEMORY.

Примечания

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

Процесс отвечает за выделение памяти, используемой объектом критической секции. Он может сделать это, объявив переменную типаCRITICAL_SECTION. Перед использованием критической секции один из потоков процесса должен вызвать функцию InitializeCriticalSectionили InitializeCriticalSectionAndSpinCount для инициализации объекта.

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

Объект критической секции не может быть перемещен или скопирован. Процесс не должен также модифицировать объект, а рассматривать его как логически прозрачный. Используйте только функции критической секции, предоставленные Win32 API, для управления объектами критической секции.

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Требует версию 1.0 или более позднюю версию.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.

 EnterCriticalSection

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

Void EnterCriticalSection(

  LPCRITICAL_SECTION lpCriticalSection 

);

Параметры

lpCriticalSection

Указатель на объект критической секции.

Возвращаемые значения

Функция не возвращает значений.

Примечания

После того, как поток стал владельцем критической секции, он может выполнить дополнительные вызовы EnterCriticalSection, не блокируя своего выполнения. Это защищает поток от блокирования самого себя ожиданием критической секции, которой он уже владеет.

Если поток завершается, владея критической секцией, состояние критической секции неопределенно.

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Требует версию 1.0 или более позднюю версию.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.

LeaveCriticalSection

Функция LeaveCriticalSection переводит объект критической секции в свободное состояние.

Void LeaveCriticalSection(

  LPCRITICAL_SECTION lpCriticalSection                                            );

Параметры

lpCriticalSection

Указатель на объект критической секции.

Возвращаемые значения

Эта функция не возвращает значений.

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Требует версию 1.0 или более позднюю версию.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.

CreateMutex

Функция CreateMutex создает или открывает именованный или неименованный мутекс.

HANDLE CreateMutex(

  LPSECURITY_ATTRIBUTES lpMutexAttributes,

  BOOL bInitialOwner,

  LPCTSTR lpName

);

Параметры

lpMutexAttributes

Указатель на структуру SECURITY_ATTRIBUTES, определяющую, может ли полученный хендл наследоваться порожденными процессами. Если lpMutexAttributes равен NULL, хендл не может быть унаследован.

Windows NT/2000: Элемент lpSecurityDescriptor данной структуры задает дескриптор безопасности для нового мутекса. ЕслиlpMutexAttributes равен NULL, мутекс получает дескриптор безопасности по умолчанию.

bInitialOwner

Задает начального владельца мутекса. Если этот параметр равен TRUE и вызывающий поток создал мутекс, этот поток становится владельцем мутекса. В противном случае, вызывающий поток не становится владельцем мутекса. Чтобы определить, создал ли поток мутекс или нет, необходимо проверять возвращаемое значение функции.

lpName

Указатель на null-terminated-строку, задающую имя мутекса. Длина имени ограничена MAX_PATH символами. При сравнении имен учитывается регистр букв.

Если lpName соответствует имени существующего мутекса, эта функция запрашивает доступ MUTEX_ALL_ACCESS к существующему объекту. В этом случае параметр bInitialOwner игнорируется, поскольку он уже установлен создавшим мутекс процессом. Если параметрlpMutexAttributes не равен NULL, он определяет, может ли хендл наследоваться, но элемент структуры, определяющий дескриптор безопасности, игнорируется.

Если lpName равен NULL, мутекс создается без имени.

Если lpName соответствует имени существующего события, семафора и т.д., функция завершается с ошибкой и GetLastError возвращаетERROR_INVALID_HANDLE. Это происходит из-за того, что эти объекты разделяют общее пространство имен.

Windows NT 4.0 и более ранние версии, Windows 95/98: Имя может содержать любые символы, кроме обратного слеша (‘\’).

Возвращаемые значения

Если функция выполнилась успешно, возвращается хендл мутекса. Если именованный мутекс уже существовал, функция возвращает его хендл иGetLastError возвращает ERROR_ALREADY_EXISTS. В противном случае, создается новый мутекс.

Если функция выполнилась неуспешно, то возвращается NULL. Для получения более подробной информации об ошибке используйтеGetLastError.

Примечания

Хендл, возвращаемый CreateMutex, имеет доступ MUTEX_ALL_ACCESS к новому мутексу и может быть использован в любой функции, которая требует хендл мутекса.

Каждый поток вызвавшего процесса может задавать хендл мутекса при вызове функций ожидания (например, WaitForSingleObject). Функции ожидания отдельного объекта завершаются, когда состояние заданного объекта установлено в « signaled ». Для функций ожидания нескольких объектов можно указать, ожидать установления в «signaled» одного из нескольких объектов либо всех объектов. Когда функция ожидания завершается, ожидающий поток продолжает свое выполнение.

Состояние мутекса «signaled», когда им не владеет ни один поток. Создавший мутекс поток может использовать флаг bInitialOwner для запроса немедленного захвата мутекса. В противном случае поток должен использовать одну из функций ожидания для захвата мутекса. Когда состояние мутекса «signaled», один ожидающий поток захватывает его, состояние мутекса изменяется на «nonsignaled», и функция ожидания завершается. Только один поток может владеть мутексом в заданный момент. Поток-владелец использует функцию ReleaseMutex для освобождения мутекса.

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

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

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

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Требует версию 1.0 или более позднюю версию.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.   Unicode: Реализовано в Unicode- и ANSI-версиях в Windows NT/2000.

WaitForSingleObject

Функция WaitForSingleObject завершается, когда выполняется одно из событий:

  • Заданный объект находится в состоянии «signaled».

  • Истекает таймаут.

DWORD WaitForSingleObject(

  HANDLE hHandle,        // хендл объекта, который надо ждать

  DWORD dwMilliseconds   // таймаут в миллисекундах

);

Параметры

hHandle

Хендл объекта.

dwMilliseconds

Задает длительность таймаута в миллисекундах. Функция завершается по истечении таймаута, даже если объект находится в состоянии «nonsignaled». Если  dwMilliseconds равен 0, функция проверяет состояние объекта и немедленно завершается. Если dwMilliseconds равенINFINITE, функция работает без таймаута.

Возвращаемые значения

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

Значение

Смысл

WAIT_ABANDONED

Заданный объект – мутекс, не освобожденный последним владельцем перед его завершением. Владение мутексом переходит вызвавшему потоку, и мутекс устанавливается в состояние «nonsignaled».

WAIT_OBJECT_0

Состояние заданного объекта «signaled».

WAIT_TIMEOUT

Истек таймаут ожидания, а состояние объекта все еще «nonsignaled».

Если функция выполнилась успешно, возвращается WAIT_FAILED. Для получения более подробной информации об ошибке используйтеGetLastError.

Примечания

Функция WaitForSingleObject проверяет текущее состояние заданного объекта. Если состояние объекта «nonsignaled», вызывающий поток входит в состояние ожидания. Поток потребляет очень мало процессорного времени, пока ждет изменения состояния объекта на «signaled» или окончания таймаута.

Перед завершением функция ожидания модифицирует состояние некоторых типов объектов синхронизации. Изменение происходит только для объектов, чье состояние «signaled» стало причиной завершения  функции. Например, счетчик семафора уменьшается на единицу.

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Требует версию 1.0 или более позднюю версию.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.

ReleaseMutex

Функция ReleaseMutex освобождает заданный мутекс.

BOOL ReleaseMutex(

  HANDLE hMutex   // хендл мутекса

);

Параметры

hMutex

Хендл мутекса.

Возвращаемые значения

Если функция выполнилась успешно, возвращается ненулевое значение.

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

Примечания

Функция ReleaseMutex завершается с ошибкой, если вызывающий поток не является владельцем мутекса.

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Требует версию 1.0 или более позднюю версию.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.

CreateSemaphore

Функция CreateSemaphore создает или открывает именованный или неименованный семафор.

HANDLE CreateSemaphore(

  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

                       // указатель на атрибуты безопасности

  LONG lInitialCount// начальное значение счетчика

  LONG lMaximumCount// максимальное значение счетчика

  LPCTSTR lpName       // указатель на имя семафора

);

Параметры

lpSemaphoreAttributes

Указатель на структуру SECURITY_ATTRIBUTES, определяющую, может ли полученный хендл наследоваться порожденными процессами. Если lpSemaphoreAttributes равен NULL, хендл не может быть унаследован.

 

Windows NT/2000: Элемент lpSecurityDescriptor данной структуры задает дескриптор безопасности для нового семафора. ЕслиlpSemaphoreAttributes равен NULL, семафор получает дескриптор безопасности по умолчанию.

Count

Задает начальное значение счетчика семафора. Это значение должно быть больше или равно нулю и меньше или равно lMaximumCount. Состояние семафора «signaled», когда его счетчик больше нуля, и «nonsignaled», когда он равен нулю. Счетчик уменьшается на единицу, когда функция ожидания освобождает поток, ожидавший семафора. Счетчик увеличивается на заданное значение при вызове функцииReleaseSemaphore.

lMaximumCount

Задает максимальное значение счетчика семафора. Должно быть больше нуля.

lpName

Указатель на null-terminated-строку, задающую имя семафора. Длина имени ограничена MAX_PATH символами. При сравнении имен учитывается регистр букв.

Если lpName соответствует имени существующего семафора, эта функция запрашивает доступ SEMAPHORE_ALL_ACCESS к существующему объекту. В этом случае, параметры lInitialCount и lMaximumCount игнорируются, поскольку они уже установлены создавшим мутекс процессом. Если параметр lpSemaphoreAttributes не равен NULL, он определяет, может ли хендл наследоваться, но элемент структуры, определяющий дескриптор безопасности, игнорируется.

Если lpName равен NULL, семафор создается без имени.

Если lpName соответствует имени существующего события, мутекса и т.д., функция завершается с ошибкой и GetLastError возвращаетERROR_INVALID_HANDLE. Это происходит из-за того, что эти объекты разделяют общее пространство имен.

Windows NT 4.0 и более ранние версии, Windows 95/98: Имя может содержать любые символы, кроме обратного слеша (‘\’).

Возвращаемые значения

Если функция выполнилась успешно, возвращается хендл семафора. Если именованный семафор уже существовал, функция возвращает его хендл и GetLastError возвращает ERROR_ALREADY_EXISTS. В противном случае, создается новый семафор.

Если функция выполнилась неуспешно, то возвращается NULL. Для получения более подробной информации об ошибке используйтеGetLastError.

Примечания

Хендл, возвращаемый CreateSemaphore, имеет доступ SEMAPHORE_ALL_ACCESS к новому семафору и может быть использован в любой функции, которая требует хендл семафора.

Каждый поток вызвавшего процесса может задавать хендл семафора при вызове функций ожидания (например, WaitForSingleObject). Функции ожидания отдельного объекта завершаются, когда состояние заданного объекта установлено в «signaled». Для функций ожидания нескольких объектов можно указать, ожидать установления в «signaled» одного из нескольких объектов либо всех объектов. Когда функция ожидания завершается, ожидающий поток продолжает свое выполнение.

Состояние семафора «signaled», когда его счетчик больше нуля, и «nonsignaled», когда равен нулю. Параметр lInitialCount задает начальное значение счетчика. Поток-владелец использует функцию ReleaseMutex для освобождения мутекса. Используйте функцию ReleaseSemaphoreдля увеличения счетчика семафора на заданную величину. Счетчик никогда не может быть меньше нуля и больше lMaximumCount.

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

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Не поддерживается.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.   Unicode: Реализовано в Unicode- и ANSI-версиях в Windows NT/2000.

ReleaseSemaphore

Функция ReleaseSemaphore увеличивает счетчик заданного семафора на нужную величину.

BOOL ReleaseSemaphore(

  HANDLE hSemaphore,       // хендл семафора

  LONG lReleaseCount,      // величина, на которую надо увеличить счетчик

  LPLONG lpPreviousCount   // адрес для предыдущего значения счетчика

);

Параметры

hSemaphore

Хендл семафора.

lReleaseCount

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

lpPreviousCount

Указатель на 32-битную переменную для получения предыдущего значения счетчика семафора. Этот параметр может быть равен NULL, если предыдущее значение не требуется.

Возвращаемые значения

Если функция выполнилась успешно, возвращается ненулевое значение.

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

Примечания

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

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Не поддерживается.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.

 

BeginThread

Функция BeginThread создает поток для выполнения в виртуальном адресном пространстве вызывающего процесса.

HANDLE BeginThread(

  LPSECURITY_ATTRIBUTES lpThreadAttributes// указатель на атрибуты защиты

  DWORD dwStackSize,                         // начальный размер стека потока

  LPTHREAD_START_ROUTINE lpStartAddress,     // указатель на функцию потока

  LPVOID lpParameter,                        // аргумент для нового потока

  DWORD dwCreationFlags,                     // флаги создания

  LPDWORD lpThreadId                        // указатель для получения ID потока

);

Параметры

lpThreadAttributes

Указатель на структуру SECURITY_ATTRIBUTES,  определяющую, может ли возвращаемый хендл наследоваться дочерними процессами. Если lpThreadAttributes равен NULL, хендл не может быть унаследован.

Windows NT/2000: Элемент lpSecurityDescriptor данной структуры задает дескриптор безопасности для нового потока. ЕслиlpThreadAttributes равен NULL, поток получает дескриптор безопасности по умолчанию.

dwStackSize

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

lpStartAddress

Указатель на функцию приложения типа LPTHREAD_START_ROUTINE, которую должен выполнить поток; представляет собой начальный адрес выполнения потока.

lpParameter

Задает значение 32-битного параметра, передаваемое в поток.

dwCreationFlags

Задает дополнительные флаги, управляющие созданием потока. Если задан флаг CREATE_SUSPENDED, поток создается в приостановленном состоянии и не будет выполняться до вызова функции ResumeThread. Если этот параметр равен нулю, поток начинает выполняться сразу после создания. На данный момент, никакие другие значения не поддерживаются.

lpThreadId

Указатель на 32-битную переменную, получающую идентификатор потока.

Windows NT/2000: Если этот параметр равен NULL, идентификатор потока не возвращается.

Windows 95/98: Этот параметр не может быть равным NULL.

Возвращаемые значения

Если функция выполнилась успешно, возвращается хендл нового потока.

Если функция выполнилась неуспешно, возвращается NULL. Для получения более подробной информации об ошибке используйтеGetLastError.

Заметьте, что BeginThread может выполниться успешно, даже если lpStartAddress указывает на данные, код, либо вообще на недоступную область. Если стартовый адрес неверен на момент, когда запускается поток, происходит исключение и поток завершается. Завершение потока из-за неверного стартового адреса обрабатывается как выход по ошибке.

Требования

  Windows NT/2000/XP: Требует Windows NT 3.1 или более позднюю версию.   Windows 95/98/ME: Требует Windows 95 или более позднюю версию.   Windows CE: Требует версию 1.01 или более позднюю версию.   Header: Объявлено в winbase.h.   Import Library: kernel32.lib.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]