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中。
这样就实现了简单的多线程调用了。