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

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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值