Многопоточные вычисления в php: pthreads

Поддержка исключений везде

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

// Если файла нет, функция просто вернет false, и программа продолжит выполняться
$content = file_get_contents('file.txt');
if ($content === false) {
    ... не удалось прочесть файл ...
}

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

set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    // Не выбрасываем исключение если ошибка подавлена с 
    // помощью оператора @
    if (!error_reporting()) {
        return;
    }

    throw new ErrorException($errstr, $errno, , $errfile, $errline);
});

Этот код превращает любые ошибки и предупреждения PHP в исключения. Некоторые современные фреймворки (Slim) включают в себя такой код.

Пример объяснил:

Приведенный выше код проверяет, если адрес электронной почты содержит строку «example» в нем, если он делает, исключение повторно выбрасывается:

  1. Класс кустомексцептион () создается как расширение старого класса исключений. Таким образом, он наследует все методы и свойства из старого класса исключений
  2. Создается функция ErrorMessage (). Эта функция возвращает сообщение об ошибке, если адрес электронной почты является недопустимым
  3. Переменная $email устанавливается в строку, которая является допустимым адресом электронной почты, но содержит строку «example»
  4. Блок «try» содержит еще один блок «try», чтобы сделать возможным повторное выбросить исключение
  5. Исключение запускается, поскольку сообщение содержит строку «example»
  6. Блок «Catch» ловит исключение и повторно бросает «кустомексцептион»
  7. «кустомексцептион» пойман и выводит сообщение об ошибке

Если исключение не перехвачено в текущем блоке try, он будет искать блок catch на «более высоких уровнях».

Что такое исключение в PHP7

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

Пример: Предположим, мы разрабатываем блог и работаем над методами удаления категории. По логике вещей нельзя удалить категорию, в которой есть посты. Здесь нам приходят на помощь исключения. Очень урезанный и простой пример, но отражающий суть:

// Где-то (модель или сервис)
public function delete($id)
{
 $category = Category::find($id);
 // Если категория не найдена - кидаем исключение
 if (!$category) throw new Exception('Page Not Found!');
 // Если в категории есть посты - кидаем исключение
 if (count($category->posts) > 0) throw new Exception('Cannot delete category with posts!');
 // Если всё хорошо - продолжаем выполнение кода
 // Удаляем категорию
}


// В контроллере
public function deleteAction($id)
{
  try {
  // Если метод delete() из модели возвращает true 
    $model->delete($id);
  } catch (Exception $e) {
  	// Если false - ловим брошенное из модели исключение 
    echo $e->getMessage();
    // Или вывести в уведомление через сессию, например
    // Session::set('error', $e->getMessage());
  }
}

Согласитесь, удобно. Вместо того, чтобы просто возвращать в случае, когда срабатывает условие , лучше кинуть исключение и как-то оповестить пользователя о каких-либо возникших исключительных ситуациях. Если же мы просто вернём , то мы сами со временем не сможем понять, что именно там случилось и почему этот метод не работает. Поэтому, я советую всегда пользоваться исключениями, но слишком не увлекаясь этим делом.

Example explained:

  1. The customException() class is created as an extension of the old exception class. This way it inherits all methods and properties from the old exception class
  2. The errorMessage() function is created. This function returns an error message if an e-mail address is invalid
  3. The $email variable is set to a string that is a valid e-mail address, but contains the string «example»
  4. The «try» block contains another «try» block to make it possible to re-throw the exception
  5. The exception is triggered since the e-mail contains the string «example»
  6. The «catch» block catches the exception and re-throws a «customException»
  7. The «customException» is caught and displays an error message

If the exception is not caught in its current «try» block, it will search for a catch block on «higher levels».

Проброс исключения

В примере выше мы использовали для обработки некорректных данных. А что, если в блоке возникнет другая неожиданная ошибка? Например, программная (неопределённая переменная) или какая-то ещё, а не ошибка, связанная с некорректными данными.

Пример:

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

В нашем случае предназначен для выявления ошибок, связанных с некорректными данными. Но по своей природе получает все свои ошибки из . Здесь он получает неожиданную ошибку, но всё также показывает то же самое сообщение . Это неправильно и затрудняет отладку кода.

К счастью, мы можем выяснить, какую ошибку мы получили, например, по её свойству :

Есть простое правило:

Блок должен обрабатывать только те ошибки, которые ему известны, и «пробрасывать» все остальные.

Техника «проброс исключения» выглядит так:

  1. Блок получает все ошибки.
  2. В блоке мы анализируем объект ошибки .
  3. Если мы не знаем как её обработать, тогда делаем .

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

Ошибка в строке из блока «выпадает наружу» и может быть поймана другой внешней конструкцией (если есть), или «убьёт» скрипт.

Таким образом, блок фактически обрабатывает только те ошибки, с которыми он знает, как справляться, и пропускает остальные.

Пример ниже демонстрирует, как такие ошибки могут быть пойманы с помощью ещё одного уровня :

Здесь знает только, как обработать , тогда как внешний блок знает, как обработать всё.

Настройка PHP

В документации сказано, что PHP должен быть скомпилирован с опцией —enable-maintainer-zts. Я не пробовал сам компилировать, вместо этого нашел пакет для Debian, который и установил себе.

Таким образом у меня остался прежний PHP, который запускается из консоли обычным образом, с помощью команды . Соответственно, веб сервер использует его-же. И появился еще один PHP, который можно запускать из консоли через .

После этого можно ставить расширение pthreads.

Вот теперь все. Ну… почти все. Представьте, что вы написали мультипоточный код, а PHP на машине у коллеги не настроен соответствующим образом? Конфуз, не правда ли? Но выход есть.

Выбрасываем исключение

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

if (!file_exists($file)) {
    throw new Exception("Ошибка: файл $file не существует");
}

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

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

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

// Если тут произойдет исключение, оно само завершит программу
// потому у нас нет необходимости проверять результат
$users = loadUsersFromFile($file);

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

Вот простой пример:

function a() 
{
    b();
}

function b()
{
    throw new Exception("Some error happened");
}

// Функция a() вызывает b() которая выбрасывает исключение. Исключение выходит
// из функции b() наверх в функцию a(), выходит из нее и, оказавшись на верхнем 
// уровне, завершает программу
a();

// Эта строчка никогда не будет выполнена
echo "Survived\n"; 

Оборачивание исключений

И, для полноты картины – последняя, самая продвинутая техника по работе с ошибками. Она, впрочем, является стандартной практикой во многих объектно-ориентированных языках.

Цель функции в примере выше – прочитать данные. При чтении могут возникать разные ошибки, не только , но и, возможно, к примеру (неправильное применение функций работы с URI) да и другие.

Код, который вызвал , хотел бы иметь либо результат, либо информацию об ошибке.

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

Обычно внешний код хотел бы работать «на уровень выше», и получать либо результат, либо «ошибку чтения данных», при этом какая именно ошибка произошла – ему неважно. Ну, или, если будет важно, то хотелось бы иметь возможность это узнать, но обычно не требуется

Это важнейший общий подход к проектированию – каждый участок функциональности должен получать информацию на том уровне, который ей необходим.

Мы его видим везде в грамотно построенном коде, но не всегда отдаём себе в этом отчёт.

В данном случае, если при чтении данных происходит ошибка, то мы будем генерировать её в виде объекта , с соответствующим сообщением. А «исходную» ошибку на всякий случай тоже сохраним, присвоим в свойство (англ. – причина).

Выглядит это так:

Этот подход называют «оборачиванием» исключения, поскольку мы берём ошибки «более низкого уровня» и «заворачиваем» их в , которая соответствует текущей задаче.

Обработка исключений

И так, метод, который вызывает метод, в котором в свою очередь может быть брошено исключение, должен сам его обрабатывать. Обработка исключения производится при помощи операторов . Блок кода, который может поймать исключение, располагается после . Блок кода, который обрабатывает исключение, располагается после оператора . В переводе с английского означает «пытаться», что очень точно отражает суть этого оператора, ведь мы пытаемся выполнить блок кода после него, а если не получается то выполняется блок кода после . переводится как «ловить». Он фактически «ловит» сгенерированное исключение. В примере ниже мы ловим исключение из метода класса User из примера выше:

// Где-то ловим исключение и обрабатываем его
try {
  $user = new User();
  $user->setName('John');
  // Случится исключение InvalidArgumentException
	$user->setName('');
  // Случится исключение LengthException
  $user->setName('Jo');
  echo $user->getName();
} catch (Exception $e) {
    echo "Message: {$e->getMessage()}<br>
    Code: {$e->getCode()}<br>
    File: {$e->getFile()}<br>
    Line: {$e->getLine()}";
}

Ловим исключение в блоке :

try {
  // ...

  $config = "config.php";
  if (!file_exists($config)) {
    throw new Exception("Configuration file not found.");
  }

  // ...
} catch (Exception $e) {
  echo $e->getMessage();
  die();
}

Но так делать не рекомендуется ( и на одном уровне). В этом случае проще написать !

Оператор внешне напоминает объявление метода с уточнением типа его аргумента. Когда генерируется исключение, управление передаётся оператору , при этом в качестве аргумента ему передаётся объект типа .

Основное использование исключений

При возникновении исключения код, следующий за ним, не будет выполнен, и PHP попытается найти соответствующий блок catch.

Если исключение не перехвачено, Неустранимая ошибка будет выдана с сообщением «неперехваченное исключение».

Давайте попробуем бросить исключение, не ловя его:

<?php
//create function with an exception
function checkNum($number) {
  if($number>1) {
    throw new Exception(«Value must be 1 or below»);
  }
  return true;
}
//trigger exception
checkNum(2);
?>

Приведенный выше код будет получать ошибку, как это:

Фатальная ошибка: неперехваченное исключение ‘ Exception ‘с сообщением ‘ значение должно быть 1 или ниже ‘ в к:\вебфолдер\тест.ФП: 6Трассировка стека: #0 к:\вебфолдер\тест.ФП (12):чеккнум (28) #1 {Main} брошен в к:\вебфолдер\тест.ФП на линии 6

Проверим

Давайте теперь посмотрим, действительно ли обработка происходит в несколько потоков и оценим выигрыш от использования этого подхода.
Я буду менять значение из примера выше и смотреть, что получается.

Информация о процессоре, на котором запускал тесты

Посмотрим диаграмму загрузки ядер процессора. Тут все соответствует ожиданиям.

А теперь самое главное, ради чего все это. Сравним время выполнения.

$threads Примечание Время выполнения, секунд
PHP без ZTS
1 без pthreads, без polyfill 265.05
1 polyfill 298.26
PHP с ZTS
1 без pthreads, без polyfill 37.65
1 68.58
2 26.18
3 16.87
4 12.96
5 12.57
6 12.07
7 11.78
8 11.62

Из первых двух строк видно, что при использовании polyfill мы потеряли примерно 13% производительности в этом примере, это относительно линейного кода на совсем простом PHP “без всего”.

Далее, PHP с ZTS

Не обращайте внимание на такую большую разницу во времени выполнения в сравнении с PHP без ZTS (37.65 против 265.05 секунд), я не пытался привести к общему знаменателю настройки PHP. В случае без ZTS у меня включен XDebug например

Как видно, при использовании 2-х потоков скорость выполнения программы примерно в 1.5 раза выше, чем в случае с линейным кодом. При использовании 4-х потоков — в 3 раза.

Можно обратить внимание, что хоть процессор и 8-ядерный, время выполнения программы почти не менялось, если использовалось более 4 потоков. Похоже, это связано с тем, что физических ядра у моего процессора 4

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

Блок catch и фильтры исключений

Последнее обновление: 23.10.2018

Определение блока catch

За обработку исключения отвечает блок catch, который может иметь следующие формы:

  • catch
    {
    	// выполняемые инструкции
    }
    

    Обрабатывает любое исключение, которое возникло в блоке try. Выше уже был продемонстрирован пример подобного блока.

  • catch (тип_исключения)
    {
    	// выполняемые инструкции
    }
    

    Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch.

    Например, обработаем только исключения типа DivideByZeroException:

    try
    {
    	int x = 5;
    	int y = x / 0;
    	Console.WriteLine($"Результат: {y}");
    }
    catch(DivideByZeroException)
    {
    	Console.WriteLine("Возникло исключение DivideByZeroException");
    }
    

    Однако если в блоке try возникнут исключения каких-то других типов, отличных от DivideByZeroException, то они не будут обработаны.

  • catch (тип_исключения имя_переменной)
    {
    	// выполняемые инструкции
    }
    

    Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch. А вся информация
    об исключении помещается в переменную данного типа. Например:

    try
    {
    	int x = 5;
    	int y = x / 0;
    	Console.WriteLine($"Результат: {y}");
    }
    catch(DivideByZeroException ex)
    {
    	Console.WriteLine($"Возникло исключение {ex.Message}");
    }
    

    Фактически этот случай аналогичен предыдущему за тем исключением, что здесь используется переменная. В данном случае в переменную , которая представляет тип DivideByZeroException, помещается
    информация о возникшем исключени. И с помощью свойства мы можем получить сообщение об ошибке.

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

Фильтры исключений

Фильтры исключений позволяют обрабатывать исключения в зависимости от определенных условий. Для их применения
после выражения catch идет выражение when, после которого в скобках указывается
условие:

catch when(условие)
{
	
}

В этом случае обработка исключения в блоке catch производится только в том случае, если условие в выражении when истинно.
Например:

int x = 1;
int y = 0;

try
{
    int result = x / y;
}
catch(DivideByZeroException) when (y==0 && x == 0)
{
    Console.WriteLine("y не должен быть равен 0");
}
catch(DivideByZeroException ex)
{
    Console.WriteLine(ex.Message);
}

В данном случае будет выброшено исключение, так как y=0. Здесь два блока catch, и оба они обрабатывают исключения типа
DivideByZeroException, то есть по сути все исключения, генерируемые при делении на ноль. Но поскольку для первого блока указано условие
y == 0 && x == 0, то оно не будет обрабатывать исключение — условие, указанное после оператора when
возвращает false. Поэтому CLR будет дальше искать соответствующие блоки catch далее и для обработки исключения
выберет второй блок catch. В итоге если мы уберем второй блок catch, то исключение вобще не будет обрабатываться.

НазадВперед

Учебный пример, в котором есть примеры использования всех классов исключений:

class Example
{
  protected $author;  
  protected $month;  
  protected $goals = [];
  
  public function exceptions(int $a, int $b): int
  {
    $valid_a = ;
    if (!is_int($a)) {
      throw new InvalidArgumentException("a должно быть целочисленным!");
    }
    if ($a > 5 || !in_array($a, $valid_a, true)) {
      throw new DomainException("a не может быть больше 5");
    }
    
    $c = $this->getByIndex($a);
    if (!is_int($c)) {
      throw new RangeException("c посчитался неправильно!");
    } else {
      return $c;
    }      
  }
  
  private function getByIndex($a)
  {
    return ($a < 100) ? $a + 1 : null;
  }
  
  public function deleteNextGoal()
  {
    if (empty($this->goals)) {
      throw new UnderflowException("Нет цели, чтобы удалить!");
    } elseif (count($this->goals) > 100000) {
      throw new OverflowException("Система не может оперировать больше, чем 100000 целями одновременно!");
    } else {
      array_pop($this->goals);
    }
  }
  
  public function getGoalByIndex($i)
  {
    if (!isset ($this->goals)) {
      throw new OutOfBoundsException("Нет цели с индексом $i"); // легитимные значения известны только во время выполнения
    } else {
      return $this->goals;
    }
  }
  
  public function setPublicationMonth(int $month)
  {
    if ($month < 1 || $month > 12) {
      throw new OutOfRangeException("Месяц должен быть от 1 до 12!"); // легитимные значения известны заранее
    }
    $this->month = $month;
  }
  
  public function setAuthor($author)
  {
    if (mb_convert_case($author, MB_CASE_UPPER) !== $author) {
      throw new InvalidArgumentException("Все буквы имени автора должны быть заглавными");
    } else {
      if (mb_strlen($author) > 255) {
        throw new LengthException("Поле автор не должно быть больше 255 сиволов!");
      } else {
        $this->author = $author;
      }
    }
  }
  
  public function __call(string $name, array $args)
  {
    throw new BadMethodCallException("Метод Example>$name() не существует");
  }
}

Вот и всё. Думаю, материал буде полезен как новичкам, так и более продвинутым программистам. Я постарался систематизировать информацию об исключениях в одной статье.

PHP 7.1

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

Функция с типом возврата или void функция может либо возвращать неявно, либо иметь оператор возврата без значения:

Обычно функция принимает или возвращает или объект, реализующий , который используется с . Однако, поскольку является примитивным типом, а — интерфейсом, в настоящее время нет способа использовать объявление типа для параметра или возвращаемого типа, чтобы указать, что значение является итерируемым.

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

Параметры, объявленные как могут использовать или массив в качестве значения по умолчанию.

Синтаксис квадратных скобок для list()

Создание пользовательского класса исключений

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

Пользовательский класс исключений наследует свойства из класса исключений PHP, и в него можно добавлять пользовательские функции.

Позволяет создать класс исключений:

Новый класс является копией старого класса Exception с добавлением функции ErrorMessage (). Так как это копия старого класса, и он наследует свойства и методы из старого класса, мы можем использовать методы класса Exception, такие как: line () и файл () и Message ().

Добавление функции is_countable()

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

-переменной является массив или объект реализующий интерфейс. Так как при проверке будет использоваться много шаблонного кода, в PHP 7.3 появилась новая функция , проверяющая переменную на… ну… возможность использования с .

Я написал полифил для is__countable(), если вы хотите начать использовать эту возможность уже сейчас.

RFC, обсуждение на Externals.io, реализация

What Is an Exception?

PHP 5 introduced a new error model which allows you to throw and catch exceptions in your application—this is a better way of handling errors than what we had in older versions of PHP. All exceptions are instances of the base class , which we can extend to introduce our own custom exceptions.

It’s important to note here that exception handling is different than error handling. In error handling, we can use the function to set our custom error handling function so that whenever an error is triggered, it calls our custom error handling function. In that way, you can control errors. Generally, however, certain kinds of errors are unrecoverable and stop program execution.

On the other hand, exceptions are thrown deliberately by the code, and it’s expected that they’ll be caught at some point in your application. So we can say that exceptions are recoverable as opposed to certain errors which are unrecoverable. If an exception which is thrown is caught somewhere in your application, program execution continues from the point where the exception was caught. And an exception which is not caught anywhere in your application results in an error, thus halting program execution.

Exception Handling Control Flow

Let’s refer to the following diagram that shows the generic exception handling control flow.

Exceptions can be thrown and caught by using the PHP  and blocks. You are responsible for throwing exceptions when something occurs which is not expected. Let’s quickly go through the basic exception handling flow, as shown in the following pseudo-code.

Most of the time, when you’re dealing with exceptions, you’ll end up using a pattern, as shown in the above snippet. You can also use the block along with the and blocks, but we’ll get back to that later in this article.

The block is the one to use where you suspect that your code may generate an exception. You should always wrap such code using and . 

Throwing an Exception

An exception might be thrown by a function that you call, or you can use the keyword to throw an exception manually. For example, you might validate some input before performing any operation, and throw an exception if the data is not valid.

It’s important to note here that if you throw an exception but you haven’t defined the block which is supposed to handle that exception, it’ll result in a fatal error. So you need to make sure that you always define the block if you’re throwing exceptions in your application.

Once an exception is caught in the block, the object contains the error message which was thrown using the keyword. The  variable in the above example is an instance of the class, so it has access to all methods of that class. In this block, you should define your own exception handling logic—what exactly you want to do with the error you catch.

In the next section, we’ll go through a real-world example to understand how exception handling works.

Как создавать пользовательские исключения

В этом разделе мы обсудим, как вы можете создавать пользовательские исключения в своих приложениях. Фактически, мы расширим пример, который мы только что обсуждали в предыдущем разделе, чтобы продемонстрировать пользовательские исключения.

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

Перейдем к предыдущему примеру, как показано в следующем фрагменте.

Во-первых, мы определили класс , который расширяет основной класс . Теперь он становится нашим настраиваемым классом исключений, и мы можем использовать его, когда хотим выбросить исключение в нашем приложении.

Затем мы использовали ключевое слово для исключения исключений в случае, если файл config.php не существует

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

Первый получает исключения типа . Итак, если генерируемое исключение относится к типу , этот блок будет выполнен. Если тип исключения не соответствует какому-либо конкретному блоку , он будет соответствовать последнему, который должен поймать все генерические сообщения об исключениях.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector