18 мая 2011, 22:54
Темы: ruby, ruby1.9, regexp, syntax, bash, mistakes
Предыстория
Не далее как прошлой осенью я писал о том, как изнутри процесса определить, запущен ли он, используя инструмент grep. В комментариях мне посоветовали использовать pidof, но мне не удалось заставить его работать для руби, т.к. поиск происходит по имени запускаемого файла, а в случае руби-скрипта это всегда ruby. Но мне существенно удалось сократить получение списка запущенных процессов с таким же именем. Вместо:
`ps ax | grep #{File.basename(__FILE__)} | grep -v grep`.split("\n").map{ |l| l.strip.split(/\s+/)[0].to_i }.reject{ |pid| pid == Process.pid }
получилось
`pgrep -f #{File.basename(__FILE__)}`.chomp.split(/\s+/).reject{ |pid| pid.to_i == Process.pid }
Довольно часто мне необходимо отфильтровать вывод или содержимое файла хитрее, чем просто поиск по регулярному выражению. Поскольку мне очень нравится руби, и, как неоднократно писалось в этом блоге, я пытаюсь использовать его везде, где можно, то почему бы снова так не поступить?

Командная строка руби
Руби имеет умеренное количество ключей командной строки. Кратко они описаны в выводе:
ruby --help
Нас в большей степени интересуют ключи -n и -p, которые создают цикл вокруг чтения из пайпа. Ссылка на подробности в конце статьи.
Например, мы хотим посчитать, сколько всего виртуальной памяти занимают все процессы браузера гугл-хром. В качестве источника информации будем использовать вывод команды:
ps axo "%p %z %c"
В которой собраны только необходимые данные (занимаемая виртуальная память и имя процесса без аргументов) и пид (ну а вдруг?). А теперь этот вывод отправим не грепу, а нашему родному руби:
ps axo "%p %z %c" | ruby -nae 'num ||= 0; num += $F[1].to_i if $F[2] =~ /chrome/; END{puts "total chrome virtual memory size #{num} Kbytes"}'
Что это означает? Ключ n означает, что вокруг нашего скрипта есть цикл вида:
while gets(); ... end
Ключ a означает, что вместо переменной $_, куда автоматически попадает результат gets, мы можем использовать $F, который есть суть $_.split. А END содержит блок, который выполняется после цикла.
Ту же магию можно использовать и внутри запускаемых руби-скриптов. Например, если мы хотим найти какое-то слово внутри файла, выделить его цветом и вывести строку с номером, где это слово нашлось, то наш скрипт будет выглядеть вот так (файл look_for):
#!/usr/bin/ruby -n
BEGIN {
unless ARGV.size == 2
puts "Usage: ./look_for <word> <path/to/file>"
exit
end
str = ARGV.shift
}
next unless $_ =~ /#{str}/
printf "%6s%s", $., $_.gsub($&, "\e[31m#{$&}\e[0m")
Теперь, если сделать этот файл запускаемым и запустить его:
./look_for word /in/some/file
То можно увидеть неземную красоту. Кстати, обратите внимание на shift. Без него программа не работает, т.к. gets, который тут за кадром правит бал, пытается воспринимать все аргументы как пути к файлам, из которых непременно нужно что-нибудь прочитать.
Прочие прекрасные применения параметров командной строки руби я предлагаю пытливому читателю подсмотреть в ссылках ниже или найти самостоятельно.
Материалы для самостоятельного изучения
- Полный код статьи на гитхабе.
- Справочник по параметрам командной строки.
- То же, что и выше, но подробнее
- Множество прекрасных примеров (со ссылкой на источник).
23 марта 2011, 18:58
Темы: ruby, ruby1.9, regexp, syntax
Пролог
Что-то большие перерывы в написании статей входят в привычку. Способность некоторых коллег по цеху регулярно выдавать что-нибудь полезное и интересное вызывает уважение.

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

Именованные группы
В регулярных выражениях руби 1.9 появились именованные группы. Вот, как выглядит их элементарное использование:
if /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/ =~ "Vassily Poopkine"
puts [first, last].inspect
end
if md = /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/.match("Vassily Poopkine")
puts [md[:first], md[:last]].inspect
end
То есть мы не только выделяем группу скобками, как обычно, назначая ей тем самым порядковый номер (по номеру открывающей скобки), но и даём имя. И использовать его можно не только в локальных переменных и объекте MatchData, но и в самом регулярном выражении.
Более того, обращение к объявленным группам внутри может быть рекурсивным. Мне сразу же захотелось написать давнишнюю мою задумку о функции, раскрывающей вложенные скобки. Вот так:
str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14}] + 15) + 16 * (17 + 18)"
re = %r{
(?<fill>[0-9+\-*/\s]+){0}
(?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
(?<braces>\{\g<expression>+\}){0}
(?<squarebrackets>\[\g<expression>+\]){0}
(?<parentheses>\(\g<expression>+\)){0}
(?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x
def calculator(str)
if str =~ /\A[0-9+\-*\/\s]+\Z/
eval str
else
raise "Invalid expression: #{str}"
end
end
f =-> s do
if $~[:expression] == $~[:fill]
calculator($~[:fill])
else
calculator($~[:brackets][1..-2].gsub(re, &f))
end
end
puts calculator(str.gsub(re, &f))
puts eval(str.gsub(/(?<left>\{|\[)|\}|\]/) { |s| $~[:left] ? "(" : ")" })
Итак, в регулярном выражении присутствует 6 именованных групп: fill (заполнения пространства между скобками), expression (выражение, содержащее одни или ни одних нераскрытых скобок), braces (фигурные скобки), squarebrackets (квадратные скобки), parentheses (круглые скобки), brackets (любые скобки). Как видите, выражение описывается через скобки, а скобки через выражение.
Для проверки правильности расчёта, используем обычный eval, заменив все скобки на круглые.

Сделав этот пример, я был доволен, как стадо слонов, но потом решил проверить, а что будет, если скобки расставлены неправильно?
str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14]} + 15) + 16 * (17 + 18)"
re = %r{
(?<fill>[0-9+\-*/\s]+){0}
(?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
(?<braces>\{\g<expression>+\}){0}
(?<squarebrackets>\[\g<expression>+\]){0}
(?<parentheses>\(\g<expression>+\)){0}
(?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x
str =~ re
И я не смог дождаться завершения работы оператора =~ для такого длинного выражения. Это, конечно, неприятно. В причины я вникал не особо, но похоже, это связано с поведением недетерминированной машины Тьюринга. По крайней мере вот ответ на похожую проблему. Для нас это всего лишь означает, что проверять правильность расстановки скобок нужно отдельно и другим способом. Чем я предлагаю заняться пытливому читателю самостоятельно.
Материалы для самостоятельного изучения
- Исходный код статьи.
- Новый синтаксис и прочие вкусняшки в руби 1.9. Для тех, кто заметил =->.
- Глобальные переменные с непонятными именами. Для тех, кто заметил $~.
- Ещё немного базовых приёмов в регулярных выражениях руби.
07 апреля 2010, 12:59
Темы: ruby, ruby1.9, syntax, security

Введение
Совершенно не по работе заинтересовался переменной $SAFE и её ролью в жизни современного разработчика. Оказалось, что всё нужно проверять самому.
Нежная безопасность
Для тестирования возможностей на разных уровнях безопасности собрал небольшую программку. Она просит ввести имя файла, делая строковую переменную небезопасной, и пытается что-то с этим всем сделать.
print "child: "
child = gets.chomp
puts "child tainted: #{child.tainted?}"
(0..4).to_a.each do |i|
puts "SAFE: #{i}"
$a = "safe"
th = Thread.new do
$SAFE = i
child_copy = child.dup
Thread.current[:out] = ""
begin
load child_copy
Thread.current[:out] += "1. Child loaded\n"
rescue SecurityError => e
Thread.current[:out] += "1. Security error: #{e.to_s}\n"
begin
child_copy.untaint
load child_copy
Thread.current[:out] += "2. Child untainted and loaded\n"
rescue SecurityError => e
Thread.current[:out] += "2. Security error: #{e.to_s}\n"
begin
Thread.current[:out] += "3. Read from file '#{child_copy}': '#{File.read(child_copy)}'\n"
rescue SecurityError => e
Thread.current[:out] += "3. Security error: #{e.to_s}\n"
begin
Thread.current[:out] += "4. Read from untainted file: '#{File.read("child.rb")}'\n"
rescue SecurityError => e
Thread.current[:out] += "4. Security error: #{e.to_s}\n"
end
end
end
end
begin
$a = "modified"
Thread.current[:out] += "5. Global variable modified: $a = '#{$a}'\n"
rescue SecurityError => e
Thread.current[:out] += "5. Security error: #{e.to_s}\n"
end
begin
Dir.mkdir "test"
Thread.current[:out] += "6. Created directory 'test': #{File.exist?("test")}\n"
Dir.rmdir "test"
rescue SecurityError => e
Thread.current[:out] += "6. Security error: #{e.to_s}\n"
end
begin
Thread.current[:out] += "7. Dir glob: #{Dir.glob(File.join("..", "*")).inspect}\n"
rescue SecurityError => e
Thread.current[:out] += "7. Security error: #{e.to_s}\n"
end
begin
Thread.current[:out] += "8. System ls output: '#{`ls`.chomp}'"
rescue SecurityError => e
Thread.current[:out] += "8. Security error: #{e.to_s}\n"
end
end
th.join
puts "Global variable: $a = '#{$a}'"
puts th[:out] if th[:out]
end
Конструкция со Thread.current[:out] используется потому, что для $SAFE >= 4 нельзя ничего писать ни в какие устройства вывода.
Вроде бы всё логично. Первый уровень годится для умеренного карантина внешних данных. При желании их можно и расколдовать. Второй уровень запрещает изменения в файловой системе. Третий уровень похож на осаду с постоянным подозрением на шпионаж. Все созданные объекты считаются небезопасными. А четвёртый уровень это самое близкое к песочнице (sandbox) в руби, что что есть.
Кстати, когда ещё github работал как репозиторий библиотек, спецификация gemspec выполнялась там под $SAFE = 3. Для разработчиков это выливалось в то, что нужно было перечислять все файлы своей библиотеки вручную вместо использования какого-нибудь листинга.
Суровый гайдлайн
Конечно же, только использование $SAFE не убережёт от действительно настойчивой атаки или блокирующего кода. Например:
Thread.new do
$SAFE = 2
class String
def ==(other_string)
true
end
end
end.join
puts "string modified: #{'a' == 'b'}"
И это на втором уровне! А на третьем открыть класс тоже можно, но вызов перегруженного оператора будет вызывать SecurityError.
На сегодняшний момент эту концепцию безопасности можно считать сырой. Актуальное поведение руби 1.8 слегка отклоняется от описаний, что я нашёл. Поведение в 1.9 изменилось, но подробно нигде не описано (я не нашёл).
Это не значит, что этой переменной нет применения в жизни прогрессивного человечества. Адекватное текущему состоянию применение это гайдлайн при разработке. Руководство для программистов, которое само следит за своим исполнением. Жестковато, но зато действенно. :)
Материалы для самостоятельного изучения
- Код примеров в статье на github
- Старая, но самая подробная документация по $SAFE
- Просто дополнительно: шпаргалка по руби
15 декабря 2009, 13:46
Темы: ruby, ruby1.9, bash
Введение
Как молодой язык с неутверждённой спецификацией, руби переживает подростковую болезнь, через которую большинство известных языков уже прошли. Есть новая более быстрая версия, на которую уже стоит переходить, но уже много написано на предыдущей, и так боязно всё ломать...
Поэтому необходимо найти удобный для себя способ (а лучше несколько) чтобы начать использовать руби 1.9.
Постановка задачи
Сейчас практически панацеей для использования более одной версии руби является rvm. Очень удобно в использовании, полностью прозрачно, и позволяет иметь разные версии руби в разных окнах терминала.
Но недавно мне понадобилось скомпилировать wxRuby под свою систему (kubuntu 9.10 amd64), и rvm не справилась с этой задачей. По какой-то причине в момент компилляции были недоступны заголовки руби. Поэтому я решил поставить две версии руби более явно: одна системная (1.8.7) и одна в папке /opt (1.9.1). Причем все команды, связанные с руби 1.9 будут вызываться с суффиксом: ruby1.9, irb1.9, gem1.9, rake1.9.
Возможно, подобных инструкций уже полно, но мне будет удобнее, если я точно буду знать, где находится одна из них :) При всём этом, конечно, rvm продолжает работать. Мы никак ему не помешаем.
Решение
Сначала нужно поставить новый readline. Без него, когда мы будем использовать irb1.9, мы не сможем наслаждаться доступом к истории с помощью стрелок вверх-вниз и перемещаться по введенному тексту с помощью стрелок в стороны.
sudo apt-get install libreadline5-dev
Теперь хорошо бы вписать пути в наше окружение. В конце ~/.bashrc добавим:
export PATH=$PATH:/opt/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/lib
Теперь следует скачать и разархивировать последнюю версию руби. Зайдя в папку скомпилировать и установить:
./configure --prefix=/opt --enable-shared --program-suffix=1.9
make
sudo make install
Теперь у нас есть две отдельных установки руби и сопутствующих инструментов. Единственное, что обе установки используют общие конфигурационные файлы: ~/.gemrc, ~/.irbrc и т.п., что вполне удобно. Также для обеих систем общей директорией джемов будет ~/.gem, куда будут устанавливаться библиотеки, запусти мы их установку без sudo (в случае с sudo, конечно же, директории установки различаются).
Так же я не нашёл быстрого способа добавить /opt/bin в переменную PATH для sudo. Поэтому в таких случаях пока использую полный путь. Например, первая команда, которую следует выполнить:
sudo /opt/bin/gem1.9 update --system
Потому что в пакете с руби идёт версия 1.3.1, а настоящие пацаны уже во всю используют 1.3.5.
Материалы для самостоятельного изучения
- Всё об установке нескольких версий руби на одной системе
- Проект «Используй руби 1.9 или вали!»
09 декабря 2009, 16:43
Темы: ruby, ruby1.9, syntax, extension
Введение
Сегодня будет блиц-молния, которой место, скорее в твиттере. Однако, мне необходимо развеять тучи, которые я сам же и нагнал. :)
Постановка задачи
Не так давно я писал о том, что при создании руби-оболочки вокруг библиотеки на си, легко получить код, который блокирует все потоки приложения. Можно ли с этим что-то сделать?
Решение
Если мы имеем чуть больше контроля над участками кода на си, которые выполняются долго (обычно в цикле), то можно добавить спасительный код в тело цикла:
if (!rb_thread_alone()) rb_thread_schedule();
Это позволит каждый раз при очередном витке передавать управление соседним потокам, если они есть, и сделает приложение более отзывчивым.
То есть, если мы сделаем точную копию функции из той статьи и добавим наш спасительный код внутрь for, то в выводе такой программы:
# coding: utf-8
require "block_thread.so"
t1 = Thread.new do
10.times { |i| puts i; sleep 0.1 }
end
t2 = Thread.new do
puts "Блокируем"
BlockThread.cycle
puts "Разблокируем"
end
t3 = Thread.new do
puts "Стараемся не блокировать"
BlockThread.cycle_with_schedule
puts "Закончили стараться"
end
t1.join
t2.join
t3.join
Будет картина гораздо приятнее:
Блокируем
0
Стараемся не блокировать
Разблокируем
1
2
3
4
5
Закончили стараться
6
7
8
9
Заметьте, кстати, что два лишних вывода между «Блокируем» и «Разблокируем» благодаря передаче управления соседнимпотокам между cycle и puts.
Понятно, что соседние потоки тормозятся. Но теперь не на всё время, а только на исполнение одного витка цикла.
Материалы для самостоятельного изучения
Полный код статьи на github
11 ноября 2009, 20:46
Темы: ruby, ruby1.9, syntax, regexp
Постановка задачи
Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.
Основа неравенств в руби
Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль Comparable:
class MyComp
attr :value
include Comparable
def initialize(val)
@value = val
end
def <=>(other)
@value <=> other.value
end
end
v1 = MyComp.new(1)
v2 = MyComp.new(2)
puts v1 < v2 # > true
puts v1 <= v2 # > true
puts v1 > v2 # > false
puts v1 >= v2 # > false
puts v1 == v2 # > false
Сам метод можно было бы описать как «возвращает -1, 0 или 1 в зависимости от того, меньше равен или больше объект, чей метод вызывается в сравнении с объектом переданным в качестве параметра». Но на самом деле, скорее, наоборот понятия «больше», «меньше» и «равен» определяются исходя из работы <=>.
Далее всё понятно и более ли менее очевидно для чисел, массивов и строк. Но есть и интересная особенность.
Сравнение модулей и классов
Сравнение для модулей и классов определено таким образом, что в результате мы знаем направление наследования или включение одного модуля другим:
module T1
end
module T2
include T1
end
T3 = T1
class C1
end
class C2 < C1
end
C3 = C1
puts "T1 <=> T2: #{(T1 <=> T2).inspect}" # > 1
puts "T1 <=> T3: #{(T1 <=> T3).inspect}" # > 0
puts "C1 <=> C2: #{(C1 <=> C2).inspect}" # > 1
puts "C1 <=> C3: #{(C1 <=> C3).inspect}" # > 0
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > nil
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > nil
C3.send(:include, T1)
puts "после включения"
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > -1
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > 1
Наследник или модуль, который включает другой модуль, меньше, чем родитель или включаемый модуль. Это видно даже из синтаксиса наследования.
Равенство
Существует три метода равенства: ==, eql?, equal?. Последний из которых никогда не следует переопределять, т.к. он отвечает за идентичность. Первые же два обычно работают одинаково. Канонический пример различия из документации:
3 == 3.0 # > true
3.eql? 3.0 # > false
Что лишь свидетельствует о том, что == проводит конвертацию чисел перед сравнением. Обычно == соответствует случаю, когда <=> возвращает 0.
Сравнение case...when
Все мы знаем, что в case...when оператор сравнения это ===. В большинстве случаев он эквивалентен равенству из предыдущего параграфа. Но если равенство симметрично
(a.==(b)) == (b.==(a))
И если это не так, то это можно считать ошибкой. То === вовсе не обязано таковым быть. Нужно помнить, что в конструкции case...when вызывается метод сравнения объекта, стоящего после when, а в качестве параметра ему передаётся объект, стоящий после case:
puts String === "строка" # > true
puts "строка" === String # > false
puts /ок/ === "строка" # > true
puts "строка" === /ок/ # > false
puts (1..10) === 5 # > true
puts 5 === (1..10) # > false
Материалы для самостоятельного изучения
- Полный код статьи на github
- Что нужно помнить, создавая свой объект руби
28 октября 2009, 13:03
Темы: ruby, daemon, ruby1.9
Введение
Некоторое время назад я писал о создании подпроцессов на руби. В числе прочего один из вопросов был об общении между собой демона и родительского процесса. Об одном из методов пойдёт речь сегодня
Постановка задачи
Не только программисты знают, что важна цель коммуникации. :) Если цель общения между основным процессом и демоном в том, чтобы вызывать методы на объектах друг друга, до давайте на этом и сосредоточимся.
Решение: DRb
Для удалённого обращения с объектами существует стандартная руби-библиотека dRuby, в которой находится модуль DRb, который мы и будем использовать. Ничего устанавливать не нужно. Согласно документации, совершенно прозрачным образом можно вызвать методы на удалённом объекте даже на другой машине. Объекты и ссылки на них передаются в формате Marshal.
Ну, довольно теории! Перейдём к практике. Для эмуляции параллельных процессов (возможно на разных машинах (!)) мы будем использовать два окна терминала. В одном запустим server.rb:
# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"
class RemoteObject
def remote_method_with_param(param)
puts "вызван метод на сервере с параметром #{param.inspect}"
case param.class.to_s
when "String"
puts "параметр типа строка"
param.reverse!
when "Array"
puts "параметр типа массив"
param.shift
else
puts "параметр оставшегося типа"
param.do_smth
end
end
end
$SAFE = 1 # Запретить eval() и eval-оподобные вызовы
DRb.start_service("druby://localhost:45678", RemoteObject.new)
DRb.thread.join
Здесь мы используем банальный Thread#join, чтобы при необходимости просто прервать выполнение. Но те, кто читал предыдущую статью, знают, что в это время можно делать что угодно и следить за потоком dRuby отдельно.
В другом терминале запустим клиентский код client.rb:
# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"
class MyString
def initialize(str)
@string = str
end
def do_smth
@string.reverse!
end
def inspect
"<#{@string}>"
end
end
rem_o = DRbObject.new_with_uri("druby://localhost:45678")
["строка", ["котик", "пёсик", "слоник"], MyString.new("суперстрока")].each do |obj|
puts "Вызов метода вернул: #{rem_o.remote_method_with_param(obj).inspect}"
puts "Параметр после вызова: #{obj.inspect}"
end
Вывод в терминалы будет следующий (я использую вывод для версии руби 1.9.1, потому что он нормально переворачивает кириллическую строку без колдовства) для сервера:
вызван метод на сервере с параметром "строка"
параметр типа строка
вызван метод на сервере с параметром ["котик", "пёсик", "слоник"]
параметр типа массив
вызван метод на сервере с параметром #<DRb::DRbUnknown:0x00000001248910 @name="MyString", @buf="\x04\bo:\rMyString\x06:\f@stringI\"\e\xD1\x81\xD1\x83\xD0\xBF\xD0\xB5\xD1\x80\xD1\x81\xD1\x82\xD1\x80\xD0\xBE\xD0\xBA\xD0\xB0\x06:\rencoding\"\nUTF-8">
параметр оставшегося типа
Клиент же упадёт с ошибкой:
Вызов метода вернул: "акортс"
Параметр после вызова: "строка"
Вызов метода вернул: "котик"
Параметр после вызова: ["котик", "пёсик", "слоник"]
(druby://localhost:45678) server.rb:17:in `remote_method_with_param': undefined method `do_smth' for #<DRb::DRbUnknown:0x00000001248910> (NoMethodError)
.....
Что, безусловно, прекрасно. Прекрасно, что упал не сервер. :) Понятно, что он не знает ничего про этот объект и не знает, как с ним обращаться.
Как видно из вывода, объекты передаются в виде копий. Нашим же третьим, самодельным объектом, мы можем исследовать две возможности: таки передавать копию объекта или передавать лишь ссылку на него, чтобы вызовы выполнялись на клиентской копии. Для первой возможности достаточно вынести определение класса в общедоступное для клиента и сервера место common.rb:
# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"
REM_URI = "druby://localhost:45678"
class MyStringCopied
def initialize(str)
@string = str
end
def do_smth
@string.reverse!
self
end
def inspect
"<<#{@string}>>"
end
end
class MyStringSingle
include DRb::DRbUndumped # это ключ :)
def initialize(str)
@string = str
end
def do_smth
@string.reverse!
self
end
def inspect
"<#{@string}>"
end
end
Добавим require "common.rb" в серверный код, а клиентский преобразится до такого:
# coding: utf-8
require "common"
rem_o = DRbObject.new_with_uri(REM_URI)
DRb.start_service # Это нужно для объекта, который не копируется при передаче
["строка",
["котик", "пёсик", "слоник"],
MyStringCopied.new("суперстрока"),
MyStringSingle.new("суперстрока без копий")].each do |obj|
puts "Вызов метода вернул: #{rem_o.remote_method_with_param(obj).inspect}"
puts "Параметр после вызова: #{obj.inspect}"
end
Как видно, мы сразу позаботились и о второй возможности, создав для неё ещё один класс. Секрет заключается во включении модуля DRb::DRbUndumped и старте ещё одного серверного процесса на клиенте (для вызовов методов объектов клиента удалённо) Клиентский вывод теперь выглядит так:
Вызов метода вернул: "акортс"
Параметр после вызова: "строка"
Вызов метода вернул: "котик"
Параметр после вызова: ["котик", "пёсик", "слоник"]
Вызов метода вернул: <<акортсрепус>>
Параметр после вызова: <<суперстрока>>
Вызов метода вернул: #<DRb::DRbObject:0x000000012588c8 @uri="druby://127.0.1.1:43998", @ref=9631244>
Параметр после вызова: <йипок зеб акортсрепус>
Если немножко почитать, и разобраться, какие объекты можно и нужно «маршализировать», а какие нельзя или не нужно, то получается вполне себе прекрасный инструмент. Который, повторюсь, входит в стандартную библиотеку и не требует никаких внешних зависимостей.
Материалы для самостоятельного изучения
- Полный код статьи на github
- Документация по DRb (rdoc)
- Документация по Marshal
22 октября 2009, 00:48
Темы: ruby, extension, syntax, daemon, ruby1.9
Введение
Сначала я расскажу, почему на сегодняшний день я не очень много работаю с подпроцессами на базе Thread, предпочитая им Kernel.fork. А потом покажу простой способ следить за потоками при работе приложения.
На текущий момент, основная проблема потоков это «ненастоящее» распределение ресурсов. Все потоки руби на самом деле находятся в одном системном потоке, который по очереди передаёт им управление. Это влечёт за собой полтора следствия.
Зависание
Когда имеешь дело с внешним оборудованием, сторонними библиотеками и серийными портами, зависание потока может случиться на самом низком уровне. Это можно симулировать небольшой программой на си block_thread.c:
#include <ruby.h>
VALUE rb_mBlockThread;
/*
* call-seq:
* BlockThread::cycle(interval=5)
*
* Блокирует текущий поток на <code>interval</code> секунд.
*
*/
VALUE bt_cycle(int argc, VALUE *argv, VALUE self) {
int i, max;
max = 5;
if (argc == 1) {
max = FIX2INT(argv[0]);
} else if (argc > 1) {
rb_raise(rb_eArgError, "Неправильное количество аргументов (%d вместо 0 или 1)");
}
for (i=0; i<max; i++) {
sleep(1);
}
return Qnil;
}
void Init_block_thread() {
/*
* Модуль содержит методы для демонстрации работы потока
*/
rb_mBlockThread = rb_define_module("BlockThread");
rb_define_module_function(rb_mBlockThread, "cycle", bt_cycle, -1);
}
Если вы никогда не расширяли руби с помощью си, поясню, что в этой программе мы создаём модуль BlockTread, в котором создаём метода класса cycle, который указанное число раз (по умолчанию 5) в цикле ждёт одну секунду. Напишем extconf.rb:
require "mkmf"
create_makefile("block_thread")
И программу на руби, в которой будут два потока, один из которых мы заблокируем на низком уровне block_threads.rb:
# coding: utf-8
require "block_thread.so"
t1 = Thread.new do
10.times { |i| puts i; sleep 0.1 }
end
t2 = Thread.new do
puts "Блокируем"
BlockThread.cycle
puts "Разблокируем"
end
t1.join
t2.join
Скомпилируем и запустим:
ruby extconf.rb
make
ruby block_threads.rb
И что же мы видим? Мы видим, как все потоки, включая основной, блокируются на пять секунд (или любое число секунд, которое мы укажем) И даже ctrl + c не в силах нам помочь. Помогает только ctrl + z и потом killall ...
В случае же с Kernel.fork, процессы действительно равномерно делят между собой ресурсы, и один подпроцесс не способен заблокировать всё.
Синхронизация
Я говорил про полторы проблемы. Об одной уже рассказал, а вторая известна давно попробуйте выполнить следующий код:
# coding: utf-8
$cnt = 0
t1 = Thread.new do
100000.times { $cnt += 1 }
end
t2 = Thread.new do
100000.times { $cnt += 1 }
end
t1.join
t2.join
puts "Without sync: #{$cnt}"
Если вы не используете руби 1.9, то вы получите неожиданный и каждый раз разный результат. Всё дело в том, что переключение между потоками происходит между элементарными операциями, а += состоит из трёх элементарных операций: достать значение, прибавить к нему число, записать значение. Чтобы этого не произошло, нужно либо использовать синхронизацию с помощью Mutex, либо руби 1.9. Ссылка на полный код для этой статьи в конце, т.к. я спешу перейти к более интересной части. :)
Слежение за потоками с помощью менеджера ThreadsWait
Совершенно недавно открыл для себя интересный способ следить за статусом пакетов в блокирующей и неблокирующей манере:
# coding: utf-8
require "thwait"
t1 = Thread.new do
10.times { |i| puts "поток 1 тик #{i}"; sleep 0.5 }
end
t2 = Thread.new do
10.times { |i| puts "поток 2 тик #{i}"; sleep 0.7 }
end
tw = ThreadsWait.new t1, t2
t3 = Thread.new do
10.times { |i| puts "поток 3 тик #{i}"; sleep 0.3 }
end
run = true
tw.join_nowait t3
while run do
begin
# Неблокирующее ожидание
puts "Закончил работу #{tw.next_wait(true).inspect }"
run = false
rescue ThreadsWait::ErrNoFinishedThread
puts "Ожидаем окончания работы одного из потоков"
sleep 0.5
end
end
# Блокирующее ожидание
tw.all_waits do |t|
puts "Закончил работу #{t.inspect}"
end
По-моему, весьма удобно, если вам нужно не просто ожидать окончания работы потоков, но ещё и делать что-то при этом.
Материалы для самостоятельного изучения
- Полный код статьи на github
- Документация ThreadsWait
- Толковая статья о многопотоковости и процессах в руби