пятница, 16 декабря 2011 г.

Python для параллельных вычислений

XXI век давно уже наступил и параллельные вычисления перестали быть чем-то сложным и недостижимым. Многие платформы и языки имеют средства для реализации распараллеливания и замечательный язык Python не является исключением.
Python предоставляет разнообразные инструменты для реализации многозадачности: многопоточную, многопроцессную и т.н. сопрограммы. Не хотелось бы вдаваться в глубокую теорию и описывать отличия и особенности многопоточной и многопроцессной многозадачностей - всё это есть в книгах, которые без труда находятся в гугле или яндексе. Вкратце стоит лишь отметить, что для математических вычислений лучше всего подходит именно многопроцессная многозадачность, которая реализована в Python благодаря модулю multiprocessing.

Перейдём к практике. Допустим есть такая нехитрая задача:

Найти минимальное натуральное число с суммой цифр 80, которое делится на 1237.

(Это несколько упрощённый аналог задачи с замечательного сайта Диофант)

Наверняка, такую задачу можно решить аналитически, обладая хорошим математическим образованием. Но нас интересует, как решить её с помощью компьютера и Python.
Если применить простой последовательный перебор в одном цикле, решаться она будет долго. Имея многоядерный процессор такую задачу можно легко распараллелить и решать тоже перебором, но параллельно ;).
Поскольку предугадать на каком интервале находится искомое число трудно - поступим следующим образом: создадим функцию поиска, принимающую на входе интервал чисел и выдающую результат. Будем запускать параллельно несколько процессов с этой функцией с последовательными и относительно небольшими интервалами и проверять, найдено ли решение.

Итак, исходный код (использовался Python 2.7.2):

#!/usr/bin/env python/
# -*- coding: utf-8 -*-

import multiprocessing
import time

# очередь для сбора результатов
result_queue = multiprocessing.Queue()

# блокировка
output_lock = multiprocessing.Lock()

def sumdig(v):
    """ Возвращает сумму цифр числа v.
    """
    Sum = 0
    remain = v
    while remain > 10:
        Sum += remain % 10
        remain = remain // 10
    Sum += remain
    return Sum

def worker(num, n1,n2, qres):
    """
    Поиск числа с суммой цифр =80 в заданном диапазоне [n1, n2].
   
    num - номер воркера. Сугубо для того, чтобы отличать их друг от друга
    n1, n2 - диапазон значений
    qres - объект Queue, очередь для накопления результатов
    """
    t1 = time.time()
    x = n1
    res = 0
    while x <= n2:
        sm = sumdig(x)
        if sm == 80:
            res = x
            break
        x += 1237
    t2 = time.time()   
   
    # запросим блокировку для вывода на экран результатов работы воркера
    output_lock.acquire()
   
    print 'worker #%d Res=%d  time: %0.4f ' % (num, res, float(t2-t1)),
    print ' n1=%d, n2=%d' % (n1,n2)

    # отпустим блокировку
    output_lock.release()
   
    if res > 0:
        qres.put_nowait(res)


if __name__ == '__main__':

    # число воркеров принимаем равным числу ядер
    worker_count = multiprocessing.cpu_count()

    start_value = 0
    M = 100000
    while True:
        jobs = []
        for i in xrange(worker_count):
            p = multiprocessing.Process(target=worker,
                                        args=(i, start_value + 1237*M*i + 1237,
                                                 start_value + 1237*M*(i+1),
                                                 result_queue))
            jobs.append(p)
            p.start()

        # ожидаем завершения работы всех воркеров
        for w in jobs:
            w.join()

        # начальное значение для следующей итерации
        start_value = start_value + 1237*M*(worker_count)
        print "start_value=", start_value


        if result_queue.empty():
            print "next iteration..."
        else:
            print "--- THE END ---"
            break

    # из очереди выберем все ответы и найдём самый минимальный
    res = []
    while not result_queue.empty():
        res.append(result_queue.get())

    print 'RESULT=', min(res) 

Некоторые замечания по исходному коду:

1) Каким выбрать число воркеров - заранее предсказать сложно. При малом его количестве - задача будет решаться медленнее. При большом - можно здорово подгрузить свою систему, что повлечет замедление работы не только воркеров, но и прочих процессов - так, например, при тестировании этого примера на 6 и более воркерах (четырёхядерный процессор i3, Windows7 и было ещё запущено много всякого типа Aptana Studio, Bat! и 4 окна Firefox c кучей вкладок) я наблюдал такие эффекты - с заметными задержками открывались программы и перетаскивались окна. Диспетчер задач показывал почти 100% загрузку всех ядер процессора.
    Практика показала, что макс. число воркеров должно выбираться эмпирически, с учётом загрузки процессора и среднего времени работы воркеров (в нашем случае, интервалы поиска чисел не следует задавать слишком длинными) и относительно оптимальным значением явлется число равное числу ядер процессора.

