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

0

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

Я сохраняю информацию обо всех подключенных пользователях в дважды связанном списке, который разделяется между потоками. Я *PPER_user g_usersList списка в глобальной переменной: *PPER_user g_usersList, а структура для пользователей выглядит так:

typedef struct _user {
  char      id;
  char      status;
  struct _user  *pCtxtBack; 
  struct _user  *pCtxtForward;
} user, *PPER_user;

Когда новый пользователь подключается к серверу, данные о подключенных пользователях собираются из связанного списка и отправляются ему:

 WSABUF wsabuf; PPER_player pTemp1, pTemp2; unsigned int c=0;
 .....
 EnterCriticalSection(&g_CSuserslist); 
 pTemp1 = g_usersList; 
 while( pTemp1 ) {
   pTemp2 = pTemp1->pCtxtBack;
   wsabuf.buf[c++]=pTemp1->id;     // fill buffer with data about all users
   wsabuf.buf[c++]=pTemp1->status; //
   pTemp1 = pTemp2;
   };
 WSASend(...,wsabuf,...);
 LeaveCriticalSection(&g_CSuserslist);

Но кое-что о коде выше меня смущает:

  1. связанный список довольно сильно используется другими потоками. Более подключенные пользователи (например, 100,1000), более длинный период списка времени заблокирован на всю продолжительность данных gaatering. Должен ли я смириться с этим или найти лучший способ сделать это?

  2. кажется, что когда один поток блокирует список, в то время как цикл прошел через всю цепочку (пользователей), собирающую все id, статус, другие потоки должны использовать тот же CriticalSection (& g_CSuserslist), когда пользователи хотят изменить свой собственный идентификатор, статус и т.д. Но это будет вероятно, убьет производительность. Может быть, мне нужно изменить дизайн моего приложения или что-то еще?

Любое понимание, которое вы, возможно, цените. Заранее спасибо.

Теги:
multithreading
iocp
critical-section

4 ответа

3
Лучший ответ

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

Поэтому вы должны защищать:

  • добавление нового пользователя
  • удаление пользователя в deconnexion
  • получение моментального снимка списка для дальнейшей обработки

Все эти операции являются только памятью, поэтому, если вы не переходите в очень тяжелые условия, все должно быть хорошо, если вы ставите все IO за пределы критических секций (1) потому что это происходит только при подключении/отключении пользователей. Если вы ставите WSASend за пределы критической секции, все должно идти хорошо, а ИМХО - достаточно.

Изменить за комментарий:

Ваш user структуры достаточно мал, я бы сказал, что от 10 до 18 полезных байтов (в зависимости от размера указателя 4 или 8 байтов) и в общей сложности 12 из 24 байтов, включая дополнение. При использовании 1000 подключенных пользователей вам нужно копировать менее 24 тыс. Байт памяти и иметь возможность проверять, является ли следующий user нулевым (или, самое большее, поддерживать текущее количество подключенных пользователей, чтобы иметь более простой цикл). Во всяком случае, сохранение такого буфера также должно выполняться в критическом разделе. IMHO, пока у вас не будет более 1000 пользователей (от 10 000 до 100 тыс., Но вы можете получить другие проблемы...) достаточно простой глобальной блокировки (например, вашего критического раздела) вокруг всего двойного связанного списка user. Но все, что нужно исследовать, потому что это может зависеть от внешних вещей, таких как аппаратное обеспечение...


Слишком долго не читайте обсуждения:

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

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


(1) в коде, который вы показываете, WSASend(...,wsabuf,...); находится внутри критического участка, когда он должен быть снаружи. Вместо этого напишите:

...
LeaveCriticalSection(&g_CSuserslist);
WSASend(...,wsabuf,...);
  • 0
    The rule is avoid any time consuming operation** while** in critical section Поэтому я должен поддерживать некоторый тип буфера с идентификатором, статусом всех пользователей и копировать этот буфер (например, с помощью memcpy среди критической секции), когда новый пользователь подключается к сервер?
  • 0
    @maciekm: Сообщение отредактировано за ваш комментарий.
Показать ещё 1 комментарий
2

Первой проблемой производительности является связанный список: для прохождения связанного списка требуется значительно больше времени, чем для перемещения массива /std::vector<>. Преимущество одного связанного списка состоит в том, чтобы разрешить потокобезопасную вставку/удаление элементов с помощью атомных типов/операций сравнения и свопинга. Сдвоенный список намного сложнее поддерживать в потокобезопасном режиме, не прибегая к мьютексам (которые всегда являются большими, тяжелыми пушками).

Итак, если вы идете с мьютексом, чтобы заблокировать список, используйте std::vector<>, но вы также можете решить свою проблему с помощью незакрепленной реализации одного связанного списка:

  • У вас есть один связанный список с одной головой, которая является глобальной, атомной переменной.

  • Все записи неизменяемы после их публикации.

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

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

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

    Не устанавливайте следующий указатель удаляемого объекта в NULL, другой поток может по-прежнему нуждаться в нем, чтобы найти остальную часть списка (по его мнению, объект еще не удалился).

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

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

1

Реальный вопрос здесь в том, насколько срочно отправлять каждый байт этого списка новому пользователю?

Насколько хорошо клиентская сторона отслеживает данные этого списка?

Если клиент может обрабатывать частичные обновления, не имеет ли смысл "просачивать" данные каждому пользователю - возможно, используя временную метку, чтобы указать свежесть данных, и не нужно блокировать список таким массовым способом?

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

1

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

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

  • 0
    Да, когда новый пользователь входит в систему в первый раз, ему нужно загрузить идентификатор, статус (также псевдоним) каждого другого пользователя, потому что на стороне клиента составляется список активных пользователей (своего рода чат-сервер). Но в моем коде я вижу очень большие конфликты потоков. Даже если некоторый подключенный клиент хочет изменить только свой собственный идентификатор или статус (которые являются членами struct, которые связаны между собой в двусвязном списке), он не может использовать собственный CriticalSection для каждого пользователя, но CriticalSection (& g_CSuserslist), то же самое , который используется для отправки списка пользователей.
  • 0
    ... Если какой-либо клиент меняет свой идентификатор без использования той же CS, тогда другой клиент, собирающий данные из связанного списка в одно и то же время, может получить случайный идентификатор. Я до сих пор не знаю, как справиться с этим.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню