Увеличивает ли IUnknown::QueryInterface() счетчик ссылок?
Если у меня есть IUnknown *ptr
мне нужно позвонить Release()
на каждом интерфейсе, который я получаю через ptr->QueryInterface()
кроме звонка ptr->Release()
когда я закончу с ptr
?
Раньше я думал, что ответ "Да", но эта цитата из MSDN смутила меня:
Иногда вам может понадобиться получить слабую ссылку на объект (то есть вы можете получить указатель на один из его интерфейсов без увеличения счетчика ссылок), но это недопустимо сделать, вызвав
QueryInterface
с последующимRelease
,
Я не понимаю, почему это проблематично - если я позвоню ptr->QueryInterface()
а затем позвоните Release
на полученном указателе, разве счетчик ссылок на объект не должен быть положительным? Как это приводит к неверному указателю?
4 ответа
Документация верна. И вы должны следовать правилам подсчета ссылок - это включает в себя вызов Release
на интерфейсах, полученных из QueryInterface
в дополнение к после того, как вы создали объект.
Чтобы понять, почему вы не можете делать слабые указатели с Release
- в вызове существует условие гонки QueryInterface
а потом Release
незамедлительно после.
- Thread1 создает объект - счетчик ссылок 1
- Thread2 звонки
QueryInterface
для слабой ссылки - счетчик ссылок 2 - Thread1 освобождает объект - счетчик ссылок 1
- Thread2 звонки
Release
для слабой ссылки - количество ссылок 0. Объект уничтожен. - Thread2 пытается использовать объект - ошибка.
Предупреждение должно предостеречь от вышесказанного - вероятно, некоторые программисты думают, что они могут ptr->QueryInterface()
а затем позвоните Release
на результирующий указатель ", а затем использовать объект...
IUnknown:: Метод QueryInterface
Получает указатели на поддерживаемые интерфейсы на объекте.
Этот метод вызывает IUnknown::AddRef для указателя, который он возвращает.
Прямо из ссылки IUnknown::QueryInterface по http://msdn.microsoft.com/en-us/library/ms682521%28v=vs.85%29.aspx
Актерство - не единственный сценарий; Я бы даже сказал, что многопоточность на самом деле не является основным сценарием: эти COM-правила относятся к Win16 до того, как упреждающая многопоточность была добавлена в Windows.
Ключевой вопрос заключается в том, что для COM, счетчики ссылок являются дляинтерфейса, а не дляобъекта. Реализация COM свободна на самом деле реализовать подсчет ссылок, реализуя его для каждого объекта - это, пожалуй, самый простой способ сделать это в C++, особенно когда объект COM отображается на один объект C++ - но это не более, чем детали реализации, и код COM-клиента не может полагаться на это.
Есть много COM-объектов, которые могут генерировать интерфейсы на лету по мере необходимости, а затем уничтожать их, как только они больше не нужны. В этих случаях, если вы вызываете QI, чтобы получить один из этих интерфейсов, после вызова Release, память для этого интерфейса может быть освобождена, поэтому ее использование может привести к ошибке / аварии / и т.д.
Вообще говоря, вы должны рассматривать любой вызов ->Release() как потенциально освобождающий память за указателем.
(Кроме того, обратите внимание, что в COM на самом деле нет концепции слабых ссылок: есть счетные (сильные) ссылки, и все.)
Предложение избегать слабых ссылок не решает проблему гонки.
T1 operator new, create object, references: 1
T1 passes interface object reference to T2, thinking it can "share" ownership
T1 suspends
T2 resumes
T2 QueryInterface
T2 suspends before InterlockedIncrement, references: 1
T1 resumes
T1 Calls Release
T1 suspends between InterlockedDecrement and operator delete, references: 0
T2 resumes, InterlockedIncrement occurs, references 1
T2 suspends
T1 resumes, operator delete executes, references 1 !!!
T1 suspends
T2 resumes
T2 Any reference to the interface is now invalid since it has been deleted with reference count 1.
Это разрешимо на COM-сервере. Однако COM-клиент не должен зависеть от сервера, предотвращающего это состояние гонки. Поэтому COM-клиенты НЕ ДОЛЖНЫ совместно использовать объекты интерфейса между потоками. Единственный поток, которому следует разрешить доступ к объекту интерфейса, - это ОДИН поток, который в настоящее время "владеет" объектом интерфейса.
T1 НЕ должен был вызывать Release. Да, он мог вызвать AddRef до передачи объекта интерфейса в T2. Но это может не решить гонку, только перенести ее в другое место. Лучше всего всегда поддерживать концепцию "один интерфейсный объект, один владелец".
Если COM-сервер желает поддерживать концепцию, согласно которой два (или более) интерфейса могут ссылаться на некоторое общее внутреннее состояние общего сервера, COM-сервер должен объявить о контракте, предоставив метод CreateCopyOfInstance, и внутренне управлять конфликтами. Конечно, есть примеры серверов, которые справляются с таким "разветвлением". Посмотрите на интерфейсы постоянного хранения от Microsoft. Там интерфейсы НЕ "разветвляются". Каждый интерфейс должен принадлежать одному пользователю (поток / процесс / что угодно), а "разветвление" управляется внутренне сервером с помощью методов, предоставляемых COM-клиенты, чтобы контролировать некоторые аспекты конфликтных вопросов. Поэтому COM-серверы Microsoft должны учитывать условия гонки как часть своих контрактов со своими клиентами.