RCW и подсчет ссылок при использовании COM-взаимодействия в С#
У меня есть приложение, которое использует сборки Interop для Office. Я знаю о "Runtime Callable Wrapper (RCW)", управляемом средой выполнения. Но я не очень уверен, как счетчик ссылок увеличивается. MSDN говорит,
RCW содержит только одну ссылку на обернутый COM-объект, независимо от количество управляемых клиентов, вызывающих его.
Если я правильно понял, в следующем примере
using Microsoft.Office.Interop.Word;
static void Foo(Application wrd)
{
/* .... */
}
static void Main(string[] args)
{
var wrd = new Application();
Foo(wrd);
/* .... */
}
Я передаю экземпляр wrd
другому методу. Но это не увеличивает счетчик внутренних ссылок. Поэтому мне интересно, какие сценарии счетчика увеличивается? Может ли кто-нибудь указать сценарий, в котором счетчик ссылок увеличивается?
Также я прочитал какой-то блог, в котором говорится, что избегать использования двойных точек при программировании с COM-объектами. Что-то вроде, wrd.ActiveDocument.ActiveWindow
. Автор утверждает, что компилятор создает отдельные переменные для хранения значений, которые будут увеличивать счетчик ссылок. ИМХО, это неправильно, и первый пример доказывает это. Это правильно?
Любая помощь будет замечательной!
Ответы
Ответ 1
Я тоже изучал этот вопрос, работая над COM/.Net-Interop-ориентированным приложением, борясь с утечками, зависаниями и сбоями.
Короткий ответ: каждый раз, когда COM-объект передается из среды COM в .NET.
Длинный ответ:
- Для каждого COM-объекта есть один объект RCW [Test 1] [Ref 4]
- Количество ссылок увеличивается каждый раз, когда объект запрашивается из объекта COM (вызов свойства или метода объекта COM, возвращающего COM-объект, возвращаемый номер ссылки на объект COM будет увеличен на единицу) [Test 1]
- Количество ссылок не увеличивается с помощью литья на другие COM-интерфейсы объекта или перемещения ссылки RCW вокруг [Test 2]
- Количество ссылок увеличивается каждый раз, когда объект передается как параметр в событии, поднятом COM [Ref 1]
На стороне примечания: вы должны ВСЕГДА освобождать COM-объекты, как только вы закончите использовать их. Оставляя эту работу в GC, можно привести к утечкам, неожиданному поведению и взаимоблокировкам событий. Это в десять раз более важно, если вы обращаетесь к объекту не в потоке STA, на котором он был создан. [Ref 2] [Ref 3] [Болезненный личный опыт]
Я надеюсь, что я рассмотрел все случаи, но COM - жесткий файл cookie.
Приветствия.
Тест 1 - счетчик ссылок
private void Test1( _Application outlookApp )
{
var explorer1 = outlookApp.ActiveExplorer();
var count1 = Marshal.ReleaseComObject(explorer1);
MessageBox.Show("Count 1:" + count1);
var explorer2 = outlookApp.ActiveExplorer();
var explorer3 = outlookApp.ActiveExplorer();
var explorer4 = outlookApp.ActiveExplorer();
var equals = explorer2 == explorer3 && ReferenceEquals(explorer2, explorer4);
var count2 = Marshal.ReleaseComObject(explorer4);
MessageBox.Show("Count 2:" + count2 + ", Equals: " + equals);
}
Output:
Count 1: 4
Count 2: 6, Equals: True
Тест 2 - счетчик ссылок
private static void Test2(_Application outlookApp)
{
var explorer1 = outlookApp.ActiveExplorer();
var count1 = Marshal.ReleaseComObject(explorer1);
MessageBox.Show("Count 1:" + count1);
var explorer2 = outlookApp.ActiveExplorer();
var explorer3 = explorer2 as _Explorer;
var explorer4 = (ExplorerEvents_10_Event)explorer2;
var explorerObject = (object)explorer2;
var explorer5 = (Explorer)explorerObject;
var equals = explorer2 == explorer3 && ReferenceEquals(explorer2, explorer5);
var count2 = Marshal.ReleaseComObject(explorer4);
MessageBox.Show("Count 2:" + count2 + ", Equals: " + equals);
}
Output:
Count 1: 4
Count 2: 4, Equals: True
Источники Я ретранслирую в дополнение к моему опыту и тестированию:
1. Johannes Passing - RCW Reference Counting Rules!= Правила подсчета ссылок COM
2. Эран Сандлер - Внутренние и нарушающие обходные файлы, используемые для работы в режиме реального времени
3. Eran Sandler - Marshal.ReleaseComObject и CPU Spinning
4. MSDN - обходная оболочка времени выполнения
Ответ 2
Я не видел кода для RCW - даже не уверен, что это часть SSCLI, но мне пришлось внедрить подобную систему для отслеживания жизни объекта COM в SlimDX и пришлось провести справедливое исследование RCW. Это то, что я помню, надеюсь, это достаточно точно, но возьмите его с прикосновением соли.
Когда система сначала видит указатель интерфейса COM, она просто переходит в кеш, чтобы увидеть, есть ли RCW для этого указателя интерфейса. Предположительно, кеш будет использовать слабые ссылки, чтобы не допустить финализацию и сбор RCW.
Если для этого указателя есть живая оболочка, система возвращает оболочку - если интерфейс был получен таким образом, чтобы увеличить счет ссылки на интерфейс, предположительно, система RCW будет вызывать Release() в этой точке. Он нашел живую обертку, поэтому он знает, что обертка является единственной ссылкой, и она хочет поддерживать ровно одну ссылку. Если в кеше нет живой обертки, она создает новую и возвращает ее.
Оболочка вызывает Release на указателя (ов) интерфейса COM-интерфейса из финализатора.
Оболочка находится между вами и COM-объектом и обрабатывает все маршалинг параметров. Это также позволяет получить исходный результат любого метода интерфейса, который сам по себе является другим указателем интерфейса, и запустить этот указатель через систему кэширования RCW, чтобы убедиться, что он существует до того, как вы вернете указатель интерфейса.
К сожалению, у меня нет хорошего понимания того, как RCW-система обрабатывает создание прокси-объекта для отправки материала через домены приложений или поточные квартиры; это не был аспект системы, которую мне нужно было скопировать для SlimDX.
Ответ 3
Вам не нужно никакого специального лечения. В среде выполнения сохраняется только одна ссылка на объект COM. Причина этого в том, что GC отслеживает все управляемые ссылки, поэтому, когда RCW выходит из области действия и собирается, ссылка COM освобождается. Когда вы передаете управляемую ссылку, GC отслеживает ее для вас - это одно из самых больших преимуществ работы на основе GC на старой схеме AddRef/Release.
Вам не нужно вручную вызывать Marshal.ReleaseComObject, если вы не хотите более детерминированного выпуска.
Ответ 4
принятое решение действительно, но здесь есть дополнительная справочная информация.
RCW содержит один или несколько внутренних интерфейсов интерфейса COM-объекта внутри своего COM-объекта.
Когда RCW выпускает свой базовый COM-объект, либо из-за получения сбора мусора, либо из-за его вызова Marshal.ReleaseComObject()
, он освобождает все свои внутренние интерфейсы COM-объектов.
Здесь на самом деле много ссылок на количество ссылок - одно определение, когда .NET RCW должен выпустить свои базовые интерфейсы COM-объекта, а затем каждый из этих необработанных COM-интерфейсов имеет свой собственный счетчик ссылок, как в обычном COM.
Здесь код для получения исходного COM IUnknown
счетчика ссылок на интерфейс:
int getIUnknownReferenceCount(object comobject)
{
var iUnknown = Marshal.GetIUnknownForObject(comObject);
return Marshal.Release(iUnknown);
}
И вы можете получить то же самое для других COM-интерфейсов объекта, используя Marshal.GetComInterfaceForObject()
.
В дополнение к способам, перечисленным в принятом решении, мы также можем искусственно увеличить число ссылок .NET RCW, вызвав что-то вроде Marshal.GetObjectForIUnknown()
.
Здесь примерный код, использующий эту технику, чтобы получить заданный номер ссылки RCW объекта COM:
int comObjectReferenceCount(object comObject)
{
var iUnknown = Marshal.GetIUnknownForObject(comObject);
Marshal.GetObjectForIUnknown(iUnknown);
Marshal.Release(iUnknown);
return Marshal.ReleaseComObject(comObject);
}
Ответ 5
Вам нужно вызвать Marshal.ReleaseComObject
на вашу wrd-переменную, чтобы освободить ссылку на слово-приложение.
Таким образом, если Word не отображается и вы закрываете приложение, exe также выгружается, если вы не сделали его видимым для пользователя.