2) Для нормального отображения хода работы, а именно чтобы сообщения не накладывались друг на друга, искажая смысл - нужно блокировать вывод на консоль. Пожалуй, тема блокировок стоит того, чтобы рассмотреть её отдельно.

3) В данном примере, из соображений упрощения, для хранения воркеров задействован обычный список (jobs). Хотя на самом деле в модуле multiprocessing есть реализация пула процессов, обладающая широким набором функций: multiprocessing.Pool().

Как видно из примера - многозадачность, основанная на механизме многопроцессности, не такая уж сложная и страшная штука, а Python вполне себе годен для решения математических задач путём распараллеливания.

среда, 23 ноября 2011 г.

Работаем с оракловыми курсорами в Python с помощью библиотеки cx_Oracle

Курсорная переменная может возвращаться в качестве результата хранимой процедуры, написанной на PL/SQL. Например, вот такой:

create or replace procedure test_cur(
  a number,
  b number,
  p_ret out sys_refcursor)
is
begin
  open p_ret for select a + level-1 v from dual
          connect by level <= b;
end;
Для работы с СУБД Oracle из Python есть весьма хорошая библиотека cx_Oracle . С курсорами работа построена там очень просто - они объявляются как обычные типизированные переменные библиотеки, в данном случае с типом курсора. Всё очень просто, смотрим на примере:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cx_Oracle as db

try:
    conn = db.connect('user', 'password', 'server')
except  db.DatabaseError, err:
    print 'DB Error: ', err
    exit()

cur = conn.cursor()

# так объявляется курсорная переменная
ret = cur.var(db.CURSOR)

# Не забываем, что есть dir(), с чьей помощью можно узнать очень
# много полезного об инстансе курсорной переменной
print 'ret: ', dir(ret)
print 'ret.getvalue(): ', dir(ret.getvalue())
   
cur.execute('''begin test_cur(1, 20, :ret); end; ''', ret=ret)

# описание полей запроса
print ret.getvalue().description

# А вот по этому можно уже пройтись через for ;-)
print ret.getvalue().fetchall()

# не забываем закрывать за собой соединение с Ораклом
conn.close()
Кстати, вот тут лежит неплохой мануал по cx_Oracle на русском языке.

среда, 13 апреля 2011 г.

Ещё один способ "заточки пилы"

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

Для лечения таких симптомов есть масса интересных способов. Ищущий, да найдёт! Недавно я открыл для себя еще один, увлекательный и полезный. Нет, это не онлайн-шахматы или подкидной дурак на мэйл.ру. Это ресурс с несколькими сотнями логических, математических, а также физико-химических задачек.

А открыл я для себя Diofant.ru

Это совместный проект компаний "Интернет-Университет Информационных Технологий (INTUIT.ru)" и издательства "Открытые Системы".
Чтобы решать задачи, регистрироваться не обязательно. Но чтобы узнать правильность ответа и инкрементировать пузомерку рейтинг - зарегистрироваться нужно.

Теперь о том, чем он полезен. Я увидел в нём следующие фишки, которые мне очень понравились.

А. Логические задачки, заставляющие мозг скрипеть, а руки - терзать карандаш и переводить бумагу. Много задач. На логику и математику. Регулярное их решение продлевает жизнь и нефигово "затачивает пилу" логического мышления.

Б. Информатика. Более трех сотен программистских задач. На арифметику, на умение составлять алгоритмы или реализовывать их на языке программирования. Много задач взято с проекта "Эйлер". Что касается выбора средств - тут полная свобода (лично я пользуюсь Python). Нужен только ответ.
Часть задачек решается тупо перебором. Но для некоторых перебор - слишком дорогое удовольствие: задача будет считаться очень, очень долго. Вот тут приходится включать мозг, оптимизировать алгоритм, внимательнее читать условия задачи, применять хитрости, присущие языку, на котором эта задача решается.
Собственно, ради этого всё и затевается. Хочешь - жди неделями, когда найдётся ответ, а хочешь - задумайся, пересмотри алгоритм и может так получиться, что ждать придется не больше минуты. Если есть математическое образование - то да, ждать вообще не придётся ;-)
Ещё я заметил, что с помощью "Диофанта" я воспитываю в себе очень хорошую привычку. А именно, получая некий расчётный ответ в какой-либо задачке, я должен быть уверен на 100% в его правильности. Диофант ошибки не прощает, что сказывается на рейтинге. Поэтому приходится внимательно читать условия и проводить дополнительные проверки.

Резюме такое. Ресурс очень полезный. В качестве источника задач при изучении нового языка программирования и просто для разминки мозгов.

Отчёты отчётами, но "пила" всегда должна быть острой!