LoginSignup
2
3

More than 5 years have passed since last update.

boost::asioのタイマーをIObservableでラップする。あとstd::chrono

Posted at

C++/CLIをやってみる気になった。C++/CLIで公開されたクラスはWPFとかで使う予定。
そうなるとプロパティとかイベントを公開することになるのでいずれIPropertyChangedなどを実装する必要がある。
ならば最初からIObservableになっているとよいのではないか。
ということでやってみた。

boost::asio::steady_timer
 ↓
std::chrono::duration
 ↓
IObservable<TimeSpan>

という機能のC++/CLIによるクラスライブラリを作成した。

プロジェクトを作る

VS2013で新規のC++/CLIのクラスライブラリプロジェクトAsioを作成。
[Templates] - [Visual C++] - [CRL] - [Class Library]
を選択した。

もうひとつこのライブラリの動作確認用のC#のコンソールプロジェクトSampleを作成。
[Templates] - [Visual C#] - [Windows Desktop] - [Console Application]
で選択。

  • ReferencesにAsioプロジェクトへの参照を追加
  • NugetでReactive Extensions - Main Libraryを追加

プロジェクト設定の
* [x]Enable native code debugging
を有効にした。

C++/CLIに必要なもうひとつのデバッグ設定

VisualStudioの[TOOLS] - [OPTIONS]から、[Debuggin] - [General]
* [ ]Require source files to exactly match the original version
のチェックを外した。
これをしないとbreakpointが●から○になってC++/CLIのデバッグができなかった。

Sampleプロジェクト

ReactiveExtensionでIObservableを使う。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reactive.Linq;


namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                var service = new Asio.Service();
                var timer = new Asio.Timer(service);

                timer
                    .Take(3)
                    .Subscribe(elapsed =>
                {
                    Console.WriteLine("Expired: " + elapsed);
                }
                , () =>
                {
                    Console.WriteLine("Completed");
                    timer.Stop();
                }
                );

                timer.Start(TimeSpan.FromMilliseconds(330));
                service.Run();
                Console.WriteLine("Service ended");
            }

            // thread
            {
                var service = new Asio.Service();
                var timer = new Asio.Timer(service);

                timer
                    .Subscribe(elapsed =>
                    {
                        Console.WriteLine("Expired: " + elapsed);
                    }
                , ex =>
                    {
                        Console.WriteLine(ex);
                    }
                , () =>
                    {
                        Console.WriteLine("Completed");
                    }
                );

                Task.Run(() =>
                {
                    System.Threading.Thread.Sleep(1000);
                    timer.Stop();
                });

                timer.Start(TimeSpan.FromMilliseconds(330));
                service.Run();
                Console.WriteLine("Service ended");
            }
        }
    }
}
Expired: 00:00:00.3300000
Expired: 00:00:00.6600000
Expired: 00:00:00.9900000
Completed
Service ended
Expired: 00:00:00.3300000
Expired: 00:00:00.6600000
Expired: 00:00:00.9900000
System.Exception: スレッドの終了またはアプリケーションの要求によって、I/O 処理は中止されました。
Service ended

Asioプロジェクト

ref class Asio.Service

単純にboost::asio::io_serviceをラップ。

    public ref class Service
    {
    public:
        Service()
            : ptr(new boost::asio::io_service)
        {}

        ~Service()
        {
            this->!Service();
        }

        !Service()
        {
            delete ptr;
        }

        void Run()
        {
            ptr->run();
        }

    internal:
        boost::asio::io_service *ptr;
    };

C++の型を単純にラップする場合は上記のようにコンストラクタでnewして、デストラクタでファイナライズ、ファイナライザでdeleteするスタイルになるらしい。

internalがミソでこのクラスを使う側からは不可視で、C++/CLIの実装側からはpublicになる。

ref class Asio.Timer

IObservableを実装する。
IObservableのインタフェースはSubscribeだけだが、
Unscriber, Unsubscribe, Expired(OnNextを呼ぶ), RaiseError, Completedは実質的に必須ぽい。
この辺よく知らずにReactiveExtensionsを使ってましたよ。


    template<class CLOCK> class TimerImpl;
    public ref class Timer : IObservable<TimeSpan>
    {
        List<IObserver<TimeSpan>^> ^m_observers;
        Queue<IObserver<TimeSpan>^> ^m_subscribeQueue;
        Queue<IObserver<TimeSpan>^> ^m_unsubscribeQueue;
        ref class Unsbscriber
        {
            Timer^ m_timer;
            IObserver<TimeSpan>^ m_observer;

        public:
            Unsbscriber(Timer^ timer, IObserver<TimeSpan> ^observer)
                : m_timer(timer), m_observer(observer)
            {
            }

            ~Unsbscriber()
            {
                this->!Unsbscriber();
            }

            !Unsbscriber()
            {
                m_timer->Unsubscribe(m_observer);
            }
        };

    public:
        Timer(Service ^service);
        ~Timer();
        !Timer();
        void Start(TimeSpan interval);
        void Stop();
        virtual IDisposable^ Subscribe(IObserver<TimeSpan> ^observer);

    internal:
        TimerImpl<std::chrono::steady_clock> *ptr;
        void Unsubscribe(IObserver<TimeSpan> ^observer);
        void RaiseError(String ^message);
        void Next(TimeSpan elapsed);
        void Completed();

    private:
        void UpdateObservers();
    };
}

timerのコールバックが別スレッドから飛んでくるため、m_observersに対するforeachとUnsubscribeによる削除がバッティングしてクラッシュする事態が発生。m_observablesはasioのタイマースレッドのみが操作するようにして、
m_subscribeQueue, m_unsubscribeQueueをロックしながら使うことで同期することにした。

class TimerImpl(C++/CLIではなくC++の作法)

C++で記述するC++とC++/CLIが相互にやりとりするクラス(ややこしい)。C++なのでref classに対する参照[^]は使えないが、

  msclr::gcroot<Timer^>

というtemplateで例外的にマネージドクラスへの参照をC++に持ち込めるらしい。
C#のstructを表すval class(今回のTimeSpan)はそのまま値渡しらしい。
C++的にはconstリファレンスにしたいところだがそのあたりはわからなかった。

    template<class CLOCK>
    class TimerImpl
    {
        boost::asio::basic_waitable_timer<CLOCK> m_timer;
        typename CLOCK::time_point m_start;
        volatile bool m_stop;

    public:
        TimerImpl(boost::asio::io_service &service)
        : m_timer(service), m_stop(true)
        {
        }

        ~TimerImpl()
        {
            Stop();
        }

        void Stop()
        {
            m_stop = true;
            m_timer.cancel();
        }

        void Start(msclr::gcroot<Timer^> refClass, TimeSpan span)
        {
            m_stop = false;

            double totalMilliSeconds = span.TotalMilliseconds;
            auto interval = std::chrono::milliseconds((int)totalMilliSeconds);
            m_start = CLOCK::now();
            m_timer.expires_from_now(interval);
            Start(refClass, interval);
        }

        void Start(msclr::gcroot<Timer^> refClass, const std::chrono::microseconds &interval)
        {
            auto self = this;
            auto onTimer = [self, refClass, interval](const boost::system::error_code& ec)
            {
                if (ec) {
                    // error;
                    refClass->RaiseError(gcnew String(ec.message().c_str()));
                    return;
                }

                auto expired = self->m_timer.expires_at();
                auto elapsed = expired - self->m_start;
#if 0
                auto tick = std::chrono::duration_cast <
                    std::chrono::duration < int64_t, std::ratio<INTMAX_C(1), INTMAX_C(10000000)> >
                > (elapsed);
                refClass->Expired(TimeSpan(tick.count()));
#else
                auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
                refClass->Expired(TimeSpan(0, 0, 0, 0, milliseconds.count()));
#endif

                // next
                if (!self->m_stop){
                    auto from = self->m_timer.expires_at();
                    self->m_timer.expires_at(from + interval);

                    self->Start(refClass, interval);
                }
            };
            m_timer.async_wait(onTimer);
        }
    };

std::chronoに関するメモ

C++11の時間ライブラリは美しさを追求したあまり、かえって使いにくくなっているのではないかにあるように、たしかにchronoちょっと分かりにくい。
だが使っていく気なので整理しておく。

duration

Boost.Chrono User's Guide

chronoの分かりにくさはduration。
durationは時の長さを表現するテンプレート。
それのtemplateとして6つの便利typedefがある。

template<
    class Rep, 
    class Period = std::ratio<1> 
> class duration;

// convenience typedefs
typedef duration<boost::int_least64_t, nano> nanoseconds;    // at least 64 bits needed
typedef duration<boost::int_least64_t, micro> microseconds;  // at least 55 bits needed
typedef duration<boost::int_least64_t, milli> milliseconds;  // at least 45 bits needed
typedef duration<boost::int_least64_t> seconds;              // at least 35 bits needed
typedef duration<boost::int_least32_t, ratio< 60> > minutes; // at least 29 bits needed
typedef duration<boost::int_least32_t, ratio<3600> > hours;  // at least 23 bits needed

こいつらはtemplate引数が違っても加減算等演算ができるぞと。

templateの第1引数は数値を格納する型で、第2引数は有理数ratio

template< 
    std::intmax_t Num, // 分子
    std::intmax_t Denom = 1 // 分母
> class ratio;

nano    std::ratio<1, 1000000000>
micro   std::ratio<1, 1000000>
milli   std::ratio<1, 1000>

これ30FPSの1000/33とかに使うとよさげだな。
異なる単位の時を相互に演算したり変換したりできるための型それがdurationらしい。

time_point

あと時間を表すdurationに対して時点をあらわすtime_pointがある。
基準時間(epoctimeとか?)からの絶対時間を表す概念か。durationは相対的な時間というか。
そのままでは使いづらいのでtime_tに変換するとかが要る。

boost.asioのtimerのexpires_at()で得られるのはtime_pointとなっている。
time_point - time_pointからdurationを得られるので下記のようにした。

auto timer=boost::asio::steady_timer(io_service);
auto start = timer::clock_type::now(); // 基準時間(time_point)

// 経過時間をdurationで得る
auto duration=timer.expires_at() - start;

C#のTimeSpanとstd::chronoを変換する例。

durationのコンストラクタが数値をセットし、count()で数値を取得できる。

chrono -> TimeSpan

auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
auto ts=TimeSpan(0, 0, 0, 0, milliseconds.count());

TimeSpan -> chrono

double totalMilliSeconds = span.TotalMilliseconds;
auto interval = std::chrono::milliseconds((int)totalMilliSeconds);

Boost.Asioのchrono版のタイマー

http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/tutorial/tuttimer4.html
を参考に書いてみた。

等間隔にtimerを発行するには

timer.expires_at(timeer.expires_at()+interval)

とする。

初回だけ

timer.expires_from_now(interval)

とする。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3