D для новичков, часть 2

  • Tutorial
Доброго времени суток, хабр!

Продолжим тематику предыдущей статьи. Здесь будет объяснение таких концепций, как @​safe, @​trusted, pure, nothrow, некоторые моменты, касающиеся ООП.

Весь код по умолчанию является @​system (за некоторым исключением), это значит, что разрешено выполнять низкоуровневые операции (работа с указателями). В таком режиме в D можно делать абсолютно всё то же самое, что и в C/C++. Это так же включает в себя и многие ошибки работы с памятью, которые можно допустить в C/C++. Существует способ избежать большого количества ошибок, если придерживаться некоторых ограничений. По сути, это подмножество языка D, называется оно SafeD и по логике работы с ним больше похоже на Java и C#. Включается такой режим атрибутом @​safe и запрещает в коде все операции, которые могут вызвать undefined behavior.
Ограничения SafeD:
  • нельзя приводить указатель какого-то типа к указателю любого другого типа, отличного от void*
  • нельзя приводить любой тип к указателю
  • нельзя модифицировать значение указателя (значение по указателю модифицировать можно)
  • доступ к полям объединений, содержащих указатели или ссылки на другие типы, запрещён
  • вызов любого @​system кода запрещён
  • можно перехватывать исключения, наследованные только от Exception (Error наследуется от базового Throwable, поэтому ошибки утверждений AssertError нельзя отлавливать)
  • вставки ассемблерного кода запрещены
  • никаких явных приведений (конструкция cast) изменяемых типов к неизменяемым и наоборот
  • никаких явных приведений локальных для потока (обычных) типов к shared и наоборот
  • запрещено взятие адреса локальных переменных и аргументов функции
  • нет доступа к __gshared («по настоящему» глобальным, как в C/C++) переменным

Атрибут @​trusted позволяет использовать system код внутри safe. Но к такой возможности нужно относиться крайне осторожно — тщательней проверять все trusted функции.

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

Атрибут pure указывает на то, что функцией не будут использоваться глобальные или статические изменяемые переменные. Это позволяет компилятору использовать некоторые оптимизации. Из этого правила есть одно исключение — debug блок:
void func() pure
{
    debug writeln( "print from pure" ); // крайне полезная фича при отладке
    ...
}

Для компиляции в debug режиме необходимо указать флаг dmd -debug (тривиально, но всё же).
Так же этот атрибут очень полезен при написании классов и структур (об этом далее).

Атрибут nothrow гарантирует, что функция не будет выбрасывать исключений, наследованных от Exception. Это не запрещает ей выбрасывать исключения Error. По замыслу разработчиков исключения Error являются невосстановимыми, поэтому перехватывать их не имеет смысла. Так же это не запрещает ей вызывать не nothrow функции, если те заключены в try-catch блок.

Все функциональные литералы и шаблонные функции по умолчанию имеют атрибуты @​safe, @​nogc, pure и nothrow, если это возможно. Для автоматического присвоения каждого из атрибутов такими функциями должны выполняться соответствующие условия.

@​disable запрещает вызов функции. Это полезно при написании структур, которые не должны иметь каких-либо функций по умолчанию, например:
struct Foo
{
    @​disable this(); // конструктор по умолчанию
    @​disable this(this); // конструктор копирования, тоже создаётся по умолчанию
    this( int v ){} // нужно определить явный конструктор в этом случае
}
void main()
{
    Foo a; // запрещён конструктор по умолчанию, выведется ошибка при компиляции
    auto b = Foo(3); // вызывается конструктор с параметром
    auto c = b; // запрещён конструктор копирования, выведется ошибка при компиляции
}

Это можно использовать не только со встроенными функциями:
interface A { int bar(); }
class B : A { int bar() { return 3; } }
class C : B { @disable override int bar(); }
void main()
{
    auto bb = new B;
    bb.bar();
    auto cc = new C;
    cc.bar(); // ошибка при компиляции
}

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

Атрибут deprecated выводит предупреждение, полезно при плавной смене api, чтобы пользователи везде убрали вызов такой функции. Этот атрибут принимает строку в качестве сообщения, выводимого при компиляции:
deprecated("because it's old") void oldFunc() {}

Атрибут может по разному применяться к коду: «просто атрибут» применяется к следующему за атрибутом объявления, при использовании фигурных скобок после атрибута он применяется к блоку верхних уровней и внутри классов (внутри функций объявление блоков с атрибутами запрещено, да и не имеет смысла) и с использованием двоеточия, в этом случае применяется до конца файла.
module test;
@​safe:
... // так мы делаем весь модуль безопасным
pure
{
    int somefunc1() {} // и @​safe, и pure
    int somefunc2() nothrow {} // @​safe, pure и nothrow
}


