проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 09.11.21 14:03
Оценка: 5 (1)
Коллеги, подскажите!

Столкнулся с такой проблемой, когда несколько потоков грузят картинки из кусков памяти (не из файлов).
Создается IStream функцией CreateStreamOnHGlobal. При каком-то количестве одновременно существующих объектов IStream (созданных с разными HGLOBAL)
функция OleLoadPicture начинает возвращать STG_E_REVERTED (0x80030102).

Что это может быть?
Если поместить функцию test_load_image_from_mem_ в критическую секцию, то проблема уходит...

При количестве threads 100 у меня через раз срабатывает assert у OleLoadPicture.
При количестве 1000 — гарантировано.
Картинка задана в коде как массив байт. Это валидная BMP 16x16 4bpp.
Вот минимальный пример:

#include <windows.h>
#include <ole2.h>
#include <olectl.h>

#include <cassert>

void test_load_image_from_mem_(int thread_num, const void* image_data, size_t data_size)
{
    HGLOBAL hmem = ::GlobalAlloc(GMEM_MOVEABLE, data_size);
    assert(hmem);
    void* pmem = ::GlobalLock(hmem);
    memcpy(pmem, image_data, data_size);
    ::GlobalUnlock(hmem);

    IStream* ps = nullptr;
    HRESULT hr = ::CreateStreamOnHGlobal(hmem, TRUE, &ps);
    assert(SUCCEEDED(hr));
    assert(ps);

    IPicture* pic_ptr = nullptr;
    hr = ::OleLoadPicture(ps, static_cast<LONG>(data_size), FALSE, IID_IPicture, (void**)&pic_ptr);
    assert(SUCCEEDED(hr));
    assert(pic_ptr);
    ps->Release();

    pic_ptr->Release();
}

const char TestData[246] = {
  0x42u, 0x4Du, 0xF6u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x76u, 0x00u, 0x00u, 0x00u, 0x28u, 0x00u, 
  0x00u, 0x00u, 0x10u, 0x00u, 0x00u, 0x00u, 0x10u, 0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x04u, 0x00u, 0x00u, 0x00u, 
  0x00u, 0x00u, 0x80u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 
  0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x80u, 0x00u, 0x00u, 0x80u, 
  0x00u, 0x00u, 0x00u, 0x80u, 0x80u, 0x00u, 0x80u, 0x00u, 0x00u, 0x00u, 0x80u, 0x00u, 0x80u, 0x00u, 0x80u, 0x80u, 
  0x00u, 0x00u, 0xC0u, 0xC0u, 0xC0u, 0x00u, 0x80u, 0x80u, 0x80u, 0x00u, 0x00u, 0x00u, 0xFFu, 0x00u, 0x00u, 0xFFu, 
  0x00u, 0x00u, 0x00u, 0xFFu, 0xFFu, 0x00u, 0xFFu, 0x00u, 0x00u, 0x00u, 0xFFu, 0x00u, 0xFFu, 0x00u, 0xFFu, 0xFFu, 
  0x00u, 0x00u, 0xFFu, 0xFFu, 0xFFu, 0x00u, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 

  0xDDu, 0xDDu, 0xDDu, 0xDDu, 0x00u, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xD0u, 0xFFu, 0x0Du, 0xDDu, 0xDDu, 
  0xDDu, 0xDDu, 0xDDu, 0xD0u, 0xFFu, 0x0Du, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xD0u, 0x00u, 0xDDu, 0xDDu, 0xDDu, 
  0xDDu, 0xDDu, 0xDDu, 0x0Du, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xD0u, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 
  0xDDu, 0xDDu, 0x0Du, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xD0u, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 
  0xDDu, 0x0Du, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xD0u, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0x00u, 
  0x0Du, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xD0u, 0xFFu, 0x0Du, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xD0u, 0xFFu, 
  0x0Du, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0x00u, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 
  0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, 0xDDu, };


DWORD WINAPI thread_proc_(LPVOID lpParameter)
{
    const int thread_num = (int)lpParameter;

    ::OleInitialize(nullptr);

    test_load_image_from_mem_(thread_num, TestData, sizeof(TestData));

    ::OleUninitialize();
    return 0;
}

HANDLE start_thread_(int num)
{
    return ::CreateThread(nullptr, 0, thread_proc_, (LPVOID)num, 0, nullptr);
}

void start_threads_(int count)
{
    HANDLE* harr = new HANDLE[count];

    for (int num = 0; num < count; ++num)
        harr[num] = start_thread_(num);

    ::WaitForMultipleObjects(count, harr, TRUE, INFINITE);

    for (int num = 0; num < count; ++num)
        ::CloseHandle(harr[num]);
    delete[] harr;
}

int CALLBACK WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR lpCmdLine,
                     int nCmdShow)
{
    ::MessageBox(NULL, L"Ready to start", L"TestOleLoadPicture", MB_OK);
    start_threads_(100);
    ::MessageBox(NULL, L"Everything OK!", L"TestOleLoadPicture", MB_OK);
}
Re: проблемы с OleLoadPicture и threads
От: Сергей Мухин Россия  
Дата: 09.11.21 15:08
Оценка:
Здравствуйте, qaz77, Вы писали:



Q> ::OleInitialize(nullptr);


OleInitialize calls CoInitializeEx internally to initialize the COM library on the current apartment. Because OLE operations are not thread-safe, OleInitialize specifies the concurrency model as single-thread apartment.

см CoInitializeEx
---
С уважением,
Сергей Мухин
Re[2]: проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 09.11.21 15:49
Оценка:
Здравствуйте, Сергей Мухин, Вы писали:

СМ>OleInitialize calls CoInitializeEx internally to initialize the COM library on the current apartment. Because OLE operations are not thread-safe, OleInitialize specifies the concurrency model as single-thread apartment.


СМ>см CoInitializeEx



Замена OleInitalize(nullptr)/OleUninitialize() на CoInitialize(nullptr, COINIT_MULTITHREADED)/CoUnitialize() ничего не меняет.

Замечу, что в каждом треде используются свои независимые объекты IStream и IPicture.
Re: проблемы с OleLoadPicture и threads
От: kov_serg Россия  
Дата: 09.11.21 17:43
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Что это может быть?

А нафига вы через GlobalAlloc и CreateStreamOnHGlobal создаёте поток?
Что мешает свой IStream реализовать?
Re: проблемы с OleLoadPicture и threads
От: VVV Россия  
Дата: 10.11.21 01:31
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Коллеги, подскажите!


Не знаю поможет ли с основной проблемой, но нужно поменять местами релизы

было:
    ps->Release();

    pic_ptr->Release();


стало:
    pic_ptr->Release();

    ps->Release();
Re[2]: проблемы с OleLoadPicture и threads
От: kov_serg Россия  
Дата: 10.11.21 06:23
Оценка:
Здравствуйте, VVV, Вы писали:

VVV>Не знаю поможет ли с основной проблемой, но нужно поменять местами релизы


VVV>было:

VVV>
    ps->Release();
    pic_ptr->Release();

VVV>стало:
VVV>
    pic_ptr->Release();
    ps->Release();

Это не должно влиять ибо счетчику ссылок всё равно.
Re[2]: проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 10.11.21 06:50
Оценка:
Здравствуйте, kov_serg, Вы писали:
_>А нафига вы через GlobalAlloc и CreateStreamOnHGlobal создаёте поток?

Взял готовое.
На самом деле, этому коду уже сто лет в обед.
Наверняка, какой-то пример из msdn был положен в основу.

_>Что мешает свой IStream реализовать?


Попробовал с вашей реализацией IStream:
    IStream* ps = nullptr;
    HRESULT hr = createMemoryStream(&ps, data_size);
    assert(SUCCEEDED(hr));
    assert(ps);

    hr = ps->Write(image_data, data_size, nullptr);
    assert(SUCCEEDED(hr));

    LARGE_INTEGER pos = { 0, 0 };
    hr = ps->Seek(pos, STREAM_SEEK_SET, nullptr);
    assert(SUCCEEDED(hr));


При 100 потоках также вылазит вся та же проблема.
OleLoadPicture возвращает STG_E_REVERTED.
Re[2]: проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 10.11.21 06:55
Оценка:
Здравствуйте, VVV, Вы писали:

VVV>Не знаю поможет ли с основной проблемой, но нужно поменять местами релизы


VVV>стало:

VVV>
VVV>    pic_ptr->Release();

    ps->>Release();
VVV>


Не помогает.

Да и странно было бы думать, что IPicture внутри держит IStream после загрузки.
Если бы вдруг и держала, то вызывала бы AddRef и мой ps->Release() только уменьшал бы счетчик стрима, а не удалял его.
Re: дополнение про OleLoadPicturePath
От: qaz77  
Дата: 10.11.21 07:41
Оценка:
Сделал аналогичный тест для загрузки IPicture из файла через OleLoadPicturePath.
При таком же количестве тредов ~100 наблюдается отказ в виде возврата E_FAIL.

Рецепт прекращения этого безобразия: поместить все время жизни объекта IPicture в критическую секцию.
Подготовка IStream может быть перед входом в критическую секцию — это не влияет.
Re[3]: проблемы с OleLoadPicture и threads
От: Jack128  
Дата: 10.11.21 08:13
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Да и странно было бы думать, что IPicture внутри держит IStream после загрузки.

Ну например GDI+ так и делает
Re[3]: проблемы с OleLoadPicture и threads
От: kov_serg Россия  
Дата: 10.11.21 09:39
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Взял готовое.

Q>На самом деле, этому коду уже сто лет в обед.
Q>Наверняка, какой-то пример из msdn был положен в основу.

Q>При 100 потоках также вылазит вся та же проблема.

Q>OleLoadPicture возвращает STG_E_REVERTED.
Еще вариант реализовать свой IPicture
А если серьёзно, то какая у вас win-да? На windows 7 не воспроизводится.
Но при 1000 потоков у меня виртуалка очень призадумалась, пришлось пристрелить.
Re[2]: дополнение про OleLoadPicturePath
От: Mr.Delphist  
Дата: 10.11.21 13:03
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Рецепт прекращения этого безобразия: поместить все время жизни объекта IPicture в критическую секцию.


Что по сути означает, что тогда потоки исполняются строго последовательно, верно?
Re: проблемы с OleLoadPicture и threads
От: Mr.Delphist  
Дата: 10.11.21 13:18
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>При количестве threads 100 у меня через раз срабатывает assert у OleLoadPicture.

Q>При количестве 1000 — гарантировано.

На всякий случай: C/C++ -> Code Generation -> Runtime Library

с каким рантаймом собирается проект? обычный или MT?
Re[4]: проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 10.11.21 15:07
Оценка:
Здравствуйте, kov_serg, Вы писали:

_>А если серьёзно, то какая у вас win-да? На windows 7 не воспроизводится.

_>Но при 1000 потоков у меня виртуалка очень призадумалась, пришлось пристрелить.

На хосте Win 8.1 и процессор с 8-ю ядрами.

Сейчас попробовал на виртуалке Win 10.
Если в настройках VM одно ядро, то не воспроизводится.
Поставил 4 ядра — стало воспроизводиться.

Виртуалки Win 7, к сожалению, сейчас нет под рукой.

Время теста под виртуалкой в пределах единиц секунд для 1000 потоков.
Под хостом чуть быстрее.
Re[3]: дополнение про OleLoadPicturePath
От: qaz77  
Дата: 10.11.21 15:09
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>Что по сути означает, что тогда потоки исполняются строго последовательно, верно?


Всякая возня со стримом выполняется параллельно.
Вплоть до вызова OleLoadPicture...
Входим в критическую секцию непосредственно перед OleLoadPicture/OleLoadPicturePath и выходим сразу после pic_ptr->Release().
Re[2]: проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 10.11.21 15:17
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>На всякий случай: C/C++ -> Code Generation -> Runtime Library


MD>с каким рантаймом собирается проект? обычный или MT?


Рантайм многопоточный. Дебажный /MDd, чтобы assert срабатывали.
Я проверял и для релиза, та же картина. Там просто AV будет при обращении нулевому pic_ptr.
В конце концов, проблема выросла из боевого проекта, который в релизе тестировался.

Собирал x86 exe VS 2013 upd 5.
Воспроизводится, по крайней мере, на Win 8.1 и Win 10.
Похоже, важным фактором для воспроизведения является кол-во ядер > 1.
Re: проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 10.11.21 15:39
Оценка:
Q> ::WaitForMultipleObjects(count, harr, TRUE, INFINITE);

Тут вот у меня косяк с ожиданием.
WFMO принимает не более MAXIMUM_WAIT_OBJECTS хендлов, которая равна 64.
Надо в цикле вызывать WaitForSingleObject (аля JoinAll).

Но это не отменяет глюка с OleLoadPicture/OleLoadPicturePath.
Если задавать кол-во тредов 60, то все воспроизводится при правильном ожидании.
Re[3]: проблемы с OleLoadPicture и threads
От: Mr.Delphist  
Дата: 10.11.21 15:44
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Собирал x86 exe


О, а вот это важный момент. Сколько памяти в итоге занимает приложение в момент первого сбоя? Тредов много, каждый имеет накладные расходы на стек и т.п.
Re[4]: проблемы с OleLoadPicture и threads
От: qaz77  
Дата: 10.11.21 16:26
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

Q>>Собирал x86 exe

MD>О, а вот это важный момент. Сколько памяти в итоге занимает приложение в момент первого сбоя? Тредов много, каждый имеет накладные расходы на стек и т.п.

Диспетчер задач показывает 32 Мб памяти.
Сейчас собрал x64 — все то же самое.
Re: проблемы с OleLoadPicture и threads
От: Слава  
Дата: 10.11.21 21:45
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>При количестве threads 100 у меня через раз срабатывает assert у OleLoadPicture.

Q>При количестве 1000 — гарантировано.

(прочитав весь тред)

Вы не желаете ли вместо критической секции повесить семафор на 60 единиц, например? Если уж ниже по треду было найдено волшебное число в 64 хэндлера.

То есть, потоков может быть уйма, а вот работающих с тем, что вы раньше закрывали критической секцией — не более 60.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.