マルチスレッドはてなキーワードしりとりを改造・その2
前回のコードではshiritori.Furiganaクラスのインスタンス作成を子スレッドで行なっていたのですが子スレッドで発生した例外は親スレッドのtry文では受け取れないことが分かったのでこの処理は親スレッドで行うように変更しました。
この処理を含め、スレッド作成〜各スレッドの結果の回収といった並列処理に関係する作業をすべてParallelAskクラスのクラスメソッドにしました。前回のコードではこれらの処理をmainに書いていたのでmain関数側で並列処理を意識する必要がありましたが、この変更によりmain関数からはマルチスレッド関連処理が見えなくなりました。
また前回のコメントでyield文について教えていただいたので上のクラスメソッドをジェネレータにし、すべての子スレッドが作業を終えていなくとも次の処理に入れるようにしました。
# coding: utf-8 import shiritori import threading WORDS_MAX = 10 # 一度に入力を受け付ける最大単語数 class PrlSupportHFD(shiritori.HatenaFuriganaDict): """親クラスの辞書登録処理に排他制御を加える""" def __init__(self): self.lock = threading.Lock() shiritori.HatenaFuriganaDict.__init__(self) def __setitem__(self, key, value): self.lock.acquire() shiritori.HatenaFuriganaDict.__setitem__(self, key, value) self.lock.release() class ParallelAsk(threading.Thread): """マルチスレッド関連処理""" _furiganadict = PrlSupportHFD() _askers = [] def __init__(self, word): self.word = word threading.Thread.__init__(self) def run(self): self.result = ParallelAsk._furiganadict[self.word] @classmethod def _set_askers(cls, words): """words内の各単語でインスタンス(子スレッド)を作成""" for word in words: asker = cls(word) cls._askers.append(asker) asker.start() @classmethod def get_result(cls, words): """問い合わせ結果をwordsの順に (ふりがな, 元の単語)のタプルで 返すジェネレータ """ cls._set_askers(words) for (asker, word) in zip(cls._askers, words): asker.join() furigana = shiritori.Furigana(asker.result, word) yield (furigana, word) @classmethod def reset_askers(cls): """子スレッドをリセット""" cls._askers = [] def multi_input_reader(usedwords, wordsmax=WORDS_MAX): """入力を受けとり、単語数が最大受付数を超えていないか 受け付けた単語内ですでに重複していないかを調べる """ wordsstr = raw_input(u"次の単語を入力してください。次は" + str(len(usedwords) + 1) + u"語目です。") wordsstrU = unicode(wordsstr).replace(u"、", u",") wordsU = wordsstrU.split(u",") if len(wordsU) > wordsmax: raise WordsInputError(value=wordsmax) if len(wordsU) != len(set(wordsU)): raise RepeatedWordsError() return wordsU class WordsInputError(shiritori.ShiritoriRuleError): def __str__(self): return (u"入力した単語が多すぎます。一度に入力できるのは" + str(self.value) + u"語までです。") class RepeatedWordsError(shiritori.ShiritoriRuleError): def __str__(self): return u"入力した単語に重複があります。" def main(): usedwords = {} initial = u"あ" # しりとりの最初の文字 print (u"最初のもじは「" + initial + u"」です") while True: try: wordsU = multi_input_reader(usedwords) for (furigana, word) in ParallelAsk.get_result(wordsU): if word in usedwords: # 使用済みでないか確認 raise shiritori.UniquenessError(word, usedwords[word]) furigana.exam(initial, word) # 冒頭文字、末尾文字確認 initial = furigana.creatIL() print word usedwords[word] = len(usedwords) + 1 except shiritori.CrtShiritoriError, e: print e break except shiritori.ShiritoriRuleError, e: print e continue finally: ParallelAsk.reset_askers() print u"終了です" if __name__ == "__main__": main()
入力を受け付けるmulti_input_reader関数自体をジェネレータにすること(下記:コード抜粋)も検討したのですが、これをやるとmulti_input_readerジェネレータが返すyield文が1つであるためにループを抜けてしまうので今回は変更しませんでした。multi_input_reader内で無限ループを行えば可能なのですが、それだと結局"while True"をどこで使うかの違いだけになってしまうので・・。
def multi_input_reader(usedwords): """入力を受けとり、単語数が最大受付数を超えていないか 受け付けた単語内ですでに重複していないかを調べる """ wordsstr = raw_input(u"次の単語を入力してください。次は" + str(len(usedwords) + 1) + u"語目です。") wordsstrU = unicode(wordsstr).replace(u"、", u",") wordsU = wordsstrU.split(u",") yield wordsU def main(): usedwords = {} initial = u"あ" # しりとりの最初の文字 print (u"最初のもじは「" + initial + u"」です") for wordsU in multi_input_reader(usedwords): try: if len(wordsU) > WORDS_MAX: raise WordsInputError(value=WORDS_MAX) if len(wordsU) != len(set(wordsU)): raise RepeatedWordsError() for (furigana, word) in ParallelAsk.get_result(wordsU): if word in usedwords: # 使用済みでないか確認 raise shiritori.UniquenessError(word, usedwords[word]) furigana.exam(initial, word) # 冒頭文字、末尾文字確認 initial = furigana.creatIL() print word usedwords[word] = len(usedwords) + 1 except shiritori.CrtShiritoriError, e: print e break except shiritori.ShiritoriRuleError, e: print e continue finally: ParallelAsk.reset_askers() print u"終了です"