質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.71%

【Python】マルチスレッドを維持しつつ、それぞれのスレッドで定期処理を行いたい

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 974

kz_

score 7

前提・実現したいこと

※Python学習を始めたばかりです。
マルチスレッドそれぞれで定期処理をするプログラムをいろいろ試してみたくて書いたソースです。
作成した内容では、suchedule_event_communicateとsuchedule_event_run_print_helloの
スレッドを立ち上げています。
それぞれで、3秒に一回時刻を表示、5秒に一回"HELLO!!!"を表示(ただし、処理の途中で10秒間待機させる)という内容になっています。

発生している問題・エラーメッセージ

下記のソースを実行すると、以下のメッセージのように、
同じ時刻で同じメッセージが二回表示されます。

2019-04-04 09:37:06.219 : get_time
2019-04-04 09:37:08.247 : HELLO!!!
2019-04-04 09:37:08.247 : HELLO!!!
2019-04-04 09:37:08.247 : 待機状態です、、、
2019-04-04 09:37:08.247 : 待機状態です、、、
2019-04-04 09:37:11.258 : 待機状態です、、、
2019-04-04 09:37:11.258 : 待機状態です、、、
2019-04-04 09:37:14.268 : 待機状態です、、、
2019-04-04 09:37:14.268 : 待機状態です、、、
2019-04-04 09:37:17.279 : 待機状態です、、、
2019-04-04 09:37:17.279 : 待機状態です、、、
2019-04-04 09:37:20.290 : print_hello_exit
2019-04-04 09:37:20.290 : print_hello_exit
2019-04-04 09:37:21.304 : get_time
2019-04-04 09:37:21.304 : get_time
2019-04-04 09:37:24.346 : get_time
2019-04-04 09:37:25.360 : HELLO!!!
2019-04-04 09:37:25.360 : 待機状態です、、、
2019-04-04 09:37:25.360 : HELLO!!!
2019-04-04 09:37:25.360 : 待機状態です、、、

該当のソースコード

import schedule
import time
import concurrent.futures

from datetime import datetime, timedelta


# ********************************
#   関数
# ********************************


def multi_thread():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
    executor.submit(suchedule_event_communicate)
    executor.submit(suchedule_event_run_print_hello)


def get_timestring():
    """
    ミリ秒までを保持できる現在時刻を取得
    :return:
    """
    now = datetime.now()
    return now.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]


def print_message(run_time=get_timestring(), message=""):
    print(run_time + " : " + message)


def suchedule_event_communicate():
    """
    3秒に一回時刻を表示する
    :return:
    """
    schedule.every(3/60).minutes.do(print_time)
    while True:
        schedule.run_pending()
        time.sleep(1)


def print_time():
    print_message(get_timestring(), "get_time")


def suchedule_event_run_print_hello():
    """
    5秒に一回HELLO!!!を表示する
    :return:
    """
    schedule.every(5/60).minutes.do(print_hello)
    while True:
        schedule.run_pending()
        time.sleep(1)


def print_hello():
    now_time = get_timestring()
    print_message(now_time, "HELLO!!!")

    # HELLO!!!が表示されてから10秒間は待機時間
    while str(datetime.strptime(now_time, '%Y-%m-%d %H:%M:%S.%f') + timedelta(
            seconds=10)) > get_timestring():
        print_message(get_timestring(), "待機状態です、、、")
        time.sleep(3)

    print_message(get_timestring(), "print_hello_exit")


# ********************************
#   main
# ********************************


# 定期処理起動
multi_thread()

試したこと

マルチスレッドでwhileを使用する以下の処理であれば交互にメッセージを出力(同じ時刻・メッセージなし)しました。
scheduleの中で、whileや待機(sleep)と組み合わせるのが良くないのでしょうか。

import time
import concurrent.futures
from datetime import datetime


def get_timestring():
    """
    ミリ秒までを保持できる現在時刻を取得
    :return:
    """
    now = datetime.now()
    return now.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]


def print_message(run_time=get_timestring(), message=""):
    print(run_time + " : " + message)


def func1():
    while True:
        print_message(get_timestring(), "func1")
        time.sleep(1)


def func2():
    while True:
        print_message(get_timestring(), "func2")
        time.sleep(1)


if __name__ == "__main__":
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=2)
    executor.submit(func1)
    executor.submit(func2)

試したこと②

suchedule_event_run_print_hello内に記載されていたwhileをなくしました。
そうするとうまくいくようです。

import schedule
import time
import concurrent.futures

from datetime import datetime, timedelta


# ********************************
#   関数
# ********************************


def multi_thread():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
    executor.submit(suchedule_event_communicate)
    executor.submit(suchedule_event_run_print_hello)


def get_timestring():
    """
    ミリ秒までを保持できる現在時刻を取得
    :return:
    """
    now = datetime.now()
    return now.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]


def print_message(run_time=get_timestring(), message=""):
    print(run_time + " : " + message)


def suchedule_event_communicate():
    """
    3秒に一回時刻を表示する
    :return:
    """
    schedule.every(3/60).minutes.do(print_time)
    while True:
        schedule.run_pending()
        time.sleep(1)


def print_time():
    print_message(get_timestring(), "get_time")


def suchedule_event_run_print_hello():
    """
    5秒に一回HELLO!!!を表示する
    :return:
    """
    schedule.every(5/60).minutes.do(print_hello)
  # whileをなくしました。
    schedule.run_pending()
    time.sleep(1)


def print_hello():
    now_time = get_timestring()
    print_message(now_time, "HELLO!!!")

    # HELLO!!!が表示されてから10秒間は待機時間
    while str(datetime.strptime(now_time, '%Y-%m-%d %H:%M:%S.%f') + timedelta(
            seconds=10)) > get_timestring():
        print_message(get_timestring(), "待機状態です、、、")
        time.sleep(3)

    print_message(get_timestring(), "print_hello_exit")


# ********************************
#   main
# ********************************


# 定期処理起動
multi_thread()
2019-04-05 10:00:37.653 : get_time
2019-04-05 10:00:39.653 : HELLO!!!
2019-04-05 10:00:39.661 : 待機状態です、、、
2019-04-05 10:00:42.661 : 待機状態です、、、
2019-04-05 10:00:45.661 : 待機状態です、、、
2019-04-05 10:00:48.662 : 待機状態です、、、
2019-04-05 10:00:51.662 : print_hello_exit
2019-04-05 10:00:52.662 : get_time
2019-04-05 10:00:55.662 : get_time
2019-04-05 10:00:56.662 : HELLO!!!
2019-04-05 10:00:56.663 : 待機状態です、、、
2019-04-05 10:00:59.663 : 待機状態です、、、

補足情報(FW/ツールのバージョンなど)

Python 3.7.0(pycharm使用)
windows7 64bit

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+1

質問のコードにあるscheduleはglobal変数です。そのためscheduleには2つのjobが定義されてしまい、それが2つのthreadで動作しているので、二重に実行されてしまいます。
その問題を解消するためには以下のように thread 毎にクラスSchedulerのインスタンスを作ればいいと思われます。

def suchedule_event_communicate():
    sc1 = schedule.Scheduler() 
    sc1.every(3/60).minutes.do(print_time)
    while True:
        sc1.run_pending()
        time.sleep(1)

def suchedule_event_run_print_hello():
    sc2 = schedule.Scheduler()
    sc2.every(5/60).minutes.do(print_hello)
    while True:
        sc2.run_pending()
        time.sleep(1)

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/05 16:56

    完全にかぶりました。失礼しました。

    キャンセル

  • 2019/04/09 09:33

    先に投稿いただけておりましたので、ベストアンサーにさせていただきました。
    ご回答くださいましてありがとうございました。

    キャンセル

+1

原因としてはscheduleモジュールの関数版のschedule.everyschedule.run_pendingを複数のスレッドで使っているためです。

scheduleモジュールのschedule.every関数を実行するとき、スケジューラーとして使用されるscheduleモジュール内の変数default_schedulerがグローバルとなっており、各スレッドでschedule.everyを使うと、それぞれのスレッドでひとつのスケジューラーを共用するかたちになります。

github schedule - init.py

スレッドそれぞれでスケジューラーを実行するため、同じ文字列が表示されてしまいます。質問者さんのコードではconcurrent.futures.ThreadPoolExecutorで生成したスレッドプールのスレッド2つが動きますが、それぞれで同じスケジュールを実行してしまうため、2つの"HELLO!!!"文字列がほぼ同時に表示されます。同じ時刻に見えるのはたまたまです。スレッドを3つにすると、今度は3つの"HELLO!!!"文字列が確認できます。

スレッドそれぞれでJOBをスケジューリングするのであれば、schedule.everyではなく、Scheduleクラスのインスタンスメソッドのeveryを使わなければなりません。また、schedule.run_pendingも同様です。上記で示した scheduleモジュールのソースファイル「init.py」を読むと分かります。

本件とは直接関係ありませんが、確認の際はthreading.get_identでスレッドのIDを表示させるようにすると分かり易くなります。例えばprint_message
適用すると、以下のようなかんじになります。

import threading

def print_message(run_time=get_timestring(), message=""):
    #print(run_time + " : " + message)
    print('{} (id={}): {}'.format(run_time, threading.get_ident(), message))

一部抜粋になりますが、質問者さんのコードをもとにして、以下のように修正します。

import threading

...

def print_message(run_time=get_timestring(), message=""):
    #print(run_time + " : " + message)
    print('{} (id={}): {}'.format(run_time, threading.get_ident(), message))


def suchedule_event_communicate():
    """
    3秒に一回時刻を表示する
    :return:
    """
    # schedule.everyではなく、インスタンスメソッドのeveryを使う
    scheduler = schedule.Scheduler()
    scheduler.every(3/60).minutes.do(print_time)
    while True:
        # schedule.run_pending ではなく、インスタンスメソッドのrun_pendingを使う
        scheduler.run_pending()
        time.sleep(1)

このようにすると、以下のように質問者さんの意図通り実行されます。

$ python3  t5.py
2019-04-05 16:25:20.673 (id=-2146563000): get_time
2019-04-05 16:25:22.674 (id=-2146563704): HELLO!!!
2019-04-05 16:25:22.689 (id=-2146563704): 待機状態です、、、
2019-04-05 16:25:23.673 (id=-2146563000): get_time
2019-04-05 16:25:25.690 (id=-2146563704): 待機状態です、、、
2019-04-05 16:25:26.673 (id=-2146563000): get_time
2019-04-05 16:25:28.691 (id=-2146563704): 待機状態です、、、
2019-04-05 16:25:29.674 (id=-2146563000): get_time
2019-04-05 16:25:31.691 (id=-2146563704): 待機状態です、、、
2019-04-05 16:25:32.674 (id=-2146563000): get_time
2019-04-05 16:25:34.691 (id=-2146563704): print_hello_exit
2019-04-05 16:25:35.674 (id=-2146563000): get_time
2019-04-05 16:25:38.674 (id=-2146563000): get_time
2019-04-05 16:25:39.691 (id=-2146563704): HELLO!!!

本件とは関係ないですが少し気になったところとしては、コードの一番下のメインスレッドでの実行は、すぐ終わってしまうことです。スレッドプールの2つのスレッドが起動したままなので気が付きにくいですが、メインは終わってしまってます。本来であれば何かしらThread.joinのようなもので待ち受けるべきだと思いますが、質問者さんの実験および習作のコードであろうし、別の課題として置いておきます。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/09 09:35

    細かくご説明いただいてありがとうございました。
    参考になりました。
    本件以外のところでも、指摘をしていただけまして勉強になりました。
    もう少し勉強してみます。
    ご回答くださいましてありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.71%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる