Пул расширенных ПЕРЕКРЫВАЮЩИХСЯ объектов в многопоточной среде: где и как эффективно использовать блокировку

#c #multithreading #winapi #atomic #iocp

#c #многопоточность #winapi #атомарный #iocp

Вопрос:

В C у меня есть Stream объект, который абстрагирует HANDLE в Windows, и у меня также есть различные производные объекты, такие как File , TcpSocket , UdpSocket , Pipe который происходит непосредственно от этого Stream объекта, а затем у меня также есть RequestIo объект, который является моей собственной версией расширенного OVERLAPPED объекта, то есть RequestIo напрямую наследуется от OVERLAPPED структуры. Отныне говорить RequesIo — это то же самое, что говорить OVERLAPPED .

В RequestIo объекте я храню несколько полезных вещей, которые невозможно сохранить в единой OVERLAPPED структуре, таких как флаги, указатели пользователя и так далее. Там я также сохраняю указатель на следующий RequestIo объект, чтобы иметь навязчивый связанный список этих объектов.

Затем Stream объект имеет 2 заголовка этого навязчивого связанного списка, один для RequestIo объектов для чтения, а другой для записи. Таким образом, Stream объект может иметь небольшой пул этих RequestIo объектов, и ему не нужно выделять / освобождать их при каждой операции ввода-вывода и не нуждается в блокировке, поскольку 2 навязчивых списка разделены для чтения и записи, которые представляют собой 2 вида операций, которые могут выполняться в 2 разных потоках одновременно вIOCPs.

Когда у меня будут потоковые объекты (такие как сокеты или каналы) У меня будет только 1 RequestIo для чтения (больше одного просто не нужно) и один для записи, поэтому мне в принципе не нужна блокировка, потому что сначала socket.read() RequestIo выделяется новый, вставляется в связанный список, и он будет использоваться снова и снова, пока сокет не будет закрыт и уничтоженто же самое для записей.

Но нет потокоподобных объектов (таких как файл произвольного доступа, сокеты udp), которые могут выдавать более одного RequestIo как для чтения, так и для записи. Давайте просто рассмотрим сокет UDP, который может выдавать N ожидающих RequestIo объектов для чтения дейтаграмм, или файл произвольного доступа, который может выдавать несколько RequestIo пакетов для чтения / записи в / из разных частей файла.

Здесь все усложняется. Если у меня есть этот связанный список RequestIo объектов, мне действительно нужно просмотреть этот список и посмотреть, какой RequestIo из них НЕ находится на рассмотрении, и выполнить с ним новую операцию ввода-вывода.

Сказал, что это кажется простым, но это не так: несмотря на то, что я могу установить флаг на a RequestIo с надписью «его ожидает», проблема не решена: разве этот флаг не должен быть атомарным целым числом? Поскольку этот флаг будет сброшен каким-либо другим потоком. А как насчет извлечения первого RequestIo доступного из связанного списка, когда существует несколько RequestIo экземпляров? Разве это не должно быть взаимосвязанной операцией? И вставка в этот связанный список? Например. когда я выделяю новый RequestIo пакет, потому что все остальные находятся на рассмотрении.

Возможное решение, о котором я думал, — это пройти по этому связанному списку и проверить атомарное целое число в RequestIo объекте с помощью инструкции CAS (CompareAndSwap), если 0, это означает, что оно не находится на рассмотрении, и немедленно установить его равным 1, чтобы другой поток увидел его как ожидающий и перешел к следующему RequestIo объекту. Если он не может найти какой-либо RequestIo объект, он выделяет новый, но здесь он должен заблокировать связанный список head…to вставьте новый выделенный RequestIo объект!

Итак, каков, по сути, самый быстрый и эффективный способ корректного управления пулом из N OVERLAPPED (или RequestIo в моем случае) объектов, не подвергаясь массивной блокировке, которая снизила бы производительность и назначение многопоточных IOCP?

Комментарии:

1. Я не понимаю, почему у вас вообще есть отложенные экземпляры RequestIo в списке? Вы передали их подсистеме IOCP с помощью вызовов WSABlah. Вы получите их обратно, когда операция ввода-вывода будет завершена (или не удалась:), так почему вы также сохраняете их в списке?

2. Кроме того, даже для потоковых сервисов я обычно стараюсь держать в ожидании как минимум два запроса на получение, чтобы система IOCP часто не попадала в ситуацию, когда для входящих данных недоступны пользовательские буферы. Что касается записи, часто встречаются множественные запросы ввода-вывода, поскольку, если буфер доступен для записи, вы можете отправить его сейчас, а не ждать, пока полный буферный массив, представляющий, скажем, HTTP-пакет HTML-файла / страницы, будет собран полностью перед отправкой первогофрагмент.

3. У меня они есть в связанном списке, поэтому мне не нужно каждый раз уничтожать и перераспределять их, и я могу использовать их повторно, даже когда я закрываю сокет и снова открываю его. Я выделяю их один раз и повторно использую. В основном объект Stream имеет_a N RequestIo объектов в небольшом пуле связанных списков. Таким образом, когда вам нужно выполнить какой-либо ввод-вывод, вы берете RequestIo из этого связанного списка и используете его для своей операции ввода-вывода. В противном случае, что вы делаете после завершения операции ввода RequestIo -вывода? В случае чтения вы можете выдать новое чтение с тем же объектом, но в случае записи?

4. @MarcoPagliaricci вы слишком беспокоитесь о блокировках. Критический раздел должен работать нормально, поскольку он имеет первичную блокировку вращения, а окно конкуренции очень маленькое — вы только нажимаете / нажимаете указатель.

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

Ответ №1:

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

Функции InitializeSListHead, InterlockedPushEntrySList и InterlockedPopEntrySList обеспечивают эффективную многопроцессорную реализацию связанного списка.

Комментарии:

1. Я уже думал об этом, но и здесь мне понадобится механизм блокировки для этих списков. Я имею в виду: lock(); get_first_available_RequestIo_for_writing(); remove_it_from_available_list(); unlock(); и так далее

2. Да, вам нужна блокировка, но кого это волнует? Это будет критический раздел, и вероятность фактического конфликта (и, следовательно, требующий блокировки ядра вместо блокировки CS) минимальна. Все, что вы делаете, это нажимаете / нажимаете один указатель на экземпляр RequstIo — это не займет много времени!

3. Если пул объектов requestIo заканчивается, вам нужна стратегия обработки. Либо создайте еще один запрос, тем самым увеличив размер пула, либо организуйте пул как очередь блокировки, чтобы запрашивающий поток должен был ждать освобождения экземпляров.

4. Вам не нужна блокировка. Взаимосвязанные функции push / pop являются атомарными и многопроцессорными, т. Е. Они обрабатывают блокировку за вас.

5. Гарри: о, да, с теми API, которые вы предоставили, в основном я буду использовать связанный список без блокировок. Спасибо.