2019-05-25

Raspberry Pi3を使ったリアル波形描画(6) Qtでマルチスレッド処理をやってみる

今回はリアル波形描画で必須となるマルチスレッド処理を Qt でやってみたいと思う。

ちなみに Raspberry Pi3 には,DebianベースのRaspberry Pi用の小規模な開発者向けのLinuxであるRaspbianを搭載することを想定している。(ラズパイの入門書でもRaspbianをインストール例が多い)

その場合,OS は Linux なのでマルチタスクでスレッドに優先度は付けられるものの,リアルタイムOSではないので インターバルの短いスレッド(RTOSならタスク)を正確に起動するのが難しい。

リアル波形を取り込む場合,正確なインターバルでA/D変換することが非常に重要になる。正確なインターバルでA/D変換ができないと正確な波形の再現ができない。

例えば,心電図をA/D変換するならば,4msインターバル(250Hz)程度のサンプリングはしたいところだ。しかし,いかに1.2GHzのマルチコアのCPUを搭載しているRaspberry Pi3と言えども,Raspbian の標準設定では 4ms以下の定時起動のスレッドを作るのは難しい。

そこで,マルチスレッドを実装する前に,リアルタイムOSとマルチタスクOSの違いについて考えてみたい。

この後説明するのは,もう何十年も前に CQ出版社の Interface に載っていたリアルタイムマルチタスクを理解するための示されていた例だ。参照元を書こうと思ったのだが,あまりにも古くで検索しても見つからない。自分の記憶だけで解説を再現する。

昔の風呂焚きの作業をリアルタイムシステムとして考える

昔の五右衛門風呂の風呂焚きの作業をリアルタイムシステムとして考えてみる。

今どき風呂焚きはボタン一発なので,「なにそれ」と思うかもしれないが,まあ,原始的な風呂焚きはそうなんだと思って欲しい。

原始的な,五右衛門風呂を沸かす手順は次の通りだ。

  1. 風呂釜を洗い栓をして水を注ぐ。
  2. 約15分たったら水を止める。
  3. 水がいっぱいになったら薪を燃やす。
  4. 約40分たったら湯加減をみてちょうど良ければ火を消す。
ポイントは次の通り。
  • 複数の作業を自分一人でこなさなければいけない。
  • 時間を含む限られた資源を有効に使いたい。
  • 水を止めそびれると水が溢れてしまう。
  • 火を消し忘れると火事になるかもしれない。
  • 水を入れているとき,風呂を沸かしている間は他のことをしいてもよい。
Inerface の特集記事では,確か,風呂を沸かしている間に原稿を書いたり,電話に出たりするという設定だったと記憶している。

これらのイベント/タスクを時間経過をX軸に,優先順位をY軸にとって書くと次のようになる。



「水を止める」「火を消す」いったタスクは,すぐにやらないと水が溢れたり,火事になったりするので優先度が高い。

一方で,原稿を書いたり,風呂に入ったりするタスクの優先度は低く,空き時間があるときにやればよい。

タスクの優先度と時間経過の関係を図にしたのが左の図となる。水張り15分後の「水を止める」や風呂焚き40分後の「火を消す」はタイムアウトのイベント発生後,他のタスク(例えば原稿書き)が動いていても,そのタスクを中断して,実施すなければならないタスクである。

このようにイベントが発生して優先度の高いタスクにリソースを割り当てなければいけないのたリアルタイムシステムであり,それを実現するためのOSがリアルタイムOSである。

CPUがシングルコアの時代,発生したイベントによってすぐさま優先度の高いタスクに切り替える必要があるようなイベントドリブンのリアルタイムシステムではリアルタイムOSを使う必要があった。

Raspberry Pi3 はマルチコアのCPUである Cortex-A53 (4コア)を搭載しているので,個々のコアにスレッドを割り当てることができれば,リアルタイムOSでなくてもいいのだが,Qt で用意されているスレッドのライブラリで コアの割り当てる方法は分からなかった。(おそらく,まだないと思う)

リアルタイムシステムのマルチタスクと非リアルタイムのマルチタスクの違いを左図に示す。

リアルタイムシステムの場合,イベントが発生するとただちに優先度の高いタスクに切り替わるが,非リアルタイムシステムの場合,一定時間ごとにタスク(スレッド)が切り替わるので,イベントが発生した後,待ち時間が発生する可能性がある。

最近の組込み系のCPUも待ち時間が気にならないほど,切り替えは早くなっているが,msオーダーのイベントでは追随できないこともある。

各タスクに処理の順番が回ってくる周期が遅いと,風呂焚きの例でいえば,水が溢れたり,湯が熱くなりすぎたりするということになる。

非力なCPUの時代の Windows では,ウインドウをたくさん開くとマウスのレスポンスが悪くなるような現象を経験したことはないだろうか。これは,ウインドウを開けば開くほど,一つのウインドウに処理が回ってくるインターバルが長くなるからだ。

非リアルタイムシステムで4msインターバルのマルチスレッドを作成してみる

では,本題に戻るとして,メインのスレッドとは別に4msインターバルで動くスレッドを Qt で実現してみる。

Qtでマルチスレッドを実現するには QtConcurrent や QTread を使う。

実際 Qt で実現するのに次の記事を参考にした。

QtConcurrent に関する記事 『QtConcurrentでマルチスレッドに挑戦
QTread に関する記事 『Qtでスレッドを使う前に知っておこう

QTread から定期的に必要な処理を起動するしくみは『スレッドを使った並列処理』を参照している。

クラス図でそれぞれの関係を示すと上図のようになる。メインウインドウのクラス MainWindow は QThead のクラス MyTread を持ち,MyTread は Qtimer のクラスtimer を持っている。

信号処理において正確なサンプリングができることは極めて重要で,サンプリング間隔が揺れてしまうと,波形がひずんでしまう。例えばもとの波形が sin波であっても,再現した 波形が奇麗な sin波でなくなってしまう。

よって,システムタイマーによる割り込みをスレッド処理で実現したら,正確にインターバルが取れているかどうか,常に確認しておきたい。この例では,4msのタイムインターバルがどれくらい正確に実現できているかどうかをインターバル間隔を表示することで確認している。
  • SIGNAL/SLOT の設定
  • periodic_work の処理を設定
  • ボタンの押下でスレッドの開始/終了を行う
  • スレッドが始まるときに呼ぶ run() メソッドを作成する。
  • run() でタイマーを生成し,タイムアウトしたときに Qtの SIGNAL/SLOT で timerHit()が呼ばれるようにする。
  • run() でタイマーを指定時間に設定する。(ms単位)
  • exec()を実行してスレッド動作を開始する。
  • なお,スレッドが終了すると exec()以下が実行される。
  • スレッド終了後に timer を停止する。
  • タイマーがタイムアウトしたら timerHit() が呼ばれる。
  • timerHit()が呼ばれたら Qt のシグナル data_update を発生させて,SLOT で受ける。
4msインターバルのスレッドを起動するときのボタンをウインドウがこれ。


MainWindows と MyThead の関係を示した図が下になる。

main.cpp のソース

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
  QApplication app(argc, argv);
  MainWindow testwindow;

  testwindow.show();

  return app.exec();
}


mainwindow.cpp のソース

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>

MainWindow::MainWindow(QWidget *parent) :
  QMainWindow(parent),
  ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //----------------------------------------------------------
  // Sendar :my_thread Signal :data_upda
  // Receiver :MainWindows Slot :periodic_work
  //----------------------------------------------------------
  QObject::connect(&my_thread, SIGNAL(data_update(int)), this, SLOT(periodic_work(int)) );

}

MainWindow::~MainWindow()
{
  my_thread.quit();
  my_thread.wait();
  delete ui;
}

void MainWindow::periodic_work( int data){
  char str[256];
  static int i=0;
// sprintf(str,"i=%d",i++);

  i++;

  // デバッグ用の表示文字列を作成
  sprintf(str,"No= %6d Interval: %d [uSec]",i, data);

  // ウインドウにカウンタを表示
  ui->textBrowser->append( QString::fromLocal8Bit(str) ); // カウンタを追記表示

}


void MainWindow::on_pushButton_clicked()
{
  static int flag_run=0;

  // push ボタンがクリックされて,スタートならスレッドをスタートする。
  if(flag_run==0){
    flag_run = 1;
    ui->pushButton->setText(QString::fromLocal8Bit("Stop thread"));

  // QTread の最優先度を設定する
  my_thread.setPriority(QThread::HighestPriority);
  my_thread.start();

  // push ボタンがクリックされて,ストップなら,スレッドを抜ける
  }else{
    flag_run = 0;
    ui->pushButton->setText(QString::fromLocal8Bit("Run thread"));
    my_thread.quit();

  }
}

mytread.cpp のソース


#include "mythread.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>


MyThread::MyThread(QObject *parent) :
  QThread(parent)
{
}

void MyThread::run(){
  QTimer timer; // Qtimer オブジェクト timer を生成

  //----------------------------------------------------------
  // Sendar :timer Signal :timerのタイムアウト
  // Receiver :このスレッド Slot :timeHit関数
  //----------------------------------------------------------
  // timerをスタートさせ,指定時間のタイムアウトが発生したら,Qt の SIGNAL/SLOT を使ってtimerHit関数を呼ぶ
  //----------------------------------------------------------
  connect(&timer, SIGNAL(timeout()), this, SLOT(timerHit()), Qt::DirectConnection );

  timer.setInterval(4); // Qtimerオブジェクトの timerの 時間間隔を 4ms に設定する
  timer.start(); // Qtimerオブジェクト timer をスタートする。

  printf("debug : Thread Starts\n"); fflush(stdout);
  exec(); //Start event loop
  printf("debug : Thread Stops\n"); fflush(stdout);

  timer.stop();
}

void MyThread::timerHit(){

  // システムのタイマーの構造体の変数を作成する
  struct timespec ts; // 現在のシステム時間を格納する構造体
  static struct timespec ts_before; // 前回のシステム時間を格納する構造体
  long diff = 0; // 前回と今回の差分を格納する変数

  // システムクロックをgetする
  clock_gettime(CLOCK_REALTIME, &ts);
  // 前回のシステム時間と今回のシステム時間の差分ナノ秒単位で計算し,ミリ秒単位に変換する
  diff = (long(ts.tv_nsec) - long(ts_before.tv_nsec))/1000;

  // 次回のために今回取得したシステムクロックを格納する
  ts_before = ts;

  // システムの実時間を秒とナノ秒に分けて表示し,前回と今回の差をミリ秒で表示する。
  printf("time: %10ld.%09ld CLOCK_REALTIME %05ld DIFF:uSec\n", ts.tv_sec, ts.tv_nsec, diff);

  // emit で前回との時間差(ms) を Qt の SIGNAL/SLOT で受け渡す
  emit data_update( int(diff) ); //emit signal
}


mainwindow.h のソース

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "mythread.h"
#include <stdio.h>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
  Q_OBJECT

public:
  explicit MainWindow(QWidget *parent = 0);
  ~MainWindow();

public slots:
  void periodic_work( int i);

private slots:
  void on_pushButton_clicked();

private:
  Ui::MainWindow *ui;
  MyThread my_thread;
};

#endif // MAINWINDOW_H

mytread.h のソース


#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include <QTimer>

class MyThread : public QThread
{
  Q_OBJECT
public:
  explicit MyThread(QObject *parent = nullptr);

  void run();

signals:
  void data_update(int i);

public slots:

private slots:
  void timerHit();
};

#endif // MYTHREAD_H



スレッドをWindows上で実施した例。インターバル間隔が,ぴったり4msではなくわずかにずれている(数μs程度)ことが分かる。

まあ,この程度なら実験としては問題ない範囲だろう。

0 件のコメント: