воскресенье, 6 декабря 2009 г.

Многопоточность в C++0x

Поддержка работы с потоками была добавлена в С++. Энтони Уильямс предлагает ознакомиться с новыми возможностями. Перевод одноименной статьи из журнала Overload’93.

Согласованность и многопоточность, именно этим должны обладать фрагменты кода, выполняющиеся параллельно. Если у вас многоядерный процессор или многопроцессорная система, тогда код действительно выполняется параллельно, в обратном случае, многопоточность достигается искусственным способом, посредством возможностей операционной системы поочередно выделять кванты времени на выполнение задач, которые должны выполняться параллельно. Это очень хорошо, но вам нужно как-то указать, какой код выполнять на всех этих потоках.
Высокоуровневые конструкции, как, скажем, параллельные алгоритмы в Intel Treading Building Block [Intel] управляют разделением кода между потоками за вас, в С++0х все по другому. Вместо этого, мы должны сами управлять потоками. Инструментом для этого является std::thread. (За полной документацией и подробностями библиотеки, смотрите мою реализацию [JustThread]).

Выполнение простейшей функции в другом потоке.

Позвольте начать с простой функции в другом потоке, который мы создали как новый объект std::thread и передали в конструктор требуемую функцию. std::thread находится в заголовочном файле , таким образом вначале нужно его подключить. (Листинг 1).

  #include < thread >  
  void my_thread_func()  
  {}  
  int main()  
  {  
  std::thread t(my_thread_func);  
  }
 

Листинг 1.

Если вы скомпилируете и запустите это маленькое приложение, оно не сделает многого: запустит новый поток, тело функции которого пустое. Единственное, что оно сделает это завершится, причем не совсем подобающим образом, потому что мы запускаем поток а потом уничтожаем объект std::thread безо всякого ожидания. Оставляя пока этот момент в стороне, давайте пока добавим в функцию хоть что-то, например вывод на экран «hello» (Листинг 2)

  #include < thread >  
  #include < iostream >  
  void my_thread_func()  
  {  
  std::cout<< "hello" << std::endl;  
  }  
  int main()  
  {  
  std::thread t(my_thread_func);  
  }  


Листинг 2

Если вы скомпилируете а потом запустите это маленькое приложение, что случится? Напечатает ли оно hello как мы этого хотим? Так, на самом деле нельзя дать однозначный ответ. Может напечатать, а может и нет. Я запускал это приложение несколько раз на своем компьютере и результат был непостоянный: иногда надпись «hello» выводилась, с переводом на новую строку, иногда выводилась без новой строки, иногда не выводилось вообще ничего. В чем же дело? Должно ли такое простое приложение вести себя предсказуемо?

Ожидание завершения потока

Ну, на самом деле, нет, это приложение не должно вести себя предсказуемо. Проблема здесь заключается в том, что мы не ждем завершения нашего потока. Когда процесс выполнения доходит до конца функции main() программа завершается, вне зависимости от того, чем заняты другие потоки. Пока планирование потока непредсказуемо, мы не может знать, как далеко зашел другой поток. Он уже мог завершиться, мог уже напечатать “hello”, но еще не выполнить печать std::endl, или мог вообще еще не начать свое выполнение. В любом случае он будет внезапно остановлен при завершении работы приложения.
Если мы хотим что бы сообщение точно вывелось на экран, мы должны убедиться, что наш поток завершился. Мы это делаем с помощью соединения (joining) потоков, путем вызова функции join() члена нашего объекта потока (Листинг 3.)

  #include < thread >  
  #include < iostream >  
 
  void my_thread_func()  
  {  
  std::cout<< "hello" << std::endl;  
  }  
 
  int main()  
  {  
  std::thread t(my_thread_func);  
  t.join();  
  }  


Листинг 3

Теперь, main() будет ожидать, пока поток завершится перед выходом, и код выведет “hello” на экран с добавлением перехода на новую строку каждый раз. Это подчеркивает важный момент: если вы хотите, чтобы поток завершился в конкретной точке вашего кода, вы должны ожидать этого. Как и необходимость в убеждении, что поток завершился, перед тем как выполнение программы завершается, также важно еще это и в том случае, когда поток имеет доступ к локальным переменным: мы хотим, что бы поток завершился до того, как локальные переменные выйдут за пределы области видимости. Также необходимо избегать некорректных завершений программы – не вызвали join() или явно указали, что не собираетесь ожидать поток путем вызова detach(), в этом случае деструктор std::thread вызовет std::terminate().

Выполнение функции объекта в другом потоке

Было бы немного ограниченным, если бы новый поток был способен выполнять только простые функции без аргументов – вся необходимая нам информация в этом случае передавалась бы только через глобальные переменные, что негативно сказалось бы на чистоте кода. К счастью, у нас нет таких ограничений.
В соответствии со стандартной библиотекой C++, вы не ограничены только лишь простыми функциями при запуске потока – конструктор std::thread так же можно вызывать с экземплярами классов, в которых реализован оператор (). Давай переделаем предыдущий пример, в соответствии с этим (Листинг 4) .

  #include < thread >  
  #include < iostream >  
 
  class SayHello  
  {  
  public:  
  void operator()() cons  
  {  
  std::cout<< "hello" << std::endl;  
  }  
  };  
 
  int main()  
  {  
  std::thread t((SayHello()));  
  t.join();  
  }  


Листинг 4

Если вам интересно, для чего нужны дополнительные скобки при вызове конструктора SayHello, то я вам расскажу: что бы избежать проблем при парсинге данного кода компилятором. Без скобок объявление представляет собой вызов функции t, которая принимает указатель на функцию без параметров и возвращающей экземпляр SayHello, и который возвращает объект std::thread, а не объект t типа std::thread. Есть несколько других способов избежать проблемы. Во первых вы можете создать именованную переменную типа SayHello и передать ее в конструктор std::thread:

  int main()  
  {  
  SayHello hello;  
  std::thread t(hello);  
  t.join();  
  }  


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

 int main()  
 {  
  std::thread t=std::thread(SayHello());  
  t.join();  
  }
 

И наконец, можно использовать новшество компилятора C++0x, когда вы можете использовать новый синтаксис инициализации с фигурными скобками вместо обычных:

  int main()  
  {  
  std::thread t{SayHello()};  
  t.join();  
  }  


В данном случае, это абсолютный эквивалент нашему первому примеру с двойными скобками.
Так или иначе, хватит об инициализации. Вне зависимости от того, какой способ вы используете, идея та же: ваш объект-функция (function object) копируется во внутреннее хранилище, доступное для нового потока, и новый поток вызывает ваш operator(). Ваш класс, разумеется, может иметь другие члены с данными или другие члены-функции, и это является способом передачи данных в функцию потока: передать их как аргументы конструктора и сохранить во внутренней переменной класса (Листинг 5).

  #include < thread >  
  #include < iostream >  
  #include < string >  
 
  class Greeting  
  {  
  std::string message;  
  public:  
  explicit Greeting(std::string const& message_):  
  message (message_)  
  {}  
  void operator()() const  
  {  
  std::cout << message << std::endl;  
  }  
  };  
 
  int main()  
  {  
  std::thread t(Greeting("goodbye"));  
  t.join();  
  }
 

Листинг 5

В этом примере, наше сообщение хранится в переменной класса, таким образом, когда экземпляр Greeting копируется в поток, сообщение также копируется, и этот пример выведет на экран не “hello”, а “goodbye”.
Этот пример также демонстрирует способ передачи информации в новый поток вне вызова функции – включает член класса для данных объекта-функции. Если вам подходит способ использование объекта-функции – это идеальное решение, иначе, нам необходим другой способ.

Передача аргументов в функцию потока

Как мы видели ранее, одним из способов передачи параметров функции потока является вариант с членами класса. Хорошо, но необходимости каждый раз писать специальный класс нету; стандартная библиотека предлагает простой метод сделать это с использованием std::bind. Шаблонная функция std::bind принимает переменное число параметров. Первым параметром всегда является функция или вызываемый объект, которой нужны параметры, а оставшиеся аргументы являются параметрами для этой функции. Результатом является объект-функция, которая содержит копии предоставленных аргументов и оператор, который вызывает пользовательскую функцию. Мы можем использовать такой метод для передачи сообщения нашему новому потоку (Listing 6).

  #include < thread >  
  #include < iostream >  
  #include < string >  
  #include < functional >  
  void greeting(std::string const& message)  
  {  
  std::cout << message << std::endl;  
  }  
  int main()  
  {  
  std::thread t(std::bind(greeting,"hi!"));  
  t.join();  
  }  


Листинг 6

Этот код работает хорошо, но на самом деле мы может сделать его лучше – мы можем передать аргументы непосредственно конструктору std::thread и они будут скопированы в локальное хранилище для нового потока и предоставлены функции потока. Таким образом, наш код может принять следующий вид (Листинг 7)

  #include < thread >  
  #include < iostream >  
  #include < string >  
  void greeting(std::string const& message)  
  {  
  std::cout << message << std::endl;  
  }  
  int main()  
  {  
  std::thread t(greeting,"hi!");  
  t.join();  
  }  


Листинг 7

Этот код не только проще, но также он более эффективен, так как аргументы непосредственно копируются в локальное хранилище для потока, в отличие от предыдущего варианта, когда сначала они копировались в объект, генерируемый std::bind, который потом копировался во внутреннее хранилище потока.
Несколько аргументов может быть передано в конструктор std::thread (Листинг 8)

  #include < thread >  
  #include < iostream >  
  void write_sum(int x,int y)  
  {  
  std::cout << x << " + " << y << " =  
  " << (x+y) << std::endl;  
  }  
 
  int main()  
  {  
  std::thread t(write_sum,123,456);  
  t.join();  
  }  


Листинг 8

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

Вызов функции-члена в новом потоке

Что если вы хотите вызвать функцию-член, который не является оператором, описанным выше?
Чтобы запустить новый поток, который вызывает функцию-член существующего объекта, вы просто передаете указатель на эту функцию и указатель на родительский объект в конструктор std::thread (Листинг 9).

  #include < thread >  
  #include < iostream >  
  class SayHello  
  {  
  public:  
  void greeting() const  
  {  
  std::cout << "hello" << std::endl;  
  }  
  };  
  int main()  
  {  
  SayHello x;  
  std::thread t(&SayHello::greeting,&x);  
  t.join();  
  }
 

Листинг 9

Разумеется, вы можете передать дополнительные аргументы в вызываему функцию (Листинг 10).

  #include < thread >  
  #include < iostream >  
  class SayHello  
  {  
  public:  
  void greeting(std::string const& message) const  
  {  
  std::cout << message << std::endl;  
  }  
  };  
  int main()  
  {  
  SayHello x;  
  std::thread t(  
  &SayHello::greeting,&x,"goodbye");  
  t.join();  
  }  


Листинг 10

Сейчас, оба предшествующие примера используют обыкновенный указатель на локальный объект; если вы так поступаете, вам необходимо быть уверенным в том, что объект, на который вы ссылаетесь, будет действительным в течение всего периода жизни потока, иначе возникнут проблемы. Как вариант, можно использовать динамическое создание объекта на куче и использовать «умный» указатель как std::shared_ptr для уверенности в том, что объект будет оставаться действительным на протяжении всего времени жизни потока:

  #include < thread >  
  int main()  
  {  
  std::shared_ptr p(new SayHello);  
  std::thread t(&SayHello::greeting,p,"goodbye");  
  t.join();  
  }
 

Итак, все примеры, с которыми мы имели дело, копировали аргументы и функцию потока во внутреннее хранилище потока, даже, если аргументами были указатели, как в случае с указателем this для члена-функции. Но что если нам необходимо передать ссылку на существующий объект, а не указатель? Это задача для std::ref.

Передача функции и аргументов в поток по ссылке

Предположим у вас есть объект, в котором реализован оператор-функция (operator()) и вы хотите вызвать его в новом потоке. Здесь нюанс заключается в том, что вы хотите вызвать функцию-оператор именно этого конкретного объекта, а не его копии. Вы можете использовать поддержку функции-члена что бы явно вызвать operator(), но это выглядит немного неестественно. Таким образом, это случай для использования std::ref – если x вызываемый объект, то std::ref(x) тоже, таким образом, мы можем передать std::ref(x) как нашу функцию, когда мы запускаем поток (Листинг 11).

  #include < thread >  
  #include < iostream >  
  #include < functional > // for std::ref
 
  class PrintThis  
  {  
  public:  
  void operator()() const  
  {  
  std::cout << "this=" << this << std::endl;  
  }  
  };  
  int main()  
  {  
  PrintThis x;  
  x();  
  std::thread t(std::ref(x));  
  t.join();  
  std::thread t2(x);  
  t2.join();  
  }
 

Листинг 11

В данном случае, функция-оператор выводит на экран адрес объекта. Реальные значения будут отличаться от случая к случаю, но принцип один и тот же: эта маленькая программа выведет три строки. Первый две будут одинаковыми, тогда как третья будет отличаться, так как она представляет собой результат вызова функции-оператора копии x. Вот такой результат можно получить:

  this=0x7fffb08bf7ef  
  this=0x7fffb08bf7ef  
  this=0x42674098


Разумеется, std::ref можно использовать и для других аргументов тоже – следующий код в листинге 12 выведет «x=43».

  #include < thread >  
  #include < iostream >  
  #include < functional >  
 
  void increment(int& i)  
  {  
  ++i;  
  }  
 
  int main()  
  {  
  int x=42;  
  std::thread t(increment,std::ref(x));  
  t.join();  
  std::cout << "x=" << x << std::endl;  
  }  


Листинг 12

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

Защита данных общего пользования с помощью std::mutex

Мы видели, как запустить поток для выполнения задачи «в фоне» и дождаться его завершения. Вы можете выполнить много полезной работы, подобной этой, передавая данные через параметр функции потока, и потом, получая результат, когда поток завершен. Однако, вы так не будете поступать, если вам необходимо взаимодействие между потоками пока они выполняются – совместный доступ к общей памяти для нескольких потоков может вызвать неопределенное поведение, если какой-нибудь поток будет модифицировать данные. Здесь вам необходим какой способ, позволяющий убедиться, что доступ к данным взаимно исключающий, таким образом, один поток может получить доступ к данным в одно время.
Концепция мьютексов проста. Он может быть «заблокирован» или «незаблокирован», и поток пытается заблокировать мьютекс, когда хочет получить доступ к некоторым защищенным данным. Если мьютекс уже заблокирован, тогда все потоки пытающиеся заблокировать мьютекс будут вынуждены ожидать. Как только поток завершил все свои операции с защищенными данными, он освобождает мьютекс и другой поток может его заблокировать. Если вы будете контролировать, что поток всегда блокирует мьютекс, до того как обращается к данным общего пользования, то другие потоки не смогут получить доступ к этим данным, пока мьютекс заблокирован другим потоком. Это предотващает совместный доступ одновременно несколькими потоками, и позволяет избежать неопределенного поведения при доступе к данным. Самый простой мьютекс, который есть в С++ это std::mutex, который описан в заголовчном файле с другими типами мьютексов и классов блокировок.
В std::mutex есть функции для явной блокировки и освобождения, и как правило мьютекс используется для блокировки доступа к конкретному региону в коде. В данном случае удобно использовать шаблон std::lock_guard<>, который подходит для данного сценария. Конструктор блокирует мьютекс, а деструктор освобождает его, таким образом, что бы заблокировать мьютекс для блока кода, необходимо только сконструировать объект std::lock_guard<> как локальную переменную в начале этого блока. Например, для того что бы защитить счетчик общего доступа, можно воспользоваться std::lock_gurad<> , что бы быть уверенным, что мьютекс заблокирован как для приращения, так и для получения его значения. (Листинг 13)

  #include < mutex >  
  std::mutex m;  
  unsigned counter=0;  
  unsigned increment()  
  {  
  std::lock_guard lk(m);  
  return ++counter;  
  }  
  unsigned query()  
  {  
  std::lock_guard lk(m);  
  return counter;  
  }  


Листинг 13

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

Безопасность исключений и мьютексы

Использование std::lock_gurad<> для блокировки мьютекса имеет еще одно преимущество над ручной блокировкой и освобождением, когда речь заходит о безопасности исключений. С ручной блокировкой, вы обязаны быть уверенными в том, что мьютекс освобожден корректно, вне зависимости от того как завершилось выполнение региона кода, который нуждается в мьютексе, включая случай когда выполнение кода завершается из-за возникшего в нем исключения. Предположим на момент, что вместо защиты целочисленной переменной нам необходима защита доступа и добавление данных в std::string. Добавление данные в строку может потребовать выделения памяти, а эта операция в свою очередь может вызвать исключение, если память не может быть выделена. При использовании std::lock_guard<> это не проблема – если возникает исключение, мьютекс освобождается. Чтобы получить такое же поведение для ручной блокировки мы должны использовать блок catch, как показано в Листинге 14.

  #include < mutex >  
  #include < string >  
  std::mutex m;  
  std::string s;  
  void append_with_lock_guard(  
  std::string const& extra)  
  {  
  std::lock_guard lk(m);  
  s+=extra;  
  }  
  void append_with_manual_lock(  
  std::string const& extra)  
  {  
  m.lock();  
  try  
  {  
  s+=extra;  
  m.unlock();  
  }  
  catch(...)  
  {  
  m.unlock();  
  throw;  
  }  
  }
 

Листинг 14

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

Гибкая блокировка с std::unique_lock<>

В то время, как std::lock_guard<> достаточно примитивный и негибкий в использовании, другой шаблонный класс – std::unique_lock<> - более гибкий. В простейшем случае, его использование не отличается от использования std::lock_guard<>. Вы передаете мьютекс в конструктор с целью блокировки, а в деструкторе мьютекс освобождается – но если это действительно все что вам нужно, для ваших задача вполне достаточно std::lock_guard<>. Существует два основных преимущества std::unique_lock<> перед std::lock_guard<>:

  1. Вы можете передавать владение блокировкой между объектами, и
  2. Объект std::unique_lock<> не обязан владеть блокировкой мьютекса, с которым он ассоциирован. 

Давайте разберем каждый из пунктов по очереди, начиная с передачи владения.

Передача владения блокировкой мьютекса между экземплярами std::unique_lock<>

Существует несколько последовательностей передачи владения блокировкой мьютекса между экземплярами std::unique_lock<>: вы можете возвратить блокировку из функции, вы можете сохранить блокировку в стандартном контейнере, и т.д.
Например, вы можете написать простую функцию, которая получает блокировку мьютекса:

  std::unique_lock acquire_lock()  
  {  
  static std::mutex m;  
  return std::unique_lock(m);  
  }  


Возможность передачи владения блокировкой между объектами также обеспечивает простой способ написания классов, которые сами по себе могут перемещаться, но хранить блокировку, это продемонстрировано на листинге 15:

  #include < mutex >  
  #include < utility >  
  class data_to_protect  
  {  
  public:  
  void some_operation();  
  void other_operation();  
  };  
  class data_handle  
  {  
  private:  
  data_to_protect* ptr;  
  std::unique_lock lk;  
  friend data_handle lock_data();  
  data_handle(data_to_protect* ptr_,  
  std::unique_lock lk_):  
  ptr(ptr_),lk(lk_)  
  {}  
 
  public:  
  data_handle(data_handle && other):  
  ptr(other.ptr),lk(std::move(other.lk))  
  {  
  other.ptr=0;  
  }  
  data_handle& operator=(data_handle && other)  
  {  
  if(&other != this)  
  {  
  ptr=other.ptr;  
  lk=std::move(other.lk);  
  other.ptr=0;  
  }  
  return *this;  
  }  
  void do_op()  
  {  
  ptr->some_operation();  
  }  
  void do_other_op()  
  {  
  ptr->other_operation();  
  }  
  };  
 
  data_handle lock_data()  
  {  
  static std::mutex m;  
  static data_to_protect the_data;  
  std::unique_lock lk(m);  
  return data_handle(&the_data,std::move(lk));  
  }  
 
  int main()  
  {  
  data_handle dh=lock_data(); // lock acquired
  dh.do_op(); // lock still held
  dh.do_other_op(); // lock still held
  data_handle dh2;  
  dh2=std::move(dh); // transfer lock to
  // other handle
  dh2.do_op(); // lock still held
  } // lock released  


Листинг 15

В данном случае lock_data() приобретает блокировку мьютекса, предназначенного для защиты данных, а затем передает ее по указателю на данные data_handle. Эта блокировка храниться в data_handle до тех пор, пока обработчик не будет уничтожен, позволяя выполнения операций с данными без освобождения блокировки. Поскольку std::unique_lock<> перемещаемый (movable), достаточно просто сделать перемещаемым и data_handle, что необходимо в случае, когда он возвращается из lock_data.
Хотя возможность передачи владения между объектами полезна, большего внимания заслуживает простота возможности управлять владением блокировки раздельно от времени жизни экземпляра std::unique_lock<>.

Явная блокировка и освобождение мьютекса с помощью std::unique_lock<>

Как вы видели раньше, std::lock_guard<> очень прямолинейно ведет себя по отношению к владению блокировкой – он владеет блокировкой от создания до разрушения, без дополнительных возможностей. std::unique_lock<> немного гибче. Получив блокировку в конструкторе, так же как и в случае с std::lock_guard<>, вы можете:

• Создать экземпляр без ассоциации с мьютексом вовсе (с помощью конструктора по умолчанию);
• Создать экземпляр с ассоциированием ему мьютекса, но оставив мьютекс незаблокированным (с помощью конструктора с отложенной блокировкой);
• Создать экземпляр, который пытается заблокировать мьютекс, но оставляет его незаблокированным, в случае если блокировка не удалась (с конструктором пытающимся применить блокировку);
• Если у вас есть мьютекс, поддерживающий блокировку с таймаутом (как например, std::timed_mutex), то вы можете сконструировать экземпляр, пытающийся применить блокировку в назначенное время, или до какого-то времени, и оставить мьютекс незаблокированным, если заданное время вышло;
• Заблокировать мьютекс, если экземпляр std:unique_lock<> не владеет в данный момент времени блокировкой (с помощью функции lock());
• Попытаться и получить блокировку мьютекса, если экземпляр std:unique_lock<> не владеет в данный момент блокировкой (возможно с таймаутом, если мьютекс имеет такую поддержку)(используя функции try_lock(), try_lock_for() и try_lock_until());
• Освободить мьютекс, если в данный момент времени std::unique_lock<> владеет блокировкой (используя функцию unlock());
• Проверить, владеет ли экземпляр блокировкой (с помощью вызова owns_lock());
• Убрать связь между экземпляром и мьютексом, оставляя мьютекс в своем текущем состоянии (заблокированном или освобожденном) (используя функцию release());
• Передать владение другому экземпляру, как было описано выше.

Как вы видите std::unique_lock<> достаточно гибок: он дает вам полный контроль над подчиненным мьютексом, и удовлетворяет всем требованиям для блокируемого (Lockable) объекта. Вы можете также использовать конструкции вида std::unique_lock<>> если хотите! Однако, несмотря на всю свою гибкость, вам также предоставляется безопасность исключений: если при уничтожении объекта мьютекс все еще заблокирован, он освобождается в деструкторе.

std::unique_lock<> и условные переменные

Вся гибкость std::unique_lock<> может использоваться вместе с std::condition_variable. std::condition_variable предоставляют реализацию условных переменных, которые позволяют потоку ожидать до тех пор, пока он не будет уведомлен, что желаемое условие истинно. Во время ожидания, вы должны указать эти условия экземпляру std::unique_lock<>, который владеет блокировкой мьютекса. Условные переменные используют гибкость std::unique_lock<> для высвобождения мьютекса во время ожидания, а затем блокируют его опять, перед возвращением вызывающему коду. Это делает возможным другим потокам получить доступ к защищенным данным пока поток заблокирован. Полное обсуждение условных переменных – это тема отдельной статьи, поэтому сейчас мы на это и остановимся.

Другие приемы гибкой блокировки

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

  std::mutex m;  
  std::vector strings_to_process;  
  void update_strings()  
  {  
  std::unique_lock lk(m);  
  if(strings_to_process.empty())  
  {  
  lk.unlock();  
  std::vector  
  local_strings=load_strings();  
  lk.lock();  
  strings_to_process.insert(  
  strings_to_process.end(),  
  local_strings.begin(),  
  local_strings.end());  
  }  
  }
 

Листинг 16

Следует отметить, что в данном фрагменте мы полагаемся на то, что функция update_strings(), которая добавляет строки в список выполнятся одним потоком – если она вызывается несколькими потоками одновременно, необходимо быть уверенным и в том, что load_strings() тоже потокобезопасна, и ее поведение соответствует желаемому. Например, если вы хотите, что бы только один поток вызывал load_strings() тогда может потребоваться дополнительная проверка. Как правило, если вы освобождаете мьютекс, вы должны полагать, что когда вы осуществите блокировку опять, защищаемые данные могли быть изменены.

Выводы

В С++0х вы можете управлять потоками с помощью класа std::thread. Существует много способов запустить поток, но есть только один дождать его завершения – join(). Если вы забудете вызвать этот метод библиотека времени исполнения принудительно завершит ваше приложение.
Как только вы создали и запустили на выполнение ваши потоки, вам необходимо быть уверенными, что данные совместного доступа синхронизированы должным образом, и самый простой способ сделать это – использовать мьютекс std::mutex. Наиболее безопасный способ заблокировать мьютекс - это использовать экземпляр std::lock_guard<>, однако вы можете использовать и std::unique_lock<> который предоставляет гораздо большую гибкость.
Синхронизировать данные в C++0x можно не только с помощью мьютексов, есть и другие способы осуществлять блокировку, кроме как использовать std::lock_guard<> и std::unique_lock<>, но им еще предстоит немного подождать.

Ссылки

[Intel] http://www.threadingbuildingblocks.org/ 
[JustThread] http://www.stdthread.co.uk/doc/