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

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

新規登録して質問してみよう
ただいま回答率
85.48%
MySQL

MySQL(マイエスキューエル)は、TCX DataKonsultAB社などが開発するRDBMS(リレーショナルデータベースの管理システム)です。世界で最も人気の高いシステムで、オープンソースで開発されています。MySQLデータベースサーバは、高速性と信頼性があり、Linux、UNIX、Windowsなどの複数のプラットフォームで動作することができます。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Raspberry Pi

Raspberry Piは、ラズベリーパイ財団が開発した、名刺サイズのLinuxコンピュータです。 学校で基本的なコンピュータ科学の教育を促進することを意図しています。

Q&A

解決済

3回答

1641閲覧

Python3 Tkinter データベースの書き込みについて

person

総合スコア223

MySQL

MySQL(マイエスキューエル)は、TCX DataKonsultAB社などが開発するRDBMS(リレーショナルデータベースの管理システム)です。世界で最も人気の高いシステムで、オープンソースで開発されています。MySQLデータベースサーバは、高速性と信頼性があり、Linux、UNIX、Windowsなどの複数のプラットフォームで動作することができます。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Raspberry Pi

Raspberry Piは、ラズベリーパイ財団が開発した、名刺サイズのLinuxコンピュータです。 学校で基本的なコンピュータ科学の教育を促進することを意図しています。

0グッド

1クリップ

投稿2020/06/02 12:11

Tkinterのエントリに入力した文字列をデータベースに書き込みたいのですが、Raspberry PiでやったところDB書き込みに少し時間がかかっているような気がしました。書き込みに時間がかかると次のデータ入力まで画面が止まってしまうのでなんか気に入りません。

そこでスレッドを使って入力をスムーズにしようと思って(これがいいやり方かはわかりませんが・・・)、次のようにソースコードを書きました。

Python

1import pymysql 2import threading 3import tkinter as tk 4 5 6def cnct(): 7 global cnctn, cur 8 cnctn = pymysql.connect( 9 host = "localhost", 10 user = "root", 11 password= "" , 12 db = "test", 13 charset = "utf8" 14 ) 15 cur = cnctn.cursor() 16 17 18def insert(): 19 global cnctn, cur, str 20 for i in range(100000): # とりあえずスレッドでできてるか見たいのでめっちゃDBに書き込む 21 cur.execute("INSERT INTO test VALUES(%s, %s);", (str, "abc")) 22 cnctn.commit() # コミットは逐次するよりも後で1回だけやったほうが時間かからない 23 24 25def discnct(): 26 global cnctn, cur 27 cur.close() 28 cnctn.close() 29 30def push(e): 31 global str, entry, t 32 str = entry.get() 33 print(str) 34 entry.delete(0, "end") 35 t = threading.Thread(target=func) 36 t.start() 37 #t.join() # ここに記述したらフリーズした 38 39 40def func(): 41 global t 42 cnct() 43 insert() 44 discnct() 45 t.join() 46 47 48win = tk.Tk() 49 50entry = tk.Entry(win) 51entry.grid() 52 53button = tk.Button(win, text="save") 54button.grid() 55 56button.bind("<ButtonRelease>", push) 57 58win.mainloop()

しかし

RuntimeError: cannot join current thread

とエラーが出てしまいます。

エラー文的にはt.join()がダメなのだろうと思いますが、どこに書けば良いかわかりません。

どのように対処すれば良いでしょうか?
回答よろしくお願いします。

また、
DB書き込み最中に閉じるボタンを押した時、とりあえずスレッド内でグローバルなフラグでも入れて、Trueの時は閉じないみたいなif文をつけて対処しようと思っています。
(ソースコードに終了時の動作を書いてないので考えていることだけ書いておきます。実現できるかはやってみないと自分にはわかりませんが・・・。)

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

#1. スレッドについて

質問者さんのコードを見るとボタンを押すたびに新規のスレッドを生成することを考えていると思います.私としては1つ生成すれば十分だと思います.

そのスレッドに,teamiklさんとのやりとりで質問者さんが書いた

スレッドの中をループにして、ループ中にDB書き込みするかどうかのif文を作ったほうがいいですかね?

をすればいいと思います.

#2. フリーズについて

コードのコメント文にあるフリーズは単にループ回数が多すぎるからずっと処理しているだけではないでしょうか?回数を減らせばフリーズしなくなるかもです.ただし,ボタンを押した時の動作として「スレッドを生成して終わるまで待っている」のでスレッドを使っている意味がないように思えます.
(UI上ではボタンを押し込んだ状態でスレッド終了するまで止まっています.)

#3. 終了時の動作について

DB書き込み最中に閉じるボタンを押した時、とりあえずスレッド内でグローバルなフラグでも入れて、Trueの時は閉じないみたいなif文をつけて対処しようと思っています。(ソースコードに終了時の動作を書いてないので考えていることだけ書いておきます。実現できるかはやってみないと自分にはわかりませんが・・・。)

できると思います.

#4. まとめ

以上3点を踏まえてソースコードを変えてみました.

Python

1import pymysql 2import threading 3import time 4import tkinter as tk 5 6 7def cnct(): 8 global cnctn, cur 9 cnctn = pymysql.connect( 10 host = "localhost", 11 user = "root", 12 password= "" , 13 db = "test", 14 charset = "utf8" 15 ) 16 cur = cnctn.cursor() 17 18 19def insert(): 20 global cnctn, cur, str 21 for i in range(100): 22 cur.execute("INSERT INTO test VALUES(%s,%s);", (str, "abc")) 23 cnctn.commit() # コミットは逐次するよりも後で1回だけやったほうが時間かからない 24 25 26def discnct(): 27 global cnctn, cur 28 cur.close() 29 cnctn.close() 30 31def push(e): 32 global str, entry, data_flag 33 if data_flag == True: 34 print("can't write.") 35 else: 36 str = entry.get() 37 if str == "": 38 print("str == ''") 39 else: 40 print(str) 41 entry.delete(0, "end") 42 data_flag = True 43 44 45def func(): 46 global loop_flag, data_flag 47 while loop_flag: 48 if data_flag == True: 49 cnct() 50 insert() 51 discnct() 52 data_flag = False 53 else: 54 pass 55 time.sleep(1) 56 57def tk_end(): 58 global loop_flag, data_flag, t 59 if data_flag == True: 60 print("writing now・・・") 61 else: 62 loop_flag = False 63 t.join() 64 win.destroy() 65 66 67loop_flag = True 68data_flag = False 69 70win = tk.Tk() 71 72entry = tk.Entry(win) 73entry.grid() 74 75button = tk.Button(win, text="save") 76button.grid() 77 78t = threading.Thread(target=func) 79t.start() 80 81button.bind("<ButtonRelease>", push) 82 83win.protocol("WM_DELETE_WINDOW", tk_end) 84 85win.mainloop() 86

何か勘違いしていたらすみません.

修正(2019/06/04)

ソースコードpush関数を修正しました.連続で保存した時に欠落がありました.実際はしないのでしょうが,for文で同じデータを100レコード保存する部分です.100レコード保存する前に次のstrを保存してしまうため,書き込めないようにしました.
もしかしたらスレッドは新規に作るのがいいかもしれません.個人的には,ループで保存処理する必要がなければ,「UIの操作時間>保存に要する時間」と言う観点から無視してもあまり問題はなさそうに思えました.

投稿2020/06/03 08:37

編集2020/06/04 15:00
P_Beginner

総合スコア99

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

teamikl

2020/06/04 16:02

大量の書き込みはテスト用とのことなので、恐らく当面している問題は、 保存時に(データベース接続・書き込み・切断)が毎回発生して、 その間 GUI のメインループが止まる(一瞬ですが一時的に操作を受け付けなくなる) のが問題だったのかと思います。 スレッド起動やデータベース接続を事前に準備しておくことは、 レスポンス改善には繋がるので、試す価値はあると思いますが、 スレッドの事前準備はともかく データベース接続はlocalhost 以外だと接続時間のtimeoutや、再接続問題があったりして 状況次第ですが、実際には難しい場合もあるのかもしれません。 あと、実際には固定のデータの書き込みではなく、 GUIに入力した任意のデータを書き込みしたいはずなので、 それをスレッド起動時の引数にする感じだったのかな。
P_Beginner

2020/06/06 08:48

確かにDBについてはアプリ起動のタイミングで接続しっぱなしの方がいいですね. フリーズについては,自分の環境ではフリーズ中もDBへINSERTされていたのとしばらく放置しておくとINSERTが終わって再び操作できるようになったのでループのせいだと思っていました.
guest

0

キューの説明の辺り、コードを参考にして下さい。

python

1#!/usr/bin/env python3.8 2 3import tkinter as tk 4from tkinter import ttk 5from threading import Thread, current_thread 6from queue import Queue 7 8SQL_CREATE_TABLE = """ 9 CREATE TABLE message_table ( 10 id INTEGER PRIMARY KEY, 11 message VARCHAR(64), 12 created TIMESTAMP DEFAULT (DATETIME('now', 'localtime')) 13 ) 14""" 15 16SQL_INSERT = """ 17 INSERT INTO message_table(message) VALUES (?) 18""" 19 20SQL_SELECT_ALL = """ 21 SELECT * FROM message_table 22""" 23 24SQL_SELECT_COUNT = """ 25 SELECT COUNT(id) FROM message_table 26""" 27 28 29def worker(queue, after_idle): 30 print("WORKER THREAD:", current_thread()) 31 32 # テスト用で sqlite3 を :memory: で使います。(データはファイルへ保存されません) 33 import sqlite3 34 35 # データベースの切断などの後始末は、コンテキストマネージャに任せる 36 with sqlite3.connect(":memory:") as con: 37 cur = con.cursor() 38 cur.execute(SQL_CREATE_TABLE) 39 40 for item in iter(queue.get, None): # Noneが送られてくるまでループ / キューが空の場合は待機 41 42 # キューに送るアイテムの形式は、用途に応じて自由に決めます。 43 # ここでは、1つ目にイベント識別の文字列、2つ目以降は任意の長さの引数としました。 44 event, *args = item 45 46 if event == "DB_INSERT": 47 cur.execute(SQL_INSERT, args) 48 49 elif event == "DB_COMMIT": 50 con.commit() 51 52 elif event == "TK_UPDATE_COUNT": 53 callback = args[0] 54 cur.execute(SQL_SELECT_COUNT) 55 total = cur.fetchone()[0] 56 57 # ※ 重要: GUI の更新は 必ずメインスレッドで行う 58 after_idle(callback, total) 59 60 # Debug 61 if 1: # テーブルの内容を出力 62 cur.execute(SQL_SELECT_ALL) 63 for row in cur.fetchall(): 64 print(row) 65 66 67def main(): 68 root = tk.Tk() 69 root.geometry("300x80") 70 text = tk.StringVar(root) 71 72 # Queue は、サブスレッド側で読み出しているので、一方向のデータ通信です。 73 # サブスレッド側からデータベースのデータを受け取るには、もう一つ別のキューが必要になりますが、 74 # tkinter の場合、mainloop() で処理されるキューを用いることができます。-> after, after_idle 75 76 queue = Queue() 77 thread = Thread(target=worker, args=(queue, root.after_idle)) 78 thread.start() 79 80 print("MAIN THREAD:", current_thread()) 81 82 def update_total_count(total): 83 # 確認用: メインスレッドで実行される 84 print("update_total_count(): ", current_thread()) 85 root.title("Total {} records".format(total)) 86 87 def post_message(): 88 queue.put(("DB_INSERT", text.get())) 89 queue.put(("DB_COMMIT", None)) 90 queue.put(("TK_UPDATE_COUNT", update_total_count)) 91 text.set("") 92 93 ttk.Entry(root, textvar=text).pack(fill=tk.X, expand=1, side=tk.LEFT) 94 ttk.Button(root, text="Post", command=post_message).pack(side=tk.RIGHT) 95 root.mainloop() 96 97 # ウィンドウを閉じた後に実行される。 98 # tkinterの要素はmainloop以降呼び出せない点に注意 99 # 100 # 上記の例であれば、"TK_UPDATE_COUNT" は root.title() にアクセスするので 101 # mainloop()以降に呼び出された場合、エラーになります。 102 # (RuntimeError: main thread is not in main loop) 103 # 104 # これを回避するには、WM_DELETE_WINDOW protocol を利用する。 105 106 queue.put(None) # キュー読込ループを終わらせるため、終端要素を送る 107 thread.join() # スレッドの完了を待つ 108 109 110if __name__ == '__main__': 111 main()

投稿2020/06/03 06:34

teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

通常であれば、終了時・ウィンドウを閉じる時で、スレッドの完了を待ちたい時に呼び出します。

tkinter の場合、mainloop() の後ろに書くと、
ウィンドウが閉じられた後に処理されます。

python

1win.mainloop() 2 3t.join()

※ 関数内でグローバル変数を定義した場合、
その関数が呼び出されてないと、未定義でNameErrorになる点は注意してください。
出来れば、同じスコープ内でその変数を作っておいた方が良いです。


ただし、スレッド内の処理中に進捗等を GUI に表示したりする場合は、
mainloop() 後は tkitner が終了しているので使えません。

その場合は、ウィンドウを閉じる前にスレッドの処理を終わらせるようにします。

python

1win = tk.Tk() 2 3# 中略 4 5def on_exit(): 6 # <-- ここでスレッドのループを終わらせる為のフラグ設定等をする 7 t.join() # <-- スレッドの終了を待つ 8 win.quit() # tkinter の終了 9 10win.protocol("WM_DELETE_WINDOW", on_exit) 11win.mainloop()

投稿2020/06/02 15:10

teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2020/06/02 23:47

自分の想定としては、スレッドの中でループするというよりはボタンを押すたびにDB書き込みようのスレッドを新規に作るようなことを考えています。 スレッドの中をループにして、ループ中にDB書き込みするかどうかのif文を作ったほうがいいですかね?
teamikl

2020/06/03 22:11 編集

要点は - thread.join() はスレッドを作成したスレッド(この場合メインスレッド) から、  スレッドの処理の完了を待つために使います。  その為、時間のかかる処理をしている場合は コードの実行は thread.join() の部分で止ります。 - tkiner のプログラムは、mainloop()の処理(GUI描画やイベント)を動かし続ける必要がある為  thread.join() 等のブロッキング操作があると、GUIはフリーズします。 - なので何処で用いるかというと、tkinterを終了した後、プログラムの終了時が多いです。 ボタンを押してすぐ終わるような処理(ただしGUIのレスポンスが気になる程度)であれば、 thread.join() はなくても良い場合があります。毎回スレッドを作るのであれば猶更。 GUIのない、端末でのプログラムであれば、thread.join()がないと メインスレッド自体がが終了してしまう事があるかもしれませんが。 GUIのプログラムの場合、そのまま起動しているのであれば、thread.join() はなくとも メインではイベントループが動いてるので、スレッドの処理は続けられます。 >スレッドの中でループするというより スレッドで呼び出す insert()関数で for i in range(100000) としてますが、 実際にはループをしないという事ですか? ---- >スレッドの中をループにして、ループ中にDB書き込みするかどうかのif文を作ったほうがいい これは少し無駄の多い処理になるので、字面通りの実装は待った方がいいです。 スレッドを予め作っておく利点は、データベースの接続をそのまま維持して使える事にあり スレッドに「キュー(Queue)を渡して」、メインスレッドとでデータのやり取りをします。 スレッド側のループは、「Queueの読込待ち」とした方が良いです。 (ここは実際にQueueを使ったコードを見ないと解り難いかもしれません) スレッド&データベースを接続しっぱなしにした方が良いか、 毎回スレッドとデータベース接続を作った方が良いかは、書き込み頻度等と相談です。 (例えば、書き込み頻度が高いと、データベースの接続ログが肥大化する、等) 追記: API等を介さないアクセスだと、接続のタイムアウトや再接続の考慮も必要です。
person

2020/06/03 04:44

for i in range(100000)についてはあまり気にしないでください。 実際はSQLのINSERT処理で数行DB書き込みするだけです。 わざわざforでループしているのはソースコメントに書いた「# とりあえずスレッドでできてるか見たいのでめっちゃDBに書き込む」です。
teamikl

2020/06/03 04:59

>実際はSQLのINSERT処理で数行DB書き込みするだけ これくらいの規模であれば、ループの処理待ちの問題はなさそうなので、注意が必要なのは、 もし複数のテーブルに対し書き込みがある場合で、不完全な書き込みが発生した場合ですね。 丁寧にスレッドの後始末をするなら 回答に書いた通り、プログラムの終了時にスレッドの完了を待つというのは変わりません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問