Wednesday, June 10, 2015

Scratch を学ぶ (2)

前回は scratch というプログラミング環境がどういうものかを簡単に話しました.そこではイベントという考えを使ってプログラムが動きます.今回はこの環境でキャラクターを動かした10歳の生徒の話です.

この Scratch というプログラミング環境では,キーイベントというイベントの一種が提供されています.たとえば,「右の矢印キーを押す」というイベントがあります.このイベントが起こると,x を 10 増やすというようなプログラムを書きます.すると,右の矢印キーを押すたびに猫を右に動かすことができます.これを上下左右のキーでそれぞれ行うことで,キーを使って猫を移動させることができます.

今日の生徒はドラゴンを登場させて,同じことをしました.するとなんと,猫もドラゴンも全く同じ動きをしてしまいます.まあ,そのようにプログラムしたので当然です.コンピュータはとても速く計算し,とても正確で,そしてとても愚かなものです.書いたとおりのことしかしません.その生徒はドラゴンを速く動かしたいと言いました.私は動く量を変えましょうと答えました.今は1回のキーを押すと,動く量が 10 です.速さとは一定時間に動く距離のことですから,これを増やせば速度は思いのままに変えられます.しばらくしてどうしたかなと思って見たら,この生徒はなんと,同じキーイベントをもう1つ作っていました.

つまり,右矢印のイベントが発生すると,2つのプログラムが起動するのです.1つのプログラムは10右へ移動であり,2つのプログラムでは合わせて20の移動になります.この環境では並列に動く部分でもちゃんと考慮されているようにできているらしく,これで正しく2倍の速度で動くのです.3倍速く動かすためには,もう1つプログラムを起動すればできます.なんという発想! 私はジョジョという漫画の1シーンを突然思い出しました.主人公がスタンドで浮くという場面です.(知らない人はすみません) 仗助がスタンドに押されて移動する場合,私の発想はスタンドの押す速度を変えることでしたが,この生徒の発想はスタンドの数を増やすことだったのです.

しかし,この生徒の方法では基本的に整数倍にしか速くできません.もちろん1つのプログラムがキャラクターを10押して,もう1つが同じキャラクターを5押すとすれば 15 押すことになり.1.5 倍の速度にすることもできます.しかしそれでは1つのプログラムが 15 押すのとあまり変わりません.むしろ難しいだけです.この生徒の発想はプログラムを完全にコピーすることで2倍になるということなのです.数学における一般化を学ぶと,どちらの問題を解くことがより大きな範囲の問題を解くことができるのかがわかります.この場合には1つのプログラムで速度を変換する方法の方が優れています.なぜなら速度を変える方法では整数倍だけでなく,小数倍にも,止めることも,逆方向に移動することもできるからです.また,計算機科学を学べば,スレッドが計算機で消費する資源,同期の問題などを考えて,やはり1つのプログラムの方が優れていることがわかります.そして保守の問題を学ぶと,コードが何倍にもふくれあがってしまう彼の方法よりも1ヶ所の変更で全てができる1つのプログラムの方がやはり優れています.しかしそれでもこの自由な発想はすばらしいと思いました.

4方向を5倍にするために,20 個のキーイベントがあるのを見て(図2),私は発想のすばらしさに驚きました.そして同時にやっぱり基礎を学ぶことも大切だなとも思いました.そしてこの生徒が今後多くを学んでいき,しかも今日の発想の素晴しさを忘れないでいて欲しいと思いました.

Figure 2. Using multiple same key events

Scratch を学ぶ (1)

しばらく前から私は Scratch [1] というプログラム言語を10歳から12歳の生徒達に教えています.時々見る生徒の創造力というのはかなり驚くものです.その1つをここに書いておこうと思います.

まずはプログラムを少し知っている人に向けての概念的な説明をしましょう.後で10歳の生徒にする例による説明も書きます.概念を理解した方が応用がきくのですが,どうしてもとっつきにくいものです.また,例による説明はその例についてはわかるのですが,その他の場合にどうなるかがわからないことが多いという問題があります.

Scratch というのは言語名でもありますが,プログラム環境でもあります.その環境ではスプライトキャラクター[2]というものをイベントリブンで動かすプログラムができます.スプライトキャラクターはオブジェクトと考えても良いもので,イベントによってプログラムが起動し,それぞれのプログラムは並列に走ります.

しかし,このような説明ではプログラムをしたことのない人には何のことはわからないでしょう.プログラム環境,スプライト,イベント,オブジェクトなどはプログラミングで使う概念なので,それぞれについて理解する必要があります.しかし,もちろんこのような概念は10歳の生徒にはあまり話をすることはありません.先に述べたように例による説明には問題点がありますが,ここではまず例を使って説明をしたいと思います.

たとえば scratch を起動すると,猫がいます(図 1).この猫に何かが起こるとどうするかということをプログラムするのです.ここで何かが起こるというのは,マウスのボタンが押されたとか,何かキーが押されたとか,そういうことです.それをイベントが発生すると言います.イベントが発生するとプログラムが動き出すので,イベントで駆動(driven: ドリブン)のプログラムと言います.それぞれのキャラクター,猫とか犬とかは自分の状態を持っており,その状態を使ってプログラムが動きます.状態というのは,猫がどこにいるかという位置とか,猫がどちらの方向を向いているとかのことです.プログラミングでは,状態を持っているものをオブジェクトということがあります.ここでは猫は1つのオブジェクトであり,犬もまた1つのオブジェクトです.

Figure 1. Scratch programming environment
Scratch では緑色の旗のボタンがスタートの合図になっています.そこで,それを最初のイベントとして使うことが多いです.緑の旗ボタンを押すと猫を中央に移動させて,「こんにちは!」と言わせたりすることがプログラムになります.私が10歳の生徒に最初に教えていることは「座標」です.とはいっても,x は横のことで y が縦のこと,両方を 0 にするとキャラクターは真ん中に行き,x を大きくすると右に移動とかいう話をします.それぞれのキャラクターの座標は常に表示されています.マウスでキャラクターを動かすとその座標が変化するのがその場で見えます.それを見せながら,(コンピュータの)マウスで猫をつかまえて,「ほら右にいくと x が増えていく」などと教えるのです.

今回は scratch の話で終わってしまいましたが,次回にはこの環境でキャラクターを動かした10歳の生徒の話をしましょう.


Friday, June 5, 2015

共有メモリによるプロセス間通信

Unix の共有メモリを使ったプロセス間通信について調べて実験をしてみた.対象は1つのホスト上での複数のプロセスである.ネット上でいくつか例題はないかと探したが,どうも良い例となるコードが見当たらなかった.結局はある解説記事と,Stack Overflow の議論と,man page を見て作ってみたものになったので,例をここに置くのも有用かと考え,この記事を書く.(もしかしたら探し方が悪くて良いコード例をみつけられなかっただけかもしれない.)

mmap を使うかどうかという話がいくつもでていたが,POSIX の方向としては,shmem_open と mmap を使うという方向があるということだったので,それを信じてその形での実装を試してみた.

基本的なコードの流れは次のようになる.

  1. 共有メモリ領域を1つのプロセスが shm_open() を使って作成する.その際に,プロセス間で共通の文字列を識別子(``identifier'')とする.(Linux ではこれが /dev/shm/identifier のように見える.)
  2. 共有メモリ領域を mmap() でメモリにマップする.共有メモリポインター (shared_ptr)が得られる.
  3. shared_ptr を使って複数のプロセスで通信をする.
  4. 利用終了後は munmap() をつかってマップを消す.
  5. 共有メモリオブジェクトを shm_unlink() によって消す.

以下に示すプログラムは,server と client の2つのプロセスが共有メモリを使って通信をするものである.ここで,server プロセス数と client プロセス数は共に 1 を仮定する.server と client は自分の領域にしか値を書き込まないことで,ロックを避けている.互いに相手の値を読み,それよりも1大きい数を一定の期間ごとに自分の領域に書くという例題である.シンプルではあるが,共有メモリで通信をする基本としては十分なものだと思う.ソースコード(shmem_test.cpp)を以下に付加する.ソースコードのコメントにコンパイル方法とどのように利用するかを書いておく.

/*
  Shared memory inter process communication minimal example
  Copyright (C) 2015 Hitoshi

  Compile:
    g++ shmem_test.cpp -o shmem_test -lrt

  Run (as a server):
    shmem_test server

  Run (as a client)
    shmem_test client

  Note: no locking. (but writes are not the same location.)

  Basic idea: server, shared memory creator

  1. Create a shared memory object by shm_open().
  2. Change the shared memory object by ftruncate().
  3. mmap the shared memory object to access via a pointer.
  4. Use the pointer to share the memory (may need lock, etc.)
  5. munmap the shared memory object
  6. Remove the shared memory object by shm_unlink()

  Basic idea: client

  1. Open the created shared memory object by shm_open().
  2. mmap the shared memory object to access via a pointer.
  3. Use the pointer to share the memory (may need lock, etc.)
  4. munmap the shared memory object

 */

#include <iostream>

#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

//----------------------------------------------------------------------
/// shard memory identifier name
const char* SHMEM_NAME = "/test.shmem";
/// shared memory size (align to the page size)
const size_t SHMEM_SIZE = 4096;

//----------------------------------------------------------------------
/// print out the usage and exit of this example
void usage_exit()
{
    std::cerr
        << "Shard memory inter process communication minimal test\n"
        << "shmem_test [server|client]\n"
        << "  server ... create/read/write shared memory\n"
        << "  client ... read/write shared memory"
        << std::endl;
    exit(1);
}

//----------------------------------------------------------------------
/// server
void run_server()
{
    // create/open the shared memory object
    int fd = shm_open(SHMEM_NAME,
                      // oflags
                      O_CREAT|  // create if not exist
                      // O_TRUNK|  // destroy once if exists
                      O_EXCL|   // error if exists
                      O_RDWR    // read/write
                      ,
                      // mode
                      S_IRUSR|  // user read
                      S_IWUSR   // user write
        );

    if (fd >= 0)
    {
        std::cout << "server: shmem_open succeed fd: " << fd << std::endl;
    }
    else
    {
        std::cerr << "server: shmem_open failed: " << fd
                  << ", if /dev/shm"    << SHMEM_NAME
                  << " exists, remove it." << std::endl;
        exit(1);
    }

    // resize the shmem object
    const int ret = ftruncate(fd, SHMEM_SIZE);
    if (ret != 0)
    {
        std::cerr << "Server: failed to ftrancate to " << SHMEM_SIZE
                  << std::endl;
        exit(1);
    }

    // get shmem address
    const size_t access_offset = 0;
    void* shmem_adder = mmap(0, SHMEM_SIZE,
                             // memory protection mode
                             PROT_READ| // Pages may be read.
                             PROT_WRITE // Pages may be written.
                             ,
                             // mapping flag
                             MAP_SHARED, // Share this mapping with other process
                             fd,
                             access_offset);
    if (shmem_adder == 0)
    {
        std::cerr << "Serevr failed to mmap." << std::endl;
        exit(1);
    }
    close(fd);                 // fd is no longer needed

    // server process work
    int* int_ptr = reinterpret_cast<int*>(shmem_adder);

    // initialize
    int_ptr[0] = 0;
    int_ptr[1] = 0;

    for (int i = 0; i < 10; ++i)
    {
        int my_int    = int_ptr[0];
        int other_int = int_ptr[1];

        if (my_int <= other_int)
        {
            my_int = other_int + 1;
            int_ptr[0] = my_int;
            std::cout << "I am taller. " << my_int << ", "
                      << other_int << std::endl;
        }
        usleep(800000);
        std::cout << "Checking client " << i << std::endl;
    }
    std::cout << "Quit server: " << int_ptr[0] << ", " << int_ptr[1] << std::endl;

    // unmap
    int ummap_ret = munmap(shmem_adder, SHMEM_SIZE);
    if (ummap_ret != 0)
    {
        std::cerr << "Failed to munmap. " << ummap_ret << std::endl;
        exit(1);
    }

    // remove shmem object
    int unlink_ret = shm_unlink(SHMEM_NAME);
    if (unlink_ret != 0)
    {
        std::cerr << "Failed to shm_unlink. " << unlink_ret << std::endl;
        exit(1);
    }
}

//----------------------------------------------------------------------
void run_client()
{
    // open the shared memory object
    int fd = shm_open(SHMEM_NAME,
                      // oflags
                      O_RDWR    // read/write
                      ,
                      // mode
                      S_IRUSR|  // user read
                      S_IWUSR   // user write
        );

    if (fd >= 0)
    {
        std::cout << "shmem_open succeed fd: " << fd << std::endl;
    }
    else
    {
        std::cerr << "shmem_open failed: " << fd
                  << ", Are you running the server?" << std::endl;
        exit(1);
    }

    // get shmem address
    const size_t access_offset = 0;
    void* shmem_adder = mmap(0, SHMEM_SIZE,
                             // memory protection mode
                             PROT_READ| // Pages may be read.
                             PROT_WRITE // Pages may be written.
                             ,
                             // mapping flag
                             MAP_SHARED, // Share this mapping with other process
                             fd,
                             access_offset);
    if (shmem_adder == 0)
    {
        std::cerr << "Failed to mmap." << std::endl;
        exit(1);
    }
    close(fd);                 // fd is no longer needed

    // server process work
    int* int_ptr = reinterpret_cast<int*>(shmem_adder);

    // initialize
    int_ptr[0] = 0;
    int_ptr[1] = 0;

    for (int i = 0; i < 10; ++i)
    {
        int my_int    = int_ptr[1];
        int other_int = int_ptr[0];

        if (my_int <= other_int)
        {
            my_int = other_int + 1;
            int_ptr[1] = my_int;
            std::cout << "I am taller. " << my_int << ", "
                      << other_int << std::endl;
        }
        usleep(500000);
        std::cout << "Checking server " << i << std::endl;
    }
    std::cout << "Quit client: " << int_ptr[0] << ", " << int_ptr[1] << std::endl;

    // unmap
    int ummap_ret = munmap(shmem_adder, SHMEM_SIZE);
    if (ummap_ret != 0)
    {
        std::cerr << "Failed to munmap. " << ummap_ret << std::endl;
        exit(1);
    }
}

//----------------------------------------------------------------------
/// main
int main(int argc, char* argv[])
{
    if (argc == 1)
    {
        usage_exit();
    }
    else if ((argc == 2) && std::string(argv[1]) == "server")
    {
        run_server();
    }
    else if ((argc == 2) && std::string(argv[1]) == "client")
    {
        run_client();
    }
    else
    {
        usage_exit();
    }

    return 0;
}

この例題が誰かの役に立てば幸いです.