【跟着Head First学python】11¾、关于线程:处理等待

it2022-05-06  3

1、引入线程

嗯,线程,在操作系统这门课中学过这玩意,最经典的问题就是线程与进程的区别:线程是轻量级的进程,线程只拥有内存balabala,现在要讨论的不是这个。

现在我们要解决以下问题:

再上一章中,我们遇到了以下问题:如果某一函数执行时间过长,异常处理没法处理这种事情。这应该交给线程解决。

怎么解决呢?这需要用到并发。设现在有一个函数A,它需要15秒的时间运行,主函数会调用到A。那么在主函数运行到A的时候,要等待15秒让A运行完,然后继续下面的工作。这样一来,在外面的用户就不得不等15秒。这体验实在是太差了。那么,如果用线程来运行这个函数,一旦主函数运行到A,开一个线程去运行A,然后主函数继续原来的工作,不就不用等了。

嗯,原理就是这样,看起来很美好。

比如说我们的webapp中的函数do_search和view_the_log:

def log_request(req:'flask_request',res:str)->None: #with open('vsearch.log','a') as log: #print(req.form,req.remote_addr,req.user_agent,res,file=log,sep='|') sleep(15) with UseDatabase(app.config['dbconfig']) as cursor: _INSERT="""insert into log (phrase,letters,ip,browser_string,results) values (%s,%s,%s,%s,%s)""" cursor.execute(_INSERT,(req.form['phrase'], req.form['letters'], req.remote_addr, req.user_agent.browser, res,)) @app.route('/search4',methods=['POST']) def do_search() -> 'html': phrase=request.form['phrase'] letters=request.form['letters'] results=str(search4letters(phrase,letters)) try: log_request(request,results) except Exception as err: print('******Logging failed with this error:',str(err)) return render_template('results.html', the_title='Here are your results', the_phrase=phrase, the_letters=letters, the_results=results) @app.route('/viewlog') @check_logged_in def view_the_log()->str: #contents=[] try: with UseDatabase(app.config['dbconfig']) as cursor: _SELECT="""select phrase,letters,ip,browser_string,results from log""" cursor.execute(_SELECT) contents=cursor.fetchall() titles=('Phrase','Letters','Remote_addr','User_agent','Results') return render_template('viewlog.html', the_title='View Log', the_row_titles=titles, the_data=contents,) except ConnectionError as err: print('Is your database or your mysql service switched on? Error:',str(err)) except CredentialsError as err: print('User-id/Password issues.Error:',str(err)) except SQLError as err: print('Is your query correct?Error:',str(err)) except Exception as err: print('Something went wrong:',str(err)) return 'Error'

其中,do_search函数在调用log_request时会阻塞,为什么会阻塞?因为log_request函数中使用了insert这一语句,它在执行,也就是在运行cursor.execute这句时,是要求服务器执行insert语句,它等着,服务器执行完了告诉它,然后它再执行下一条代码。同样的,view_the_log函数在执行select时也要阻塞。我们使用sleep来模拟时间较长的操作。

但是它俩有区别:前者在调用insert后没有接下来的操作,也就是说,我insert之后,不会去确认是否insert成功与否,没有操作是需要insert作为前置的;然而后者在执行select后,需要用contents来保存所有select得到的结果,也就是说,有操作需要select作为前置。

这样一来我们可以发现,insert没必要阻塞,用另外一个线程执行它很好,因为我们不关心它何时写入,不关心代码的运行次序,只要它最后写入即可;而select必须阻塞,而且不能新建一个线程运行它,因为我们的代码有次序,select后的代码必须在select运行之后才能继续运行,即使你用一个线程去运行该代码,那主函数该等还是要等。

因此,我们选择用一个线程运行log_request函数。

2、利用线程实现并发

要利用线程,我们要import Thread类。要想让某个线程执行某个函数,需要按如下形式:

t=Thread(target=A,args=())

其中,A是想要线程运行的函数名,args是A需要的参数。然后使用t.start()在适当位置开始线程即可。

看起来很简单。我们使用线程运行log_request如下:

@app.route('/search4',methods=['POST']) def do_search() -> 'html': phrase=request.form['phrase'] letters=request.form['letters'] results=str(search4letters(phrase,letters)) try: t=Thread(target=log_request,args=(request,results)) t.start() except Exception as err: print('******Logging failed with this error:',str(err)) return render_template('results.html', the_title='Here are your results', the_phrase=phrase, the_letters=letters, the_results=results)

其效果如图:

额,并不十分理想,有两个报错:RuntimeError。

为什么会报错?我们来看do_search的代码:

先开一个线程,让它执行log_request,然后启动这个线程。不管这个线程,返回查询的结果。

没问题,看log_request的函数:

先睡15秒,然后执行insert。

也没问题啊。真的没问题吗?

log_request函数需要两个参数:request、results。这两个参数是由do_search函数维护的。在返回查询的结果之后,do_search函数执行完毕,解释器回收内存,这两个变量不再存在。15秒之后log_request找不到它的两个参数了,因此报错。

因此出错的原因在于:在开一个线程时,args并不为log_request函数另外保存变量,而是直接告诉它变量放在do_search的变量区域的哪个位置,当do_search结束,即使log_request知道变量原本在哪个位置,也没有用了,这个位置动迁了,人面不知何处去,桃花依旧笑春风。

那么我们要做的就是另外保存log_request的变量,而不是直接指定变量的位置;或者让do_search函数直到log_request执行完再回收内存。这该怎么做呢?

需要Flask出场了。

Flask提供了一个修饰符copy_current_request_context,这个修饰符可以保证HTTP请求在被修饰函数运行期间一直是活动的。也就是说,调用某函数时存在活动的HTTP请求,在线程中执行该函数时这个请求一直是活动的。这就实现了“让do_search函数直到log_request执行完再回收内存”。如下:

@app.route('/search4',methods=['POST']) def do_search() -> 'html': @copy_current_request_context def log_request(req:'flask_request',res:str)->None: #with open('vsearch.log','a') as log: #print(req.form,req.remote_addr,req.user_agent,res,file=log,sep='|') sleep(15) with UseDatabase(app.config['dbconfig']) as cursor: _INSERT="""insert into log (phrase,letters,ip,browser_string,results) values (%s,%s,%s,%s,%s)""" cursor.execute(_INSERT,(req.form['phrase'], req.form['letters'], req.remote_addr, req.user_agent.browser, res,)) phrase=request.form['phrase'] letters=request.form['letters'] results=str(search4letters(phrase,letters)) try: t=Thread(target=log_request,args=(request,results)) t.start() except Exception as err: print('******Logging failed with this error:',str(err)) return render_template('results.html', the_title='Here are your results', the_phrase=phrase, the_letters=letters, the_results=results)

注意一点,修饰符copy_current_request_context修饰的函数必须在调用它的函数中定义,被修饰函数必须嵌套在其调用函数中。因此我们必须将log_request写在do_search中。

这样就实现了简单的多线程调用了。

 

 

 


最新回复(0)