как сделать поток приложений безопасным? - PullRequest
55 голосов
/ 26 февраля 2011

Я думал, что потокобезопасность, в частности, означает, что он должен удовлетворять потребности нескольких потоков для доступа к одним и тем же общим данным. Но, похоже, этого определения недостаточно.

Может кто-нибудь, пожалуйста, перечислить, что нужно сделать или позаботиться о том, чтобы сделать поток приложения безопасным . Если возможно, дайте ответ относительно языка Си / Си ++.

Ответы [ 4 ]

55 голосов
/ 26 февраля 2011

Есть несколько способов, которыми функция может быть поточно-ориентированной.

Может быть реентерабельным . Это означает, что функция не имеет состояния и не затрагивает глобальные или статические переменные, поэтому ее можно вызывать из нескольких потоков одновременно. Термин происходит от разрешения одному потоку войти в функцию, в то время как другой поток уже находится внутри нее.

Может иметь критическую секцию . Этот термин часто используется, но, честно говоря, я предпочитаю критические данные . Критическая секция возникает каждый раз, когда ваш код касается данных, которые совместно используются несколькими потоками. Поэтому я предпочитаю сосредоточиться на этих важных данных.

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

Если все ваши функции являются поточно-ориентированными и все ваши общие данные защищены должным образом, ваше приложение должно быть поточно-ориентированным.

Как сказал Сумасшедший Эдди, это огромный вопрос. Я рекомендую читать в буст-потоках и использовать их соответствующим образом.

Предупреждение низкого уровня : компиляторы могут переупорядочивать операторы, что может нарушить безопасность потоков. С несколькими ядрами каждое ядро ​​имеет свой собственный кеш, и вам необходимо правильно синхронизировать кеш, чтобы обеспечить безопасность потоков. Кроме того, даже если компилятор не переупорядочивает операторы, аппаратное обеспечение может. Таким образом, полная, гарантированная безопасность потоков сегодня на самом деле невозможна. Тем не менее, вы можете получить 99,99% пути, и сейчас ведется работа с поставщиками компиляторов и производителями процессоров, чтобы исправить эту давнюю оговорку.

В любом случае, если вы ищете контрольный список, чтобы сделать класс потокобезопасным:

  • Определите любые данные, которые являются общими для всех потоков (если вы пропустите их, вы не сможете защитить их)
  • создайте элемент boost::mutex m_mutex и используйте его всякий раз, когда вы пытаетесь получить доступ к этим данным общего члена (в идеале общие данные являются частными для класса, поэтому вы можете быть более уверены, что защищаете их должным образом).
  • очистить глобалы. Глобалы в любом случае плохи, и удачи в попытках сделать что-нибудь с потоками безопасным для глобальных потоков.
  • Остерегайтесь ключевого слова static. На самом деле это не потокобезопасно. Так что, если вы пытаетесь сделать синглтон, он не будет работать правильно.
  • Остерегайтесь парадигмы блокировки с двойной проверкой. Большинство людей, использующих его, неправильно понимают его, и оно может быть нарушено из-за низкого уровня предупреждения.

Это неполный контрольный список. Я добавлю больше, если подумаю, но, надеюсь, этого достаточно, чтобы вы начали.

14 голосов
/ 26 февраля 2011

Две вещи:

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

Например, если мы начнем с:

// Globals
int x;
int y;

// Function that needs to be accessed by multiple threads
// currently relies on globals, and hence cannot work with
// multiple threads
int myFunc()
{
    return x+y;
}

Как только мы добавим в структуру состояний код:

typedef struct myState
{
   int x;
   int y;
} myState;

// Function that needs to be accessed by multiple threads
// now takes state struct
int myFunc(struct myState *state)
{
   return (state->x + state->y);
}

Теперь вы можете спросить, почему бы просто не передать x и y в качестве параметров. Причина в том, что этот пример является упрощением. В реальной жизни ваша структура состояний может иметь 20 полей, и передача большинства из этих параметров через 4-5 функций становится пугающей. Вы бы предпочли передать один параметр вместо многих.

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

3 голосов
/ 09 марта 2016

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

Другой тип блокировок:

Использование atomic_flg_lck:

class SLock
{
public:
  void lock()
  {
    while (lck.test_and_set(std::memory_order_acquire));
  }

  void unlock()
  {
    lck.clear(std::memory_order_release);
  }

  SLock(){
    //lck = ATOMIC_FLAG_INIT;
    lck.clear();
  }
private:
  std::atomic_flag lck;// = ATOMIC_FLAG_INIT;
};

Использование атомный:

class SLock
{
public:
  void lock()
  {
    while (lck.exchange(true));
  }

  void unlock()
  {
    lck = true;
  }

  SLock(){
    //lck = ATOMIC_FLAG_INIT;
    lck = false;
  }
private:
  std::atomic<bool> lck;
};

Использование мьютекс:

class SLock
{
public:
  void lock()
  {
    lck.lock();
  }

  void unlock()
  {
    lck.unlock();
  }

private:
  std::mutex lck;
};

Только для Windows :

class SLock
{
public:
  void lock()
  {
    EnterCriticalSection(&g_crit_sec);
  }

  void unlock()
  {
    LeaveCriticalSection(&g_crit_sec);
  }

  SLock(){
    InitializeCriticalSectionAndSpinCount(&g_crit_sec, 0x80000400);
  }

private:
  CRITICAL_SECTION g_crit_sec;
};

atomic и и atomic_flag сохраняют нить в счетчике вращения. Мьютекс просто спит нить.Если время ожидания слишком велико, возможно, лучше спать нить.Последний " CRITICAL_SECTION " сохраняет поток в счетчике спинов до истечения времени, затем поток переходит в режим сна.

Как использовать эти критические секции?

unique_ptr<SLock> raiilock(new SLock());

class Smartlock{
public:
  Smartlock(){ raiilock->lock(); }
  ~Smartlock(){ raiilock->unlock(); }
};

Использование языка raii.Конструктор для блокировки критической секции и деструктор для ее разблокировки.

Пример

class MyClass {

   void syncronithedFunction(){
      Smartlock lock;
      //.....
   }

}

Эта реализация является поточно-ориентированной и исключительной, поскольку блокировка переменной сохраняется в стеке, так что когдаобласть действия функции завершена (конец функции или исключение), будет вызван деструктор.

Я надеюсь, что вы найдете это полезным.

Спасибо !!

0 голосов
/ 26 февраля 2011

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

Затем «решите» проблему производителя / потребителя, однако вы хотите предотвратить переполнение или переполнение очередей. http://en.wikipedia.org/wiki/Producer-consumer_problem

Пока вы сохраняете свои потоки локализованными, просто обмениваясь данными, отправляя копии по очереди, и не обращаясь к небезопасным вещам, таким как (большинство) библиотек графического интерфейса пользователя и статических переменных в нескольких потоках, у вас все будет хорошо. *

...