LE Blog

Инженер с поэтической душой

01.12.2009 firtree_right Делаем quake-like консоль в любом DE на базе XServer (KDE, GNOME, XFCE, ...)

Недавно написал гайд по производству самопальной quake-like консоли под Openbox. А перед этим вышла новая версия Kubuntu 9.10, в которой моя любимая quake-like консоль yakuake стала притормаживать при открытии. Несильно, но заметно, чтобы надоесть к концу дня. А вновь испробованная quake-like консоль tilda очень некрасиво скроллит текст. Возможно, обе эти проблемы связаны со слабенькой видеокартой на работе, но не менять же её из-за консоли?

Почесав репу, я написал свой способ организовать вывод консоли по F12, который подходит для любого Desktop Environment, работающего на «иксах», а значит почти любого.

  1. Устанавливаем три пакета: xdotool, xbindkeys, xbindkeys-config. На убунте это так:
sudo apt-get install xdotool xbindkeys xbindkeys-config
  1. Выбираем свою консоль, которой мы хотим управлять по горячей клавише. И настраиваем для неё уникальное слово, которое будет выводиться в заголовке окна. Я выбрал konsole. И настроил так, чтобы в заголовке окна всегда было «qk : ». Этот пункт будет разным для всех :)

  2. Сердце гайда — скрипт, использующий xdotool для управления нашим окном. Я как фанат руби, все скрипты на нём и пишу. Вот это лежит у меня в $HOME/.quake-console/quake-console:

#!/usr/bin/ruby

PROG_NAME = "konsole"
WIN_NAME = "qk : "

win_id = `xdotool search "#{WIN_NAME}" 2> /dev/null`.split("\n").select{ |l| l =~ /^\d+$/ }.first

if win_id.nil?
system "#{PROG_NAME} &"
else
if `xdotool search --onlyvisible "#{WIN_NAME}"`.include? win_id
if `xdotool getactivewindow`.chomp.strip == win_id
system "xdotool windowunmap #{win_id}"
else
system "xdotool windowraise #{win_id} && xdotool windowfocus #{win_id} && xdotool windowmove #{win_id} 0 0"
end
else
system "xdotool windowmap #{win_id} && xdotool windowfocus #{win_id} && xdotool windowmove #{win_id} 0 0"
end
end

Как видно, если окна с «qk : » в заголовке нет, то запускается моя выбранная консоль. Если оно есть, но не сверху и не в фокусе, то выводим его на верх и в фокус, если оно в фокусе, то прячем, а если спрятано, то показываем и выводим в фокус.

  1. Теперь запускаем xbindkeys, который как раз нужен для привязки горячих клавиш, и конфигурируем его:
xbindkeys --defaults > $HOME/.xbindkeysrc
xbindkeys
xbindkeys-conifg

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

  1. Наслаждение

19.11.2009 firtree_right Пломбир

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

А сегодня написал про тему, на которую съел мозг как минимум трём человекам (Петров, Руди, Лёха), прежде чем четвёртый прислал мне решение, не зная, что я его ищу. Это очень специфичный пост для тех кто в теме про то, как сделать ссылку на правильную библиотеку.

А ещё я сделал жене пломбир!

Birdie sealer

Раз вы заглянули под кат, то скажу вот ещё что. Не так давно пересматривал передачу «гордонкихот», в которой Задорнов и Джигурда троллили Гордона и учёных дядек. Я неоднократно видел, как вспыхивали эмоциональные споры при упоминании этой передачи, подобные тем, что там показаны. Даже как-то наблюдал своих папу и маму за спором на повышенных тонах на эту тему.

Так вот, мне кажется, что основное эмоциональное зерно передачи заключено не в роли славянского языка в мировой лингвистике, а в том, достаточно ли того, что я просто есть, чтобы себя любить, или это надо как-то заслужить?

Это важный вопрос для любого уважающего себя программиста. Так то!

18.11.2009 firtree_right Сборка руби-билиотеки в заданной среде

Постановка задачи

Как разработчику gphoto4ruby мне приходится сталкиваться с особыми задачами. Связано это с тем, что этот gem является оболочкой поверх ещё одной библиотеки. И как у всякой более-менее развитой сторонней библиотеки, у libgphoto2 есть версия, распространяемая через системные репозитории и порты и есть, так сказать, последний писк моды (bleeding edge).

Отсюда вытекает необходимость:

  1. Иметь разные версии библиотеки не конфликтующие между собой, установленные не одной системе,
  2. Компилировать свою руби-библиотеку под любую из версий.

Установка двух gphoto2 :)

Проделаю весь путь с самого начала. Для пущей целостности. Для начала установка из системного репозитория:

sudo apt-get install libgphoto2-2-dev gphoto2
gphoto2 --version

Теперь можно скачать нужную версию и установить её отдельно. Поскольку я в основном использую две версии, то версию из исходников нужно установить в /opt. Предположим, что исходники libgphoto2 и gphoto2 скачаны:

tar zxvf libgphoto2-x.x.x.tar.gz
cd libgphoto2-x.x.x.tar.gz
./configure --prefix=/opt
make
sudo make install
tar zxvf gphoto2-x.x.x.tar.gz
cd gphoto2-x.x.x.tar.gz
./configure --prefix=/opt --with-libgphoto2=/opt
make
sudo make install
/opt/bin/gphoto2 --version

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

Компиляция джема

Если скачать исходник библиотеки, то можно проделать руками то, что делает команда gem install. Для создания Makefile используется утилита mkmf, которая входит в ruby-dev и с которой работает файл extconf.rb. В моём случае последовательность действий установщика такая:

cd ext
ruby extconf.rb
make

Теперь в папке ext мы имеем скомпилированную библиотеку (.so или .bundle в зависимости от системы). Установщик потом копирует её в папку lib, но мы пока остановимся. Мы можем посмотреть, какие другие библиотеки использует эта:

ldd gphoto4ruby.so

По выводу этой команды видно, что используется библиотека установленная из центрального репозитория. Теперь попробуем скомпилировать под версию «по последней моде». Поскольку я написал в extconf.rb

dir_config("gphoto2")

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

ruby extconf.rb --with-gphoto2-dir=/opt
make
ldd gphoto4ruby.so

Но что это? Вывод показывает нам, что библиотека привязалась опять к тому, что установлено из репозиториев, а не тому, что в /opt. То есть компилятор, конечно, находит нужные ему заголовки (*.h), но ничего в них не говорит о том, где искать соответствующие им библиотеки. Об этом ему должны сказать мы:

ruby extconf.rb --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"
make
ldd gphoto4ruby.so

Вуаля!

Теперь, собственно, главное. Как это сделать при установке джема. Чтобы передать ключи для extconf нужно задать их после дополнительного «--»:

sudo gem i gphoto4ruby -- --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"

Вот такой экскурс в жизнь разработчиков библиотек. Как это звучало в школьные времена: «Спэтсыално дла джэма».

Материалы для самостоятельного изучения

Руководство по расширению руби с помощью C (см. главу про extconf.rb)

11.11.2009 firtree_right Сравнения и неравенства в руби

Постановка задачи

Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.

Основа неравенств в руби

Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль 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

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Что нужно помнить, создавая свой объект руби

06.11.2009 firtree_right Политика

Вот, кстати, заметьте! В то время как Майкрософт и его преспешники пишут тролльскую статью про то, что юнит-тесты никому не нужны. Гугл выпускает фреймворк для тестирования JavaScript. Вот более короткое и простое описание от фанатов.

И сразу всё понятно. Будете смеяться над TDD, запомнитесь, как и Майкрософт, за свои глюки.

01.11.2009 firtree_right Дабл

Ну что ж, похоже, моё альтер-эго успокоилось. Писать одновременно в два жжурнала невозможно. Думаю, когда это будет возможно, нужно будет пойти ко врачу. :) Кстати, у меня и бумажный дневник есть.

Так же в моей жизни произошло знаменательное событие. Ко мне в почту упал человек, который пользуется (пытается) нашей (нек4) библиотекой gphoto4ruby. И кроме того, что он мне помог с документацией и ему всё нравится, у него не всё получается, потому что у него Canon. Я что-то, конечно, попробовал сделать удалённо, но. Мне очень нужно для тестирования на недельку какой-нить Canon EOS (40D, 400D, 1000D, 300D, 450D, 5D Mark II). У них, видите ли, есть одна особенность. Нет ли у кого погонять? Могу даже расплатиться деньгами, едой или вязанием крючком.

Ире вчера пришлось два раза ходить в Альфа-Банк, чтобы закрыть там счета. С одного раза не получилось. Моё терпение в их отношении кончилось гораздо раньше, поэтому я уже давно закрыл свои.

Последнее время в блог получается писать раз в две недели. Тема последнего поста — удалённые объекты в руби — так или иначе витала надо мной ещё со времён, когда я был молодым java-программистом-студентом. Но заглянуть в неё плотно и разобраться мне удалось уже только после того, как я сам написал нечто подобное с нуля в программе для съёмок проекта Градоглядъ. Оказалось, что пока я там сам пишу что-то с нуля (но, к моей чести, стандартное решение всё равно неприменимо без значительного допиливания), Лёха во всю строит распределённую систему обработки данных, которая как раз и обращается к объектам на разных машинах, вызывая их методы. И тут он уезжает на две недели и оставляет меня с ней. :) Отличный способ разобраться.

28.10.2009 firtree_right Удалённые вызовы через систему распределённых объектов в руби (dRuby)

Введение

Некоторое время назад я писал о создании подпроцессов на руби. В числе прочего один из вопросов был об общении между собой демона и родительского процесса. Об одном из методов пойдёт речь сегодня

Постановка задачи

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

Решение: 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>
Параметр после вызова: <йипок зеб акортсрепус>

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

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Документация по DRb (rdoc)
  3. Документация по Marshal

25.10.2009 firtree_right Винегрет

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

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

Кстати, Витла себе тоже поставил такую форму обратной связи на ito-sama (такая синяя кнопка слева). Ему теперь тоже можно что-то предлагать.

Кстати, сам проект http://reformal.ru/ с формами обратной связи — это калька с буржуйского проекта http://uservoice.com/. И ничего — все счастливы.

22.10.2009 firtree_right Работа с потоками (Thread) в руби

Введение

Сначала я расскажу, почему на сегодняшний день я не очень много работаю с подпроцессами на базе 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

По-моему, весьма удобно, если вам нужно не просто ожидать окончания работы потоков, но ещё и делать что-то при этом.

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Документация ThreadsWait
  3. Толковая статья о многопотоковости и процессах в руби

09.10.2009 firtree_right Спецыално дла джэма

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

Удивительно, что проект существовал уже какое-то время, но вновь засиял после редизайна, и стремительно набирает популярность. Леденцовый дизайн — моя слабость: хочется лизнуть монитор.