HZK
@HZK (Ritoku Sakamae)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

tkinter: afterメソッドを1ミリ秒以下にできたが、機序が分からない

問題となっている現象

時計の針を高速回転させて、残像ができないか試行錯誤しています。

以下のコードでは、開始ボタンを押すと1ミリ秒でafterメソッドを繰り返します。その後、開始ボタンを押せば押すほど回転が早くなり、画面のリフレッシュレートを超えても、残像は生じないものの高速回転しているような錯覚がみられます。

計測すると1ミリ秒以下でafterメソッドが繰り返されています。そして、開始ボタンを押した数と同じだけ、停止ボタンを押すと回転を止めることができます。

質問事項

(1)なぜ高速回転できるのか?
afterメソッドの最小単位は1ミリ秒ときいています。したがって、開始ボタンを押すたびにafterメソッドの1ミリ秒が重なっていくだけなので、その分遅くなりそうなものですが、実際は逆です。感覚的には再帰の深度が深くなっているように推測しますが、どういう機序で起こっているのか分かりません。

(2)マイクロ秒の領域における値のバラツキについて
開始ボタンを押すたびに確かに計測値は短くなっていきます。しかし、計測値をみると倍々で短縮していく訳でもなさそうです。そもそも、1ミリ秒の設定でも時折計測値の外れ値がみられます。これは、処理時間が関与しているためでしょうか。

(3)方法論的に正しいのか
afterメソッドを1ミリ秒以下とする方法論として、コード的に問題はないのでしょうか。また、他に正しい方法はあるのでしょうか。

以上、ご教授お願いします。

該当するソースコード

# -*- coding: utf-8 -*-
import tkinter as tk
import math
import time
import random

# キャンバスのサイズの設定
CANVAS_WIDTH = 400
CANVAS_HEIGHT = CANVAS_WIDTH
CANVAS_SIZE = CANVAS_WIDTH

# 時計の前面と背景の色の設定
BG_COLOR = "black"

#時計の外枠
FG_COLOR = "gray"

# 盤面を表す円の半径の設定
CLOCK_OVAL_RADIUS = CANVAS_SIZE / 2

#針の長さ
R = CLOCK_OVAL_RADIUS*0.95

# キャンバスの中心座標
X0 = CANVAS_HEIGHT/2
Y0 = CANVAS_WIDTH/2

class POV(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
  
        self.frame0= tk.Frame(self.master, bg="white") 
        self.frame0.pack()  
        
        # 変数定義
        self.rad =0
        self.after_id = 0    
        self.time_lst = [0,]
    
        self.create_widget()
        self.create_button()
        self.draw_hand()
        
    def create_widget(self):
        self.canvas = tk.Canvas(self.frame0, width = CANVAS_WIDTH, height = CANVAS_HEIGHT, 
                                     bg=BG_COLOR, highlightthickness=0)
        #gridは3x3       
        self.canvas.grid(column=0, row=0, columnspan=3, padx=0, pady=0,
                         sticky=tk.NSEW,
                         )  

        # 時計の盤面を表す円を描画する
        x1 = X0 - CLOCK_OVAL_RADIUS
        y1 = Y0 - CLOCK_OVAL_RADIUS
        x2 = X0+ CLOCK_OVAL_RADIUS
        y2 = Y0+ CLOCK_OVAL_RADIUS

        self.canvas.create_oval(
            x1, y1, x2, y2,
            fill="black",
            width=2,
            outline="gold"
        )

    def draw_hand(self):
        self.hand1 = self.canvas.create_line(
        X0, Y0, X0+R*math.cos(self.rad), Y0+R*math.sin(self.rad),
            fill="white",
            width=10,
        )        
        # math.pi/180が1度なので、3度ずらした針
        self.hand2 = self.canvas.create_line(
        X0, Y0, X0+R*math.cos(self.rad+ 3*math.pi/180), Y0+R*math.sin(self.rad+3*math.pi/180),
            fill="white",
            width=10,
        ) 
                
    def rotate_hand(self):
        #ループの間隔測定
        time_a = time.time()
        duration = time_a - self.time_lst[-1]
        self.time_lst.append(time_a)
        dur_r = round(duration, 5)
        
        #print頻度を調整
        r = random.randint(1, 300)
        if r ==5:
            print(dur_r)
        
        xr = X0+R*math.cos(self.rad)
        yr = Y0+R*math.sin(self.rad)
        
        xr_2 = X0+R*math.cos(self.rad+3*math.pi/180)
        yr_2 = Y0+R*math.sin(self.rad+3*math.pi/180)       
        
        self.canvas.coords(self.hand1, X0, Y0, xr, yr)
        self.canvas.coords(self.hand2, X0, Y0, xr_2, yr_2)
        
        #1度ずつふえる
        if self.rad > 2*math.pi:
            self.rad = self.rad -  2*math.pi
        
        self.rad += math.pi/180
        self.after_id = self.after(1, self.rotate_hand)

    def create_button(self):
        self.start_button = tk.Button(self.frame0, width=20, height=1, text="開始", fg = "gray20",
                                 font=("MSゴシック体", "14"), command=self.start_button_clicked)
        self.start_button.grid(column=0, row=1, columnspan=3, sticky=tk.NSEW)
              
        self.stop_button = tk.Button(self.frame0, width=20, height=1, text="停止", fg = "gray20",
                                 font=("MSゴシック体", "14"), command=self.stop_button_clicked)
        self.stop_button.grid(column=0, row=2, columnspan=3, sticky=tk.NSEW)
    
    #ボタンを押した時
    def start_button_clicked(self):
            self.rotate_hand()
            
    def stop_button_clicked(self):
            self.after_cancel(self.after_id)

if __name__ == "__main__":
    app = tk.Tk()
    POV(app)
    app.mainloop()
0

3Answer

afterメソッドの最小単位は1ミリ秒ときいています。したがって、開始ボタンを押すたびにafterメソッドの1ミリ秒が重なっていくだけなので、その分遅くなりそうなものですが、実際は逆です。感覚的には再帰の深度が深くなっているように推測しますが、どういう機序で起こっているのか分かりません。

逆とはどういう状況でしょうか?
ボタンを押すごとに1ミリ秒タイマーが増えていくので、1ミリ秒の間にコールバック関数が複数回呼び出されることになります。
self.after_idには最後に起動したタイマーIDだけが代入されることになり、その他のタイマーIDは残らないけど動いています。

1ミリ秒の設定でも時折計測値の外れ値がみられます。これは、処理時間が関与しているためでしょうか。

Python以外にもOSや他のアプリが裏で動いています。そこで重い処理があるとCPUコアやスレッドの割り当ての関係によってPythonの処理が影響を受けることがあります。

afterメソッドを1ミリ秒以下とする方法

人間の目やディスプレイ表示が追い付かない表示処理はCPUの無駄使いに思えます。
1回のクリックで5度増やすようにすれば、5回押したのと同じように見えると思います。

2Like

Comments

  1. @HZK

    Questioner

    shiracamus 様

    端的なご説明、ありがとうございます。
    for文を用い、1回クリックで5回分コールバック関数を呼び出しましたが、確かにその通りでした。
    CPUを限界近くまで追い込むと、それなりに残像が得られそうな感触を得ました。役には立ちませんが・・・。

    今後ともよろしくお願い致します。

(1)なぜ高速回転できるのか?

afterメソッドが1ミリ秒以下になったのではなく、afterメソッドを呼んだ数だけ多重で起動されているだけと思います。
おそらく、マルチスレッドでしょうから、ローカル変数はスレッド固有、グローバル変数はスレッド間で共有だと思います。

2Like

Comments

  1. @HZK

    Questioner

    nak435 様

    ご指摘のように、多重起動であることを理解しました。
    再帰ではなかったです。
    ありがとうございました。

(1)なぜ高速回転できるのか?

質問内容は、最小単位は1ミリ秒なのになぜ測定値が1ミリ秒以下が測定されるかですね。
1msの待っている間に処理(rotate_hand)が実行されるからです。(推測)
時間は短くなりますが、時間間隔がおかしくなると思います。(開始ボタンのタイミングの影響)
例)1回目
a.png

例) 2回目
b.png

(2)マイクロ秒の領域における値のバラツキについて
しかし、計測値をみると倍々で短縮していく訳でもなさそうです。

リソース(CPU空き時間)が無限ではないので、空き時間が減れば、時間短縮も減ります。

そもそも、1ミリ秒の設定でも時折計測値の外れ値がみられます。これは、処理時間が関与しているためでしょうか。

質問者殿のプログラム及びPython、OSの処理などが関与してると思います。これはPC環境やOSによっても違うと思います。

(3)方法論的に正しいのか
afterメソッドを1ミリ秒以下とする方法論として、コード的に問題はないのでしょうか。

想像の域を脱してないのですが、1ミリ秒ぐらいで動作する処理を複数実行しただけで、処理間隔が適当で動作していると思いますし、もし並列でrotate_hand() 同士が動作していた場合、排他処理が正しくされないので1ミリ秒以下の周期で動作させる方法としては、正しくないと考えます。

他に正しい方法はあるのでしょうか。

方法(pythonプログラム)でですよね。
普通なら、周期タイマーで rotate_hand処理 を起動するのが普通と思います。pythonで実現可能かどうか検討しないとわからないですね。

2Like

Comments

  1. @HZK

    Questioner

    YearCentury 様

    わざわざ図解までして頂き、本当に感謝致します。
    この図をみたことで、直感的に理解することができました。まさに、ボタンのタイミング次第です。

    また、ご指摘のように、rotate_hand処理を周期タイマーで 1ミリ秒以下、具体的にはマイクロ秒単位で繰り返すことができれば、正しい方法論を確立できそうです。
    ただし、sleep関数にせよ最小単位は1ミリ秒なので、Pythonでは見当もつきません。処理遅延の問題もありますし・・・。
    そして、for文で即時で回すことができても、これは周期とは言えないですね。

    ちなみに、実物のバーサライタではArduinoを用いるそうですが、delay()はミリ秒指定なので、マイクロ秒で指定のできる delayMicroseconds() を使用するとのことです。
    先人達がLED制御にPythonを用いていないところをみると、不可能なのかもしれません。

    非常に示唆に富む助言を頂き、考えを深めることができました。ありがとうございました。

Your answer might help someone💌