Большой архив статей, книг, документации по программированию, вебдизайну, компьютерной графике, сетям, операционным системам и многому другому
 
<Добавить в Избранное>    <Сделать стартовой>    <Реклама на сайте>    <Контакты>
  Главная Документация Программы Обои   Экспорт RSS E-Books
 
 

   Программирование -> C/C++ -> Сущность технологии COM


Маршалер свободной поточной обработки (FreeThreaded Marshaler)

Если в классе установлена опция ThreadingModel="Both", то она показывает, что экземпляры класса, а также объект класса могут безопасно находиться в любых апартаментах: STA или МТА. В то же время, согласно правилам СОМ, любой данный экземпляр будет находиться только в одном апартаменте. Если бы разработчик объекта прошел все этапы проверки того, что объект может благополучно находиться в МТА, то в этом случае объекту вообще не нужно было бы заботиться об апартаментах. Одновременный доступ к подобному объекту мог бы быть не только для нескольких потоков внутри МТА, но также от потоков вне МТА (например, от потоков, выполняемых в STA). В то же время клиенты не могут знать, что такой доступ является безопасным для отдельно взятого объекта, поэтому любое совместное использование интерфейсного указателя в нескольких апартаментах должно быть установлено с использованием явной технологии маршалинга. Это означает, что доступ к внутрипроцессному объекту будет осуществляться через ORPC-вызовы, если только вызывающий объект не выполняется в том же самом апартаменте, где был создан объект.

В отличие от клиентов, объекты знают о своих отношениях с апартаментами, о своем параллелизме и реентерабельности. Объекты, удовлетворяющиеся ORPC-запросами при доступе из нескольких апартаментов одного и того же процесса, ведут себя так по умолчанию. А объект, которого не устраивает доступ ORPC, имеет возможность обойти это путем реализации специального маршалинга. Довольно просто использовать специальный маршалинг для обхода администратора заглушек и преобразования исходного указателя на объект в маршалированную объектную ссылку. При использовании этой технологии реализация специального заместителя могла бы просто считывать исходный указатель из маршалированной объектной ссылки и передавать его вызывающему объекту в импортирующем апартаменте. Клиентские потоки по-прежнему передавали бы интерфейсный указатель через границу апартамента с помощью явного или неявного вызова CoMarshalInterface / CoUnmarshalInterface. Однако объект мог бы договориться со специальным заместителем о том, чтобы просто передать исходный указатель нужному объекту. Хотя данная технология безупречно работает для внутрипроцессного маршалинга, она, к сожалению, не приводит к успеху в случае межпроцессного маршалинга. Но, к счастью, реализация объекта может просто обратиться к стандартному маршалеру за другим контекстом маршалинга, отличным от MSHCTX_INPROC.

Поскольку только что описанное поведение является полезным для большого класса объектов, в СОМ предусмотрена агрегируемая реализация IMarshal, выполняющая в точности то, что было описано. Эта реализация называется маршалером свободной поточной обработки (FreeThreaded Marshaler - FTM) и может быть осуществлена с помощью вызова API-функции CoCreateFreeThreadedMarshaler:

HRESULT CoCreateFreeThreadedMarshaler( 
          [in] IUnknown *pUnkOuter, 
          [out] IUnknown **ppUnkInner);

Класс, который желает использовать FTM, просто агрегирует экземпляр либо во время инициализации, либо по требованию при первом запросе QueryInterface об интерфейсе IMarshal. Следующий класс заранее обрабатывает FTM во время построения.

class Point : public IPoint { 
    LONG m_cRef; 
    IUnknown *m_pUnkFTM;
    long m_x;
    long m_y;
    Point(void) : m_cRef(0), m_x(0), m_y(0) { 
        HRESULT hr = CoCreateFreeThreadedMarshaler(this,&m_pUnkFTM); 
        assert(SUCCEEDED(hr)) ; 
    }
    virtual ~Point(void) { m_pUnkFTM->Release(); } 
};

Соответствующая реализация QueryInterface просто запросила бы интерфейс IMarshal из FTM:

STDMETHODIMP Point::QueryInterface(REFIID riid, void **ppv) 
{ 
    if (riid == IID_IUnknown || riid == IID_IPoint) 
        *ppv = static_cast<IPoint*>(this); 
    else if (riid == IID_IMarshal)
        return m_pUnkFTM->QueryInterface(riid, ppv); 
    else
         return (*ppv = 0), E_NOINTERFACE;
    ((IUnknown* )*ppv)->AddRef();
    return S_OK; 
}

Поскольку используется FTM, не понадобится никаких заместителей, как бы ни маршалировались через внутрипроцессные границы апартамента ссылки на объекты Point. Это применимо к явным вызовам CoMarshalInterface / CoUnmarshalInterface, а также в случаях, когда ссылки на объекты Point передаются как параметры метода на внутрипроцессные заместители объектов, не являющихся объектами Point.

FTM занимает не менее 16 байт памяти. Поскольку многие внутрипроцессные объекты никогда не используются за пределами своего апартамента, то предварительное выделение памяти для FTM не является лучшим использованием имеющихся ресурсов. В высшей степени вероятно, что объект уже имеет некий примитив для синхронизации потоков. В таком случае FTM может быть отложенно агрегирован (lazy-aggregated) при первом же запросе QueryInterface о IMarshal. Для того чтобы добиться этого, рассмотрим такое определение класса:

class LazyPoint : public IPoint { 
    LONG m_cRef; 
    IUnknown *m_pUnkFTM; 
    long m_x; long m_y;
    LazyPoint (void) : m_cRef (0) .m_pUnkFTM(0),m_x(0), m_y(0) {} 
    virtual ~LazyPoint(void) 
      { if (m_pUnkFTM) m_pUnkFTM->Release(); } 
    void Lock(void); 
      // acquire object-specific lock
      // запрашиваем блокировку, специфическую для объектов 
    void Unlock(void); 
      // release object-specific lock 
      // освобождаем блокировку, специфическую для объектов
    :    :    :
};

Основываясь на таком определении класса, следующая реализация QueryInterface осуществит корректное агрегирование FTM по требованию:

STDMETHODIMP Point::QueryInterface(REFIID riid, void **ppv) 
{ 
    if (riid == IID_IUnknown || riid == IID_IPoint)
        *ppv = static_cast<IPoint*>(this);
    else if (riid == IID_IMarshal) {
        this->Lock();
        HRESULT hr = E_NOINTERFACE; 
        *ppv = 0; 
        if (m_pUnkFTM == 0) 
            // acquire FTM first time through 
            // получаем первый FTM
          CoCreateFreeThreadedMarshaler(this, &m_pUnkFTM);
        if (m_pUnkFTM != 0) 
            // by here, FTM is acquired 
            // здесь получен FTM
            hr = m_pUnkFTM->QueryInterface(riid, ppv);
        this->Unlock(); 
        return hr;
    } else
        return (*ppv = 0), E_NOINTERFACE;
    ((IUnknown *)*ppv)->AddRef();
    return S_OK; 
}

Недостатком данного подхода является то, что все запросы QueryInterface на IMarshal будут сериализованы (преобразованы в последовательную форму); тем не менее, если IMarshal вообще не будет запрошен, то будет запрошено меньше ресурсов.

Теперь, когда мы убедились в относительной простоте использования FTM, интересно обсудить случаи, в которых FTM не годится. Конечно, те объекты, которые могут существовать только в однопотоковых апартаментах, не должны использовать FTM, так как маловероятно, что они будут ожидать одновременного обращения к ним. В то же время объекты, способные работать в апартаментах МТА, отнюдь не обязаны использовать FTM. Рассмотрим следующий класс, который использует для выполнения своих операций другие СОМ-объекты:

class Rect : public IRect 
{ 
    LONG m_cRef; 
    IPoint *m_pPtTopLeft;
    IPoint *m_pPtBottomRight;
    Rect(void) : m_cRef(0) { 
        HRESULT hr = CoCreateInstance(CLSID_Point, 0, 
                                      CLSCTX_INPROC, IID_Ipoint, (void**) &m_pPtTopLeft);
        assert(SUCCEEDED (hr));
        hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, 
                              IID_Ipoint, (void**)&m_pPtBottomRight);
        assert (SUCCEEDED(hr));
    } 
    ;    ;    ;
}

Пусть класс Rect является внутрипроцессным и помечен как ThreadingModel = "Both". Разработчик данного Rect-объекта всегда будет выполняться в апартаменте потока, вызывающего CoCreateInstance (CLSID_Rect). Это означает, что два вызова CoCreateInstance (CLSID_Point) будут также выполняться в апартаменте клиента. Правила же СОМ гласят, что элементы данных m_pPtTopLeft и m_pPtBottomRight могут быть доступны только из того апартамента, который выполняет вызовы CoCreateInstance.

Похоже на то, что по меньшей мере один из методов Rect использует в своей работе два интерфейсных указателя в качестве элементов данных:

STDMETHODIMP Rect::get_Area(long *pn) 
{ 
    long top, left, bottom, right; 
    HRESULT hr = m_pPtTopLeft->GetCoords(&left, &top); 
    assert(SUCCEEDED(hr)); 
    hr = m_pPtBottomRight->GetCoords(&right, &bottom);
    assert (SUCCEEDED (hr)); 
    *pn = (right - left) * (bottom - top);
    return S_OK;
}

Если бы класс Rect должен был использовать FTM, тогда можно было бы вызывать этот метод из апартаментов, отличных от того апартамента, который осуществлял начальные вызовы CoCreateInstance. К сожалению, это заставило бы метод get_Area нарушить правила СОМ, поскольку два элемента данных - интерфейсные указатели - являются легальными только в исходном апартаменте. Если бы класс Point также использовал FTM, то формально это не было бы проблемой. Тем не менее, в общем случае клиенты (такие, так класс Rect), не должны делать допущений относительно этой специфической исключительно для реализаций детали. Фактически, если объекты Point не используют FTM и окажутся созданными в другом апартаменте из-за несовместимости с ThreadingModel, то в этом случае объект Rect содержал бы указатели на заместители. Известно, что заместители четко следуют правилам СОМ и послушно возвращают RPC_E_WRONG_THREAD в тех случаях, когда к ним обращаются из недопустимого апартамента.

Это оставляет разработчику Rect выбор между двумя возможностями. Одна из них - не использовать FTM и просто принять к сведению, что когда клиенты передают объектные ссылки Rect между апартаментами, то для обращения к экземплярам класса Rect будет использоваться ORPC. Это действительно является простейшим решением, так как оно не добавляет никакого дополнительного кода и будет работать, не требуя умственных усилий. Другая возможность - не содержать исходные интерфейсные указатели как элементы данных, а вместо этого держать в качестве элементов данных некую маршалированную форму интерфейсного указателя. Именно для этого и предназначена глобальная интерфейсная таблица (Global Interface Table - GIT). Для реализации данного подхода в классе Rect следовало бы иметь в качестве элементов данных не исходные интерфейсные указатели, а "закладку" (cookies) DWORD:

class SafeRect : public IRect { 
    LONG m_cRef; 
      // СОМ reference count 
      // счетчик ссылок СОМ 
    IUnknown *m_pUnkFTM;
      // cache for FTM lazy aggregate 
      // кэш для отложенного агрегирования FTM 
    DWORD m_dwTopLeft;
      // GIT cookie for top/left 
      // закладка GIT для верхнего/левого 
    DWORD m_dwBottomR1ght; 
      // GIT cookie for bottom/right 
      // закладка GIT для нижнего/правого

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

SafeRect::SafeRect(void) : m_cRef(0), m_pUnkFTM(0) 
{ 
      // assume ptr to GIT is initialized elsewhere 
      // допустим, что указатель на GIT инициализирован 
      // где-нибудь в другом месте 
    extern IGIobalInterfaceTable *g_pGIT;
    assert(g_pGIT != 0);
    IPoint *pPoint = 0; 
      // create instance of class Point 
      // создаем экземпляр класса Point 
    HRESULT hr = CoCreateInstance(CLSID_Point, 0, 
                                  CLSCTX_INPROC, IID_Ipoint, (void**)&pPoint);
    assert (SUCCEEDED (hr)); 
      // register interface pointer in GIT 
      // регистрируем интерфейсный указатель в GIT 
    hr =  g_pGIT->RegisterInterfaceInGlobal(pPoint, IID_Ipoint,
                                               &m_dwTopLeft);
    assert(SUCCEEDED(hr)); 
    pPoint->Release();
      // reference is now held in GIT 
      // ссылка теперь содержится в GIT 

      // create instance of class Point 
      // создаем экземпляр класса Point 
    hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, 
                          IID_Ipoint, (void**)&pPoint);
    assert(SUCCEEDED(hr));
      // register interface pointer in GIT 
      // регистрируем интерфейсный указатель в GIT 
    hr = g_pGIT->RegisterInterfaceInGlobal(pPoint, IID_Ipoint, &m_dwBottomRight); 
    assert(SUCCEEDED(hr));
    pPoint->Release();
      // reference is now held in GIT 
      // ссылка теперь содержится в GIT 
} 

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

Поскольку класс был преобразован для использования GIT вместо исходных интерфейсных указателей, он должен демаршалировать новый заместитель в каждом вызове метода, которому требуется доступ к зарегистрированным интерфейсам:

STDMETHODIMP SafeRect::get_Area(long *pn) 
{
    extern IGlobalInterfaceTable *g_pGIT; 
    assert(g_pGIT != 0);
      // unmarshal the two interface pointers from the GIT 
      // демаршалируем дВа интерфейсных указателя из GIT 
    IPoint *ptl = 0, *pbr = 0; 
    HRESULT hr = g_pGIT->GetInterfaceFromGlobal(m_dwPtTopLeft,
                            IID_Ipoint, (void**)&ptl); 
    assert (SUCCEEDED(hr));
    hr = g_pGIT->GetInterfaceFromGlobal(m_dwPtBottomRight, 
                            IID_Ipoint, (void**)&pbr);
      // use temp ptrs to implement method 
      // дпя реализации метода используем временные указатели 
    long top, left, bottom, right; 
    hr = ptl->GetCoords(&left, &top); 
    assert (SUCCEEDED(hr)); 
    hr = pbr->GetCoords(&right, &bottom);
    assert (SUCCEEDED (hr));
    *pn = (right - left) * (bottom - top);
      // release temp ptrs. 
      // освобождаем временные указатели 
    ptl->Release(); 
    pbr->Release();
    return S_OK;
}

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

Все зарегистрированные интерфейсные указатели будут храниться в таблице GIT до тех пор, пока они не будут явно удалены нз GIT. Это означает, что класс SafeRect должен явно аннулировать элементы GIT для двух своих элементов данных:

SafeRect::~SafeRect(void) 
{
    extern IGlobalInterfaceTable *g_pGIT; 
    assert(g_pGIT != 0);
    HRESULT hr = g_pGIT->RevokeInterfaceFromGlobal(m_dwTopLeft);
    assert(SUCCEEDED(hr)); 
    hr = g_pGIT->RevokeInterfaceFromGlobal(m_dwBottomRight);
    assert(SUCCEEDED(hr));
}

Удаление интерфейсного указателя из GIT освобождает все хранящиеся ссылки на объект.

Отметим, что совместное использование GIT и FTM влечет за собой очень много обращений к GIT, которые будут сделаны для создания временных интерфейсных указателей, необходимых для использования в каждом отдельном методе. Хотя GIT оптимизирована именно для поддержки такой схемы использования, код остается однообразным. Следующий простой класс C++ скрывает использование "закладки" GIT за удобным интерфейсом, обеспечивающим безопасность типа:

template <class Itf, const IID* piid> 
class GlobalInterfacePointer { 
    DWORD m_dwCookie; 
      // the GIT cookie 
      // "закладка" GIT 
      // prevent misuse 
      // предотвращаем неправильное использование 
    GlobalInterfacePointer(const GlobalInterfacePointer&);
    void operator =(const GlobalInterfacePointer&);
  public: 

      // start as invalid cookie 
      // начинаем как неправильная "закладка"
    GlobalInterfacePointer(void) : m_dwCookie(0) { } 

      // start with auto-globalized local pointer 
      // начинаем с автоматически глобализованным локальным указателем 
    GlobalInterfacePointer(Itf *pItf, HRESULT& hr) 
         : m_dwCookie(0) { hr = Globalize(pItf); } 

      // auto-unglobalize 
      // осуществляем автоматическую деглобапизацию 
    ~GlobalInterfacePointer(void) 
      { if(m_dwСооkiе) Unglobalize() ; } 

      // register an interface pointer in GIT 
      // регистрируем интерфейсный указатель в GIT 
    HRESULT Globalize(Itf *pItf) { 
        assert (g_pGIT != 0 && m_dwCookie == 0); 
        return g_pGIT->RegisterInterfaceInGlobal(pItf, * piid, &m_dwCookie); 
    } 

      // revoke an interface pointer in GIT 
      // аннулируем интерфейсный указатель в GIT 
    HRESULT Unglobalize(void) { 
        assert(g_pGIT != 0 && m_dwCookie != 0);
        HRESULT hr = g_pGIT->RevokeInterfaceFromGlobal(m_dwCookie);
        m_dwCookie = 0;
        return hr;
    } 

      // get а local interface pointer from GIT 
      // получаем локальный интерфейсный указатель из GIT 
    HRESULT Localize(Itf **ppItf) const { 
        assert(g_pGIT != 0 && m_dwCookie != 0);
        return g_pGIT->GetInteгfaceFromGlobal(m_dwCookie, *piid, (void**)ppItf); 
    } 

      // convenience methods 
      // методы для удобства 
    bool IsOK(void) const { return m_dwCookie != 0; } 
    DWORD GetCookie(void) const { return m_dwCookie; } 
};
    
#define GIP(Itf) GlobalInterfacePointer<Itf, &IID_##Itf> 

Имея данное определение класса и макрос, класс SafeRect теперь вместо исходных DWORD сохраняет GlobalInterfacePointers:

class SafeRect : public IRect { 
    LONG m_cRef: 
      // СОM reference count 
      // счетчик ссылок СОМ 
    IUnknown *m_pUnkFTM;
      // cache for FTM lazy aggregate 
      // кэш дпя отложенного агрегирования FTM 
    GIP(IPoint) m_gipTopLeft;
      // GIT cookie - top/left 
      // "закладка" GIT для верхнего/левого элемента 
    GIP(IPoint) m_gipBottomRight;
      // GIT cookie - bottom/right 
      // "закладка" GIT для нижнего/правого элемента 
    :    :    :
} 

Для инициализации элемента GlobalInterfacePointer разработчик (который выполняется в апартаменте объекта) просто регистрирует обрабатываемые указатели, вызывая метод Globalize на каждый GlobalInterfacePointer:

SafeRect::SafeRect(void) : m_cRef (0), m_pUnkFTM(0) 
{
    IPoint *pPoint = 0; 
      // create instance of class Point 
      // создаем экземпляр класса Point 
    HRESULT hr = CoCreateInstance(CLSID_Point, 0,
                   CLSCTX_INPROC, IID_Ipoint, (void**)&pPoint);
    assert (SUCCEEDED(hr));
      // register interface pointer in GIT 
      // регистрируем интерфейсный указатель в GIT 
    hr = m_gipTopLeft.Globalize(pPoint);
    assert (SUCCEEDED(hr)); 
    pPoint->Release();
      // reference is now held in GIT 
      // теперь ссыпка хранится в GIT 
      // create instance of class Point 
      // создаем экземпляр класса Point 
    hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, 
                          IID_Iроint, (void**) &рРоint);
    assert(SUCCEEDED(hr));
      // register interface pointer in GIT 
      // регистрируем интерфейсный указатель в GIT 
    hr = m_gipBottomRight.Globalize(pPoint); 
    assert (SUCCEEDED (hr)); 
    pPoint->Release();
      // reference is now held in GIT 
      // теперь ссылка хранится в GIT 
} 

Те методы, которым нужен доступ к глобализованным указателям, могут импортировать локальную копию посредством метода Localize из GlobalInterfaсePointer:

STDMETHODIMP SafeRect::get_Top(long *pVal) 
{ 
    IPoint *pPoint = 0;
      // local imported pointer 
      // локальный импортированный указатель 
    HRESULT hr = m_gipTopLeft.Localize(&pPoint);
    if (SUCCEEDED(hr)){ 
        long x; 
        hr = pPoint->get_Coords(&x, pVal);
        pPoint->Release(); 
    } 
    return hr; 
} 

Отметим, что в силу применения маршалера свободной поточной обработки (FreeThreaded Marshaler) исходный интерфейсный указатель не может быть кэширован, а должен импортироваться при каждом вызове метода, чтобы предотвратить попытку доступа из неверного апартамента.

Предыдущий фрагмент кода может быть автоматизирован еще больше. Поскольку большинство вызовов методов в классе GlobalInterfacePointer должны будут локализовать временный указатель в самом вызове метода, то приводимый ниже класс автоматизирует импорт временного указателя и его последующее освобождение, что очень напоминает интеллектуальный указатель (smart pointer):

template <class Itf, const IID* piid> 
class LocalInterfacePointer { 
    Itf *m_pItf; 
      // temp imported pointer 
      // временный импортированный указатель 
      // prevent misuse 
      // предотвращаем неверное использование 
    LocalInterfacePointer(const LocalInterfacePointer&);
    operator = (const LocalInterfacePointer&);
  public: 
    LocalInterfacePointer(const GlobalInterfacePointer<Itf, piid>& rhs, HRESULT& hr) 
    { hr = rhs.Loca1ize(&m_pItf) ; } 

    LocalInterfacePointer(DWORD dwCookie, HRESULT& hr) { 
        assert(g_pGIT != 0);
        hr = g_pGIT->GetInterfaceFromGlobal(dwCookie, *piid, (void**)&m_pItf); 
    } 

    ~LocalInterfacePointer(void) { if (m_pItf) m_pItf->Release(); } 

  class SafeItf : public Itf { 
    STDMETHOD_(ULONG, AddRef) (void) = 0; 
    // hide 
    // скрытый 
    STDMETHOD_(ULONG, Release)(void) = 0; 
    // hide 
    // скрытый 
  }; 

  SafeItf *GetInterface(void) const 
  { return (SafeItf*) m_pItf; } 

  SafeItf *operator ->(void) const 
  { assert(m_pItf != 0); return GetInterface(); } 

}; 

#def1ne LIP(Itf) LocalInterfacePointer<Itf, &IID_##Itf> 

С получением этого второго класса C++ обработка импортированных указателей становится намного проще:

STDMETHODIMP SafeRect::get_Area(long *pn)
{ 
    long top, left, bottom, right; 
    HRESULT hr, hr2;
      // import pointers 
      // импортируем указатели 
    LIP(IPoint) lipTopLeft(m_gipTopLeft, hr); 
    LIP(IPoint) lipBottomRight(m_gipBottomRight, hr2);
    assert(SUCCEEDED(hr) && SUCCEEDED(hr2));
      // use temp tocal pointers 
      // используем временные локальные указатели 
    hr = lipTopLeft->GetCoords(&left, &top);
    hr2 = lipBottomRight->GetCoords(&right, &bottom);
    assert(SUCCEEDED(hr) && SUCCEEDED(hr2));
    *pn = (right - left) * (bottom - top); 
    return S_OK;
      // LocalInterfacePointer auto-releases temp ptrs. 
      // LocalInterfacePointer сам освобождает 
      // временные указатели 
} 

Макросы GIP и LIP делают совместное использование GIT и FTM намного менее громоздким. До появления GIT использование FTM в классе с интерфейсными указателями было значительно более трудным, чем теперь обеспечивает любой из кодов, приведенных в данном разделе.

Где мы находимся?

 

 
Интересное в сети
 
10 новых программ
CodeLobster PHP Edition 3.7.2
WinToFlash 0.7.0008
Free Video to Flash Converter 4.7.24
Total Commander v7.55
aTunes 2.0.1
Process Explorer v12.04
Backup42 v3.0
Predator 2.0.1
FastStone Image Viewer 4.1
Process Lasso 3.70.4
FastStone Image Viewer 4.0
Xion Audio Player 1.0.125
Notepad GNU v.2.2.8.7.7
K-Lite Codec Pack 5.3.0 Full


Наши сервисы
Рассылка новостей. Подпишитесь на рассылку сейчас и вы всегда будете в курсе последних событий в мире информационных технологий.
Новостные информеры. Поставьте наши информеры к себе и у вас на сайте появится дополнительный постоянно обновляемый раздел.
Добавление статей. Если вы являетесь автором статьи или обзора на тему ИТ присылайте материал нам, мы с удовольствием опубликуем его у себя на сайте.
Реклама на сайте. Размещая рекламу у нас, вы получите новых посетителей, которые могут стать вашими клиентами.
 
Это интересно
 

Copyright © CompDoc.Ru
При цитировании и перепечатке ссылка на www.compdoc.ru обязательна. Карта сайта.