С простыми вещами разобрались. Теперь стоит осветить, видимо, самую неясную тему: shared с immutable со структурами и классами.

Возьмём простой пример: мы хотим организовать очередь сообщений из одного потока в другой, используя при этом собственную структуру данных.
Начнём со структуры. Предположим нам нужны временная метка и некоторое сообщение, при этом структура будет всегда immutable (другие её варианты нам не нужны).
import std.stdio;
import std.traits;
import std.datetime;

// проверка "является ли типом", это стандарный приём, часто используется при метапрограммировании
template isMessage(T) { enum isMessage = is( Unqual!T == _Message ); }

struct _Message
{
    ulong ts;
    immutable(void[]) data; // неизменяемые данные

    @disable this(); // мы не можем создать сообщение без содержания

immutable: // нам нужны только immutable структуры, по этому все методы будут immutable

    this(T)( auto ref const T val )
    {
        static if( isMessage!T )
        {
            // просто копируем поля из сообщения
            ts = val.ts;
            data = val.data;
        }
        else
        {
            // иначе копируем в поле data то что пришло аргументом в конструктор
            static if( isArray!T )
                data = val.idup;
            else static if( is( typeof(val.array) ) ) // в случае, если это range
                data = val.array.idup;
            else static if( !hasUnsharedAliasing!T ) // если это структура данных, не имеющая массивов, делегатов и прочего некопируемого контента
                data = [val].idup;
            else static assert(0, "unsupported type" );
            
            // и берём текущее вермя
            ts = Clock.currAppTick().length;
        }
    }

    // для удобства
    auto as(T)() @property
    {
        static if( isArray!T )
            return cast(T)(data.dup);
        else static if( !hasUnsharedAliasing!T )
            return (cast(T[])(data.dup))[0];
        else static assert(0, "unsupported type" );
    }
}

alias Message = immutable _Message;

// хороший тон писать тест в том же файле, сразу за проверяемой структурой/функцией/классом
/// тройной слеш - документирующий коментарий. Текст теста будет добавлен в документацию как пример использования
unittest
{
    auto a = Message( "hello" );
    auto b = Message( a );
    assert( a.ts == b.ts );
    assert( b.as!string == "hello" );
    auto c = Message( b.data );
    assert( a.ts != c.ts );
    assert( c.as!string == "hello" );
    auto d = a;
    auto e = Message( 3.14 );
    assert( e.as!double == 3.14 );
}

«А почему только immutable?» можете спросить Вы? Тут вопрос неоднозначный. Реально ли Вам нужны мутабельные сообщения в многопоточном программировании (про более серьёзные типы чуть дальше)? Сообщение на то и сообщение, что оно маленькое, «одноразовое». В Rust например все переменные по умолчанию неизменяемые. Это в дальнейшем позволяет избежать лишнего гемороя с синхронизацией, в следствии меньше ошибок и меньше кода. Но если всё-таки нужно. Во-первых, конструктор должен быть чистым (pure) — это позволит создавать любые типы объекта с помощью одного конструктора (в нашем примере мы используем функцию, получающую время, она не является чистой). Во-вторых, придётся отчасти дублировать код методов доступа к объекту. Если конструктор не может быть чистым, то придётся тоже дублировать код, явно указывая случаи его применения. Пример:
struct CrdMessage
{
    ulong code;
    float x, y;

    this( ulong code, float x, float y ) pure // чистый конструктор
    {
        this.code = code;
        this.x = x;
        this.y = y;
    }

    this( in CrdMessage msg ) pure // чистый конструктор
    {
        code = msg.code;
        x = msg.x;
        y = msg.y;
    }

    float sum() const @property { return x+y; } // здесь
    float sum() shared const @property { return x+y; } // здесь
    float sum() immutable @property { return x+y; } // здесь
}

Дублирование можно было бы убрать с помощью mixin template, но это всё не просто так. Если мы не используем shared объекты такой структуры, то можно обойтись только const вариантом метода (immutable объекты будут вызвать именно его). Метод shared необходим, так как мы явно говорим, что объект может быть разделяем, следовательно, мы берём на себя ответственность за синхронизацию. Это значит, что код в примере содержит ошибку, мы никак не учли, что значения могут меняться в другом потоке. Методов const и shared const недостаточно для вызова метода для immutable объекта, так как immutable объект может быть разделён между потоками и система типов не может выбрать какой из методов нужно вызывать (const или shared const). Так же метод const может отличаться от immutable, так как в случае const мы гарантируем неизменность ссылки на объект, а в случае immutable гарантируем неизменность всех полей структуры на протяжении всего времени её жизни, поэтому нам может понадобиться в const методе делать некоторые действия, которые при immutable нет необходимости выполнять (дополнительное копирование например). Такая система типов заставляет задумываться над совершаемыми действиями и быть внимательней при написании разделяемого кода, но по началу может вызвать боль.
Когда-то давно я сам испытал эту боль, но был упёрт и безграмотен, хотел всего и сразу
В итоге у меня получился некоторый код, который работал, так как я хотел (с небольшими оговорками) и с тех пор он, в принципе, не сильно менялся: здесь структура, хранящая нетипизированные данные, здесь сообщение, передаваемое между потоками, использующее структуру хранения данных.

Вернёмся к созданию нашего многопоточного приложения. Реализуем очередь простейшую очередь (не задумываемся об оптимизации выделения памяти).
synchronized class MsgQueue
{
    Message[] data; // массив наших сообщений
    // волшебные методы, позволяющие использовать экземпляр класса в конструкции foreach
    bool empty() { return data.length == 0; }
    Message front() { return data[0]; }
    void popFront() { data = data[1..$]; }
    void put( Message msg ) { data ~= msg; }
}

Да, так всё просто! По сути, всё, что касается структур, можно применить к классам (в плане shared, immutable и тд). Ключевое слово synchronized значит, что класс shared, но synchronized можно использовать только с классами и оно имеет важное отличие от shared. По порядку, как могло бы быть:
class MsgQueue
{
    Message[] data;

    import core.sync.mutex;
    Mutex mutex; // объект синхронизации

    this() shared { mutex = cast(shared Mutex)new Mutex; } // почему-то Mutex не имеет shared конструктора
    ...
    void popFront() shared // 
    {
        synchronized(mutex) // блок синхронизации, критический участок
        {
            data = data[1..$];
        }
    }
    ...
}

Можно не помечать каждый метод атрибутом shared, а сделать весь класс shared. Так же можно использовать сам объект класса MsgQueue (и любого другого) в качестве объекта синхронизации:
shared class MsgQueue
{
    Message[] data;
    ...
    void popFront()
    {
        synchronized(this)
        {
            data = data[1..$];
        }
    }
    ...
}

Объект любого класса может быть объектом синхронизации за счёт того, что от базового класса (Object) каждый объект перенимает объект синхронизации (__monitor), реализующий интерфейс Object.Monitor (Mutex тоже его реализует).

В случае, если мы хотим синхронизировать не блок внутри метода, а весь метод, при этом мы хотим использовать в качесте объекта синхронизации сам экземпляр класса, то мы можем сделать весь метод synchronized:
shared class MsgQueue
{
    Message[] data;
    ...
    void popFront() synchronized { data = data[1..$]; }
    ...
}

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

Надеюсь, мне удалось разъяснить некоторые неочевидные моменты. Опять же, если Вы считаете, что стоит на что-то обрать особое внимание, напишите об этом. Я привёл тут только то, что показалось неочевидным для меня.

Полный текст программы с передачей сообщений
import std.stdio;
import std.traits;
import std.datetime;

// проверка "является ли типом", это стандарный приём, часто используется при метапрограммировании
template isMessage(T) { enum isMessage = is( Unqual!T == _Message ); }

struct _Message
{
    ulong ts;
    immutable(void[]) data; // неизменяемые данные

    @disable this(); // мы не можем создать сообщение без содержания

immutable: // нам нужны только immutable структуры, по этому все методы будут immutable

    this(T)( auto ref const T val )
    {
        static if( isMessage!T )
        {
            // просто копируем поля из сообщения
            ts = val.ts;
            data = val.data;
        }
        else
        {
            // иначе копируем в поле data то что пришло аргументом в конструктор
            static if( isArray!T )
                data = val.idup;
            else static if( is( typeof(val.array) ) ) // в случае, если это range
                data = val.array.idup;
            else static if( !hasUnsharedAliasing!T ) // если это структура данных, не имеющая массивов, делегатов и прочего некопируемого контента
                data = [val].idup;
            else static assert(0, "unsupported type" );
            
            // и берём текущее вермя
            ts = Clock.currAppTick().length;
        }
    }

    // для удобства
    auto as(T)() @property
    {
        static if( isArray!T )
            return cast(T)(data.dup);
        else static if( !hasUnsharedAliasing!T )
            return (cast(T[])(data.dup))[0];
        else static assert(0, "unsupported type" );
    }
}

alias Message = immutable _Message;

synchronized class MsgQueue
{
    Message[] data;

    bool empty() { return data.length == 0; }
    Message front() { return data[0]; }
    void popFront() { data = data[1..$]; }
    void put( Message msg ) { data ~= msg; }
}

unittest
{
    auto mq = new shared MsgQueue;

    mq.put( Message( "hello" ) );
    mq.put( Message( "habr" ) );

    string[] msgs;
    foreach( msg; mq ) msgs ~= msg.as!string;
    assert( msgs == ["hello", "habr"] );
}

void randomsleep(uint min=1,ulong max=100)
{
    import core.thread;
    import std.random;
    Thread.sleep( dur!"msecs"(uniform(min,max)) );
}

import std.string : format;

void sender( shared MsgQueue mq, string name )
{
    scope(exit) writefln( "sender %s finish", name );

    foreach( i; 0 .. 15 )
    {
        mq.put( Message( format( "message #%d from [%s]", i, name ) ) );
        randomsleep;
    }
}

void receiver( shared MsgQueue mq )
{
    uint empty_mq = 0;
    bool start_receive = false;
    scope(exit) writeln( "reciver finish" );

    m: while(true)
    {
        if( mq.empty ) empty_mq++;
        if( empty_mq > 10 && start_receive ) return;

        foreach( msg; mq )
        {
            writefln( "[%012d]: %s", msg.ts, msg.as!string );
            randomsleep;
            start_receive = true;
        }
    }
}

import std.concurrency;

void main()
{
    auto mq = new shared MsgQueue;
    spawn( &receiver, mq );

    foreach( i; 0 .. 10 )
        spawn( &sender, mq, format( "%d", i ) );

    writeln( "main finish" );
}



Так же в стандартной библиотеке D есть реализация «зелёных» потоков (это я на всякий случай), документация на офф.сайте.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 29

  • НЛО прилетело и опубликовало эту надпись здесь
      0
      К сожалению не могу ответить на этот вопрос, я пользуюсь vim, под него есть хороший плагин dutyl, он использует DCD для автодополнения и поиска документации, dub для выяснения путей, dfmt для форматирования кода, DScanner для проверки синтаксиса, то есть по сути полный фарш (правда, dfmt у меня не завёлся ровно).
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Порой мне кажется, что серьёзным препятствием на пути к широкому применению D является именно отсутствие полноценной IDE.
            0
            Не совсем понял проблему. Нужна именно «родная» IDE или достаточно плагина к уже существующей?
              0
              Вот нашел плагин для IDEA, она уж точно полноценная IDE.
              0
              Не пользовался, но оно есть в виде плагина для qtcreator.
          0
          Я использую Sublime Text 3 с плагинами github.com/yazd/DKit (DCD), github.com/economicmodeling/SublimeLinter-dscanner
          Не фонтан, но лучше чем ничего.
            0
            Для винды Visual Studio + VisualD
            Для линукса по всей видимости Eclipse + DDT
              0
              Под виндой пользуюсь Mono-D — очень нравится, но под виндой отладчик с пятой версией Xamarin Studio не работает, только с четвёртой (и вроде разработчик Mono-D не собирается это исправлять в каком-то ближайшем будущем). Для D у меня это основная среда разработки. Иногда также использую Visual-D.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Вы не совсем правильно поняли. Если Вам нужны указатели, просто используйте их как Вам угодно: с арифметикой, приведением одних типов указателей к другим и т.д. SafeD это только подмножество языка, включаемое атрибутом @​safe, то есть Вы намерено декларируете в коде, что хотите его использовать и декларируете участок, где оно должно использоваться. Это может быть либо личным желанием оградить себя от ошибок работы с памятью, либо требованием к качеству ПО.
                  0
                  Прекрасно D работает с указателе. Другое дело, что можно включить режим мешающий «стрелять себе по ногам», в нем с указателями нельзя :)
                  • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Как-то сумбурно, новичок не поймёт. Особенно про многопоточность. Кроме того, тема передачи сообщений не раскрыта. Зачем использовать свой опасный неудобный велосипед, если есть стандартные удобные средства обмена сообщениями?
                    0
                    Новичок в программировании != новичок в D. Я ставил целью разъяснить некоторые моменты людям, знакомым с программированием, но не знакомыми с D. Насчёт велосипедов. Я пробовал организовывать этими стандартными средствами сложные очереди, где одни сообщения должны отрабатываться гарантировано, другие пропускаться по истечению срока, третие идут в обратном направлении и так далее. И понял, что через std.concurrency сложные вещи делать реально сложно. Проще создать несколько очередей (в одном разделяемом объекте) и по ним пускать разные сообщения, а в std.concurrency я не нашёл способа создания нескольких очередей. Соответственно, в простых ситуациях всё что я описал не нужно. Тут же возникает вопрос, чем опасен подход, который я описал и какой конкретно аспект вопроса не расскрыт? Synchronized самостоятельно разруливает синхронизацию каждого метода, immutable объекты нельзя изменять, в разных потока их использовать безопасно, можно это дело сломать, конечно, с помощью cast, но это уже будет на Вашей совести.
                      0
                      И всё же тема сложная, её не уместить в короткую статью.

                      Что же вы такое сложное делали, что сообщений вам не хватило?

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

                          Касательно производительности — подозреваю передача сообщений реализована через CAS, что куда эффективней.
                            0
                            Чтобы получить состояние взаимоблокировки нужно использовать вложенные блоки synchronized, если Вам нужно заблокировать несколько mutex'ов их нужно использовать в одном блоке синхронизации. Использование в одном блоке безопасно, так как действительный порядок блокировки не зависит от порядка написания объектов синхронизации и он всегда будет одинаков. То есть, если в одном блоке сначала блокируется A а затем B, то и в других блоках этот порядок сохранится. Начёт передачи сообщений вы заблуждаетесь через CAS, в std.concurrency используются для этого обычная схема с блокировками. И если честно не совсем представляю как можно для этого использовать CAS, было бы не плохо, если бы Вы привели пример.
                              0
                              Вот именно, что нужно быть очень аккуратным и вызывая функцию проверить все функции, которые та явно или косвенно вызывает, и взять все необходимые блокировки заранее все разом.

                              Печально, я надеялся там додумались реализовать обмен сообщениями без блокировок. Например — статья про неблокирующие очереди.
                                0
                                Не совсем понял аргумент. Если мы говорим про многопоточное программирование с блокировками в общем, то оно само по себе не является простым занятием. Передача сообщений это лишь способ снижения сложности, а конкретней, способ уйти от таких взаимоблокирующих вызовов. Многопоточное программирование без блокировок ещё более сложная задача, что подтверждает Ваш пример (кстати, весьма познавательный). Да и нет у него особой специфики в D. Та же самая функция cas и все те же проблемы, что и, например, в С++. А от велосипедов уйти никогда не удасться на 100%, так как std.concurrency не может покрыть все задачи, а реализовывать непокрытые как-то нужно. Поход, описанный мной, показывает только особенности способа создания очереди сообщений именно в D. Я не говорил, что надо делать всегда именно так. Выбор всегда остаётся за програмистом.
                                  0
                                  Я тут поигрался с потоками. Для передачи сообщений между ними можно даже без CAS обойтись — достаточно иметь по очереди между каждой парой потоков, где один только пишет, а другой только читает, сохраняя инвариант «В очереди всегда есть как минимум одно сообщение». Небольшой пример. Тут нет поддержки произвольных типов сообщений и соответственно паттерн-матчинга. Также нет ограничения на размер почтового ящика, приоритетов и протухания, но это всё легко прикручивается. Также я попробовал сделать синхронным лишь мапку Tid=>Queue, чтобы не париться, оставив саму очередь неблокирующей. Не знаю получилось ли у меня это (пока ещё плохо понимаю типизацию в D). Идея в том, что блокировка нужна лишь при появлении новых потоков и удалении старых, а друг с другом они обмениваются сообщениями без каких-либо блокировок.
                          0
                          На самом деле сейчас может быть я решил бы ту задачу совершенно по другому. Ну а старое решение осталось. Мне нужно было максимально гибко управлять потоками, приостанавливать их деятельность, они обрабатывали данные, которые можно было пропускать по истечению времени, отправляли друг другу сигналы. Вроде справлялось решение с поставленной задачей.
                            0
                            Ну, судя по описанию send-receive с минимальной обёрткой, реализующей протокол поддерживающий таймауты, должны бы подойти идеально.
                      0
                      Кстати, в чём отличие __gshared и shared переменных?
                        0
                        __gshared это полный аналог глобальных перменных из C/C++. В основном __gshared используется для создания обёрток и биндингов. Модификатор shared является частью системы типов D, он транзитивный (если объект shared, значит и все поля его shared), определяет набор операций (у shared объектов можно вызывать только shared методы). __gshared не относится к системе типов, и в этом плане не безопасен. Использовать его Вы можете только на свой страх и риск (никаких проверок на наличие синхронизации нет, как в C/C++). С точки зрения доступности данных они эквивалентны. А начинается __gshared с двух подчёркиваний, чтобы легче было искать в коде, когда начнутся проблемы =) В итоге, если Вам не нужно использовать разделяемые глобальные переменные с C/C++ кодом, то по сути нет необходимости использовать __gshared.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое