Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > C/C++: Системное программирование и WinAPI > ...Эх потоки... пыль, да туман...


Автор: Paspartu 22.6.2010, 23:54
Доброго времени суток! 
…Вот, разбираюсь с потоками, прочитал Дж. Рихтера, но возникли вопросы по синхронизации
 и вообще в каких случаях ее делать, так что вопрос чисто теоретический:

Допустим есть функция : (.cpp):
Код

namespace
{
    // Куча разных вспомогательных функций
}

bool IsPrime (INT64 n)
{
    // пользуемся вспомогательными функциями возвращаем результат
}


И где-то класс CBaseThread который создает поток со static методом:
Код

//…
unsigned WINAPI CBaseThread::ThreadEntry(void *pArg)
{
    CBaseThread* pActive = (CBaseThread*) pArg;
    // Здесь pActive – указатель на свойже класс

    pActive->Run(); // вызов метода Run() в наследнике CBaseThread
    // Run – чисто виртуальный…
    
    return 0;
}
//…



// Где-то в производном классе:
Код

void CDerived::Run()
{
       // Сюда попадаем из каждого потока т.е. из каждого экземпляра класса
       // CBaseThread (CDerived), хорошо, но!
       //
       // 1. Могу ли я вызвать прямо отсюда IsPrime() ? или необходимо ее вызов 
       // синхранизировать, т.к. несколько потоков будут пытаться в нее
       // залезть?
       //
       // 2. Если синхронизировать доступ, то только один поток сможет в нее
       // залезть, а если есть необходимость в одновремменной работе – нужны
       // несколько экземляров?
       //
       // 3. Если необходимо несколько экземляров – это класс т.е. IsPrime()
       // поместить в класс, напрашивается этот же,но необходимо в другой (так 
       // надо), пусть в класс… СPrime.
       // Есть ли разница как создавать этот класс на стеке или new если он
       // будет являться членом CDerived (в случае new – хранить указатель)? 
       // 
       // 4. И вообще что происходит если разные потоки пытаются залезть в один
       // и тот же класс через один указатель? Если доступ к членам класса,
       // вроде понятно, что надо в любом случае синхронизировать на
       // чтение/запись так ли это? …А что происходит к аналогичному доступу,
       // но к методам класса? 

}




Автор: xvr 23.6.2010, 00:07
Цитата(Paspartu @  22.6.2010,  23:54 Найти цитируемый пост)
1. Могу ли я вызвать прямо отсюда IsPrime() ? или необходимо ее вызов 
       // синхранизировать, т.к. несколько потоков будут пытаться в нее
       // залезть?
Зависит от содержимого функции IsPrime. Если она (прямо или косвенно, через вызываемые из нее функции) не модифицирует глобальные объекты - то можно звать как есть.
Если что то модифицирует - то надо смотреть, что и как (в общем случае нужно синхронизировать)


Цитата(Paspartu @  22.6.2010,  23:54 Найти цитируемый пост)
       // 2. Если синхронизировать доступ, то только один поток сможет в нее
       // залезть, а если есть необходимость в одновремменной работе – нужны
       // несколько экземляров?
Опять же - если необходимо что бы разные потоки видели общие данные, модифицируемые этой функцией - то нужна синхронизация. Если же вызовы из разных потоков не влияют друг на друга - то можно сделать несколько экземпляров (но не функции, а данных, с которыми она работает)

Цитата

3. Если необходимо несколько экземляров – это класс т.е. IsPrime()
       // поместить в класс, напрашивается этот же,но необходимо в другой (так 
       // надо), пусть в класс… СPrime.
       // Есть ли разница как создавать этот класс на стеке или new если он
       // будет являться членом CDerived (в случае new – хранить указатель)? 
С точки зрения многопоточности разницы нет. С точки зрения здравого смысла со стеком нужно обращаться осторожно - он не резиновый (особенно в потоках)

Цитата(Paspartu @  22.6.2010,  23:54 Найти цитируемый пост)
А что происходит к аналогичному доступу,
       // но к методам класса? 
Ничего. Защищать нужно в конечном случае именно данные. Сериализация вызовов методов делается ИМЕННО для защиты данных, с которыми они работают


Автор: Earnest 23.6.2010, 07:04
Цитата(xvr @  23.6.2010,  01:07 Найти цитируемый пост)
Зависит от содержимого функции IsPrime. Если она (прямо или косвенно, через вызываемые из нее функции) не модифицирует глобальные объекты - то можно звать как есть.

Не совсем так. Если IsPrime читает переменные, которые могут изменять другие функции (конкурентно), то тоже нужно защищать. Даже простые переменные типа int. И вообще, пока не встали вопросы производительности, я бы в начале всех функций, которые могут вызываться из разных потоков, поставила блокировку.

Автор: jonie 23.6.2010, 07:28
Earnest, чтение потокобезопасно. Лично я не вижу смысла ставить блокировку на чтение - на запись ставить надо. Паттерн UnitOfWork в общем с блокировкой типа "заблокировал и всё че надо поменял одним махом".

В .NET например нет функций чтения Interlocked* - именно потому что всегда прочтенное значение будет именно то которое надо, как бы логически это не смотрелось неверным - просто один поток успеть может прочесть до изменения - но бловировка тут ничего не даст - он все-равно прочтет "старое" значение.

Автор: GremlinProg 23.6.2010, 08:59
а Interlocked* и введены как раз для изменения значения, читать - понятно можно и без синхронизации,
но вот если к примеру идет очень точный счет среди потоков, и они для этого счета используют общую переменную,
которая постоянно меняется этими же потоками, то простое чтение без синхронизации может привести к неожиданным результатам:

переменная еще не дописана потоком 1, а поток 2 уже начал ее читать, причем, когда поток 2 закончил ее читать, полток 1 только закончил в нее писать 

что получится?
поток 2 может не получить не только нового значения переменной, но и не старого, т.е. поток 2 в момент расчета следующей итерации точно не опирается на расчеты потока 1

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

вот например идет заполнение массива потоком 1, поток 2 может его спокойно читать без синхронизации,
но что он в итоге прочитает, если для потока 2 массив актуален только в заполненном состоянии?

Автор: xvr 23.6.2010, 09:00
Цитата(jonie @ 23.6.2010,  07:28)
Earnest, чтение потокобезопасно. 

Но может быть не атомарно, в таком случае не гарантируется целостность считанного значения

Автор: jonie 23.6.2010, 10:58
GremlinProgxvr, читать можно без локов, всегда. Никакая атомарность чтения не проблема, т.к. запись атомарна (вот где локи нужны) - остальные ждут пока все запишется.
Цитата

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

Чета я в общем сомневаюсь что вышеописанная неатомарность чтения на intel совместимых процах реализуема при должном обеспечении блокировки при записи

Добавлено @ 11:00
Цитата

вот например идет заполнение массива потоком 1, поток 2 может его спокойно читать без синхронизации,
но что он в итоге прочитает, если для потока 2 массив актуален только в заполненном состоянии? 
это уже дизайн такой, потоки тут непричем. Даже в одномпотоке, заменив слово "поток" на "функция" получим тот же "косяк". Тут надо завести bool bFilled и синхронизироваться при ее записи, а второй поток пусть ожидает в while(!bFilled) без синхронизации.

В общем не надо путать проблемы дизайна решения и проблемы потоков.

Автор: GremlinProg 23.6.2010, 11:04
Цитата(jonie @  23.6.2010,  12:58 Найти цитируемый пост)
GremlinProg, xvr, читать можно без локов, всегда. Никакая атомарность чтения не проблема, т.к. запись атомарна (вот где локи нужны) - остальные ждут пока все запишется.

это все правильно до тех пор, пока процессор и/или ядро в единственном экземпляре, т.е. в полном отсутствии параллелизма

Добавлено через 1 минуту и 11 секунд
в наше время это уже редкость

Автор: Paspartu 23.6.2010, 11:06
Доброго времени суток!
Всем огромное спасибо за разъяснение, но поправьте меня если я ошибаюсь:
1. Синхронизацию нужно проводить только при доступе различных потоков к общим данным
Таким образом: 

a). Если 1-й поток читает к примеру массив то если в этот момент 2-й поток пытается его изменить то его нужно блокировать, пока чтение не будет завершено.
б). Если 1-й пишет, а 2-й читает то нужна блокировка 2-го потока так что бы он читал уже измененные значения. Без нее ничего страшного не произойдет? Кроме того, что будет прочитано старое значение?
в). Если оба потока читают – блокировка какого-либо из них не нужна.

2. Если происходит вызов (одной и той же) функции которая возвращает значение сколько угодными потоками то синхронизация не нужна так как общих данных в ней нет, а возвращаемое значение, ее параметры и ее локальные переменные будут для каждого потока своими?

3. Запутался с классами и их методами… если класс создан через new и в каждом потоке используется указатель на него, что будет копироваться в стек потока? К примеру, в разных потоках через указатель будет вызываться один и тот же метод, аналогично будут копироваться только возвращаемое значение, параметры метода и его локальные переменные? Или повторюсь, есть ли необходимость для каждого потока создавать свой экземпляр класса?
т.е. есть класс:

Код

class CA
{
public:
    // ...
    void        F1(int a, int b);
    double    F2(int a, int b, int c);
        int        F2(int a) const;
    // ...
private:
    int        m_n;
    double    m_db;
    char     m_ch;
    // ...
};



Если при вызове методов F1, F2 их параметры будут копироваться в стек потока, то что будет с его членами m_n, m_db, m_ch? Если мы используем оди и тот же указатель на этот класс в разных потоках. В каких случаях нужна синхронизация методов F1, F2? Только если они изменяют значения членов класса? Нужно ли синхронизировать const или нет, т.к. он не изменяет объект? Опять же таки если в классе нет общих данных нужных нескольким потокам, к примеру в нем несколько методов, а члены int m_n и т.д. испоьзуются как промеж. Значения вычислений(образно)… можно ли при исопользовании одного экземляра для разных потоков обойтись без синхронизации, или она небходима, т.к. члены int m_n и т.д. все таки будут общими и они не будут копироваться в стек? 
Короче говоря… как бы с функциями вроде понятно, что они (значения локальных переменых и т.д.) копируются в стек, а если используется общий указатель на класс? Копирование в стек потока параметров и локальных переменных метода происходит в момент вызова потоком данного метода? А что происходит с членами класса (не методами) они становяться общими? А если мы в каждом потоке используем свой экземпляр то, синхронизация не нужна т.к. общих данных нет?

… Много налил воды… но все же помогите разобраться раз и навсегда… наверное. smile 

Автор: GremlinProg 23.6.2010, 11:07
Цитата(jonie @  23.6.2010,  12:58 Найти цитируемый пост)
Даже в одномпотоке, заменив слово "поток" на "функция" получим тот же "косяк"

не-не, в одном потоке такого эффекта не получишь

Автор: xvr 23.6.2010, 12:11
Цитата(jonie @  23.6.2010,  10:58 Найти цитируемый пост)
GremlinProg, xvr, читать можно без локов, всегда. Никакая атомарность чтения не проблема, т.к. запись атомарна (вот где локи нужны) - остальные ждут пока все запишется.
Проблема, даже с атомарной записью. Пример - читаем 64х битное значение на 32х битном процессоре. Чтение будет производится в 2 приема (2 по 32 бита). Другой процесс в это время инвертирует значение переменной. Предположим, что в начале переменная равна 0, чтение начинается с младшего слова, и 2й процесс записал новое значение между 2мя чтениями 1го процесса. В результате 1й процесс прочтет 0xFFFFFFFF00000000, что неверно с любой точки зрения  smile 


Автор: xvr 23.6.2010, 12:29
Цитата(Paspartu @  23.6.2010,  11:06 Найти цитируемый пост)
a). Если 1-й поток читает к примеру массив то если в этот момент 2-й поток пытается его изменить то его нужно блокировать, пока чтение не будет завершено.
б). Если 1-й пишет, а 2-й читает то нужна блокировка 2-го потока так что бы он читал уже измененные значения. Без нее ничего страшного не произойдет? Кроме того, что будет прочитано старое значение?
в). Если оба потока читают – блокировка какого-либо из них не нужна.
Если ЛЮБОЙ поток модифицирует содержимое массива, то нужен какой либо способ СИНХРОНИЗАЦИИ доступа к массиву. Эта синхронизация должна в том или ином виде быть применена ко ВСЕМ потокам, которые работают с массивом, т.е. это по сути принадлежность массива, а не потока.
В случае если только 1 поток пишет данные, а все остальные читают, синхронизация может быть сильно упрощена (вплоть до полного отсуствия, если изменяемые данные имеют длинну 32 бита и выровненны на 4х байтовую границу)
Может сильно помочь набор Interlocked* функций (уже упоминались). С ними можно избежать блокировки потоков

Цитата(Paspartu @  23.6.2010,  11:06 Найти цитируемый пост)
2. Если происходит вызов (одной и той же) функции которая возвращает значение сколько угодными потоками то синхронизация не нужна так как общих данных в ней нет, а возвращаемое значение, ее параметры и ее локальные переменные будут для каждого потока своими?
Да

Цитата(Paspartu @  23.6.2010,  11:06 Найти цитируемый пост)
К примеру, в разных потоках через указатель будет вызываться один и тот же метод, аналогично будут копироваться только возвращаемое значение, параметры метода и его локальные переменные?
Да. Но вот члены класса (переменные) будут общими

Цитата(Paspartu @  23.6.2010,  11:06 Найти цитируемый пост)
Или повторюсь, есть ли необходимость для каждого потока создавать свой экземпляр класса?
Это сильно зависит от предназначения класса

Цитата(Paspartu @  23.6.2010,  11:06 Найти цитируемый пост)
Если при вызове методов F1, F2 их параметры будут копироваться в стек потока, то что будет с его членами m_n, m_db, m_ch?
Будут общими

Члены классов (переменные) ничем не отличаются (для multithread'а) от обычных глобальных переменных. Все требования по сериализации доступа так же применимы и к ним

NB. Обычно при разделении одной переменной между потоками сериализацию применяют ИМЕННО к переменной, а не к потокам. Пример:
Код

void some_action(int some)
{
 EnterCriticalSection(&cs);
 my_global_var+=some;
 LeaveCriticalSection(&cs);
}
Эту функцию можно безопасно вызывать из любых потоков - она сама сериализует доступ к глобальной переменной my_global_var

Можно написать С++ класс (указатель), который будет сериализовать доступ к объекту, который он содержит
Код

template<class Item>
class TSafePtr {
 Item* self;
 CRITICAL_SECTION cs;
public:
 TSafePtr(Item* s) :self(s)
  {
   InitializeCriticalSection(&cs);
  }
 ~TSafePtr()
  {
   DestroyCriticalSection(&cs);
  }

 class Proxy {
   Item* self;
   CRITICAL_SECTION& cs;
 public:
   Proxy(Item* s, CRITICAL_SECTION& c) : self(s), cs(c)
    {
     EnterCriticalSection(&cs);
    }
  ~Proxy()
   {
     LeaveCriticalSection(&cs);
   }
  Item* operator -> () {return self;}
 };

 Proxy operator -> () {return Proxy(self,cs);}
};

(Не компилировал, писал прямо тут)
Использование:
Код

MyClass mclass;

TSafePtr<MyClass> my_ptr(&mclass);

my_ptr->some();

Вызов MyClass::some(); будет сериализован



Автор: Paspartu 23.6.2010, 12:33
Конечно, про 32-х и 64-х битные процессоры... это, конечно, все интересно  smile , но нельзя ли как это там... ближе к теме, а уж лучше конкретно по вопросам...  smile 

Автор: jonie 23.6.2010, 12:34
xvr, хм, ладно убедили)

Автор: Paspartu 23.6.2010, 12:41
Ура!!!  smile 
xvr, С-П-А-С-И-Б-О !!!

К сожалению, у меня не достаточно прав, на увеличение Вашего рейтинга, и все что могу - это еще раз спасибо за разъясгегие!!!  smile

Добавлено позже
Ура!!!  smile 
xvr, С-П-А-С-И-Б-О !!!

К сожалению, у меня не достаточно прав, на увеличение Вашего рейтинга, и все что могу - это еще раз спасибо за разъясгегие!!!  smile

Добавлено через 3 минуты и 28 секунд
Еще раз всем  спасибо! Многое для меня прояснилось!

Автор: GremlinProg 23.6.2010, 12:51
Цитата(Paspartu @  23.6.2010,  14:41 Найти цитируемый пост)
К сожалению, у меня не достаточно прав, на увеличение Вашего рейтинга, и все что могу - это еще раз спасибо за разъясгегие!!!

без проблем, xvr, +1

Автор: Earnest 23.6.2010, 15:45
Во понаписали-то, пока я своими делами занималась...
Цитата(jonie @  23.6.2010,  13:34 Найти цитируемый пост)
xvr, хм, ладно убедили) 

И слава богу, а то, поверь, убеждаться в этом на собственной шкуре гораздо неприятнее.  smile 

Автор: exploys 28.10.2010, 03:43
Цитата(xvr @  23.6.2010,  10:29 Найти цитируемый пост)
Можно написать С++ класс (указатель), который будет сериализовать доступ к объекту, который он содержит

    
Код

template<class Item>
class TSafePtr {
 Item* self;
 CRITICAL_SECTION cs;
public:
 TSafePtr(Item* s) :self(s)
  {
   InitializeCriticalSection(&cs);
  }
 ~TSafePtr()
  {
   DestroyCriticalSection(&cs);
  }
 class Proxy {
   Item* self;
   CRITICAL_SECTION& cs;
 public:
   Proxy(Item* s, CRITICAL_SECTION& c) : self(s), cs(c)
    {
     EnterCriticalSection(&cs);
    }
  ~Proxy()
   {
     LeaveCriticalSection(&cs);
   }
  Item* operator -> () {return self;}
 };
 Proxy operator -> () {return Proxy(self,cs);}
};

(Не компилировал, писал прямо тут)

Удобная штука.
Единственное не DestroyCriticalSection(&cs); а DeleteCriticalSection(&cs);

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)