django中静态文件的使用

发布在 Django

记得我刚刚开始接触django的时候,对于静态文件的引用始终一头雾水,按照网上说明的添加代码就是不好使。
今天再回头看看,突然发现以前自己还真是笨阿!补一篇记录算是弥补以前的缺憾了~
django版本1.5.3
目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
projectname
----projectname
--------templates
------------base.html
------------appname
----------------a.html
--------media
------------mp3
------------flv
--------static
------------img
----------------s.jpg
------------css
------------js
--------settings.py
--------urls.py
----manage.py

方式一

setting.py中添加、修改以下代码

1
2
3
4
5
6
7
import os
SITE_ROOT = os.path.dirname(__file__)
STATIC_ROOT=''
STATIC_URL = '/static/'
STATICFILES_DIRS = (
SITE_ROOT+STATIC_URL, #注意逗号!
)

urls.py修改如下

1
2
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()

<img name="a" src="/static/img/s.jpg"/>进行引用。

方式二

避开static相关,用media。

settings.py:

1
2
MEDIA_ROOT = os.path.join(os.path.dirname(__file__),'media/').replace('\','/')
MEDIA_URL = '/site_media/'

urls.py:

from django.conf import settings
    url(r'^site_media/(.*)$','django.views.static.serve',
                          {'document_root':settings.MEDIA_ROOT}),

<img name="a" src="/site_media/img/a.png"/>这里的set_media和上面urls.py中的名要对应。

目录结构自行修改。

个人推荐第一种方式。

评论和分享

fedora19安装cairo-dock

发布在 Linux

最近又把电脑折腾残废了,没办法,只好重安装系统。本来想告别fedora回到opensuse的怀抱,不过却发现opensuse下用zypper安装的软件版本都比较低,为了能和我已经编好的程序保持一致性,只好又换回fedora19。

安装过程还算顺利,添加国内源、三方源后更新系统却发现

1
2
3
4
错误:软件包:perl-PathTools-3.2701-1.el5.rf.x86_64 (rpmforge)
需要:perl(VMS::Filespec)
您可以尝试用 --skip-broken 来解决该问题
您可以尝试运行: rpm -Va --nofiles --nodigest

即便安装了perl、使用了提示的命令也是不好使。折腾了很久都不知道结果,只好卸载了rpmforge源后问题解决。不过发现卸载了这个源后很多软件都yum找不到了……直到刚才我才发现我居然用的是el5的架构……眼残了阿……而用yum erase rpmforge-release
这个命令居然把我的rpmfusion源也给删除了。不知道什么原理……重新安rpmfusion源,却死活提示以经安装。

1
ls /etc/yum.repos.d

查看后又没有rpmfusion的文件,奇怪了。

最后想起来我安装rpmfusion的时候是用rmp安装包的方式,于是用
rpm -qa | grep -i rpm
查看了,果然存在 rpmfusion的rpm。删除后重新安装,OK。

再yum更新,也没那个错误提示了,看样子全是因为rpmforge这个库阿。不过另一台电脑就没有问题,难道是选错架构了?

在用yum search查找cairo-dock并安装,sudo yum install cairo-dock*

一切顺利~

用cairo-dock还是很漂亮的,不过最吸引我的还是那个terminal的小插件,十分便利阿!

评论和分享

fedora19安装skype

发布在 Linux
1
sudo yum -y install libXv.i686 libXScrnSaver.i686 qt.i686 qt-x11.i686 pulseaudio-libs.i686 pulseaudio-libs-glib2.i686 alsa-plugins-pulseaudio.i686 qtwebkit.i686 compat-libffi.i686

64位系统,安装上面的那些依赖之后,到 http://www.skype.com/en/download-skype/skype-for-linux/ 下载
fedora16 32bit

下完上面那个rmp安装即可使用。

网址被墙,给个链接吧 http://download.skype.com/linux/skype-4.2.0.11-fedora.i586.rpm

评论和分享

有些网站为了防止爬虫抓取而设定了某些条件,比如如果一个ip某时间段内访问量很大,则禁止这个ip访问。如果这个网站启用了cdn加速服务的话,可以修改header中的host,并在url中直接用ip进行网站的访问。

#coding=utf-8
import urllib2
import random
import os,sys
DIRNAME = os.path.dirname(os.path.abspath(__file__))
class Gethtml_mouser():
    def __init__(self,key,debug=False,**dict_args):
        self.key = key
        self.debug = debug
        self.i_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0",
                          "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                          "Host":"www.mouser.com",
                          "Accept-Language":"en-US",
                          "Cookie":"preferences=ps=www&pl=en-US&pc_www=USDu; SDG1=1; SDG2=0; SDG3=0;"
                          }#伪造一个抓取USD的cookie
        ''' 如有效 就代表查询详细信息页面url 并且判断Host头信息的域名取值 '''
        self.info_url   = dict_args.get('info_url', '')
        if self.info_url and 'cn.mouser.com' in self.info_url:
            self.i_headers['Host']  = 'cn.mouser.com'
        self.get_iplist()
        self.creat_temip()
    def get_iplist(self):
        '''获取mouser_ip列表,请保证同目录下存在mouser_ip.txt'''
        iptxt = file(os.path.join(DIRNAME,'mouser_ip.txt'),'r')
        ipt = iptxt.read()
        self.iplist = ipt.split('n')
        iptxt.close()
    def log(self,flag):
        '''日志记录,生成mouser_log.txt为以后记录分析提供数据'''
        try:
            f = file(os.path.join(DIRNAME,'mouser_log.txt'),'a')
            if flag == 0:
                log = "%s,sucessn" % (self.tem_ip)   #此处的n为了分析日志用
                if self.debug: print log
                f.write(log)
            elif flag == 1:
                log = "%s,failn" % (self.tem_ip)
                if self.debug: print "%s is failed,try next ip..." % (self.tem_ip)
                f.write(log)
            else:
                if self.debug: print "log error"
            f.close()
        except Exception,e:
            if self.debug: print str(e)
            pass#防止由于服务器上没有写权限出错。
    def creat_temip(self):
        '''随机获取一个临时ip'''
        ip_len = len(self.iplist)
        ip_num = random.randint(0,ip_len-1)
        self.tem_ip = self.iplist[ip_num]
    def get_html(self,max_num=10):
        '''
         根据获取的临时ip构造最终url并返回所抓取的html
         如果发生异常则更换ip继续自动请求直到获取成功
         prams为空使用GET请求
        '''
        if max_num > 0:
            if self.debug: print 'there are %s changes to get html' % max_num
            now_num     = max_num - 1
            params={}
            if self.info_url:
                final_url   = self.info_url
            elif not self.key.startswith('ProductDetail'):
                final_url = "http://%s/Search/Refine.aspx?Keyword=%s" % (self.tem_ip,self.key)
            else:
                final_url = "http://%s/%s" % (self.tem_ip,self.key)
            if self.debug: print final_url
            req = urllib2.Request(final_url, headers=self.i_headers)
            try:
                if self.debug: print 'begin'
                if self.debug: print "use ip:",self.tem_ip
                page = urllib2.urlopen(req,timeout=15)#15秒超时
                self.hc = page.read()
                info_url = page.geturl()
                self.hc = '<info_url>%s</info_url>' % str(info_url) + str(self.hc)
                #print len(hc)
                #open('tmpa.html','w').write(hc)
                self.log(0)
                if self.debug: print 'end'
                return self.hc
            except urllib2.HTTPError, e:  
                if self.debug: print "Error Code:", e.code
                if self.debug: print "Error context:",e
                self.log(1)
                self.creat_temip()
                self.get_html(now_num)
            except urllib2.URLError, e:  
                if self.debug: print "Error Reason:", e.reason
                self.log(1)
                self.creat_temip()
                self.get_html(now_num)
            except:
                if self.debug: print "TimeOut error!"
                self.log(1)
                self.creat_temip()
                self.get_html(now_num)
        else:
            if self.debug: print 'Find error (had use max_num changes) ""'
            return ''

评论和分享

python 获取网站cookie

发布在 Python

对于一般的网站来说,通过以下代码便可以获取到cookie:

1
2
3
4
5
6
7
8
9
10
import urllib2
import urllib
import cookielib
logurl = "https://www.digikey.com/classic/RegisteredUser/Login.aspx?"
cj = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
urllib2.install_opener(opener)
resp = urllib2.urlopen(logurl)
for index, cookie in enumerate(cj):
print '[',index, ']',cookie

然后在构造post数据向目标url发送即可(至于header,有人说如果在此时再次提交自己构造的handers将会覆盖获取到的有cookie的hander,未亲自试验,不过若是真的可以试试调用opener.addheaders方法添加)

但digikey这个网站不知什么原因访问后不给返回cookie???

经过试验发现,从浏览器中直接提取登录后的cookie添加到headers中,直接访问登录后的页面就实现了登录!连postdata似乎都是多余的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
teurl = "https://www.digikey.com/classic/RegisteredUser/MyDigikey.aspx"
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language":"q=0.8,en-us",
"Host":"www.digikey.com",
"Cookie":"TS6b482d=1aa460f525235eaaaf6763d718b53ba564323; sid=5278426151-44225; TS50f921=c7b09221315dc72ed80a2b24f014fc2ffa4b16ccf877976952494826a3c65dd840b6a4ae6bad0a341809f751ca85ab033df615a7; TS168127=b58c4c2296cc37b2a5515974e403a008cdc46701e181d2a252494823019cdd5115d4a2a6a3c65dd840b6a4ae6bad0a341809f751ca85ab033df615a724f3d3ac91dbdb26; cur=USD; SiteForCur=US; utag_main=_st:1380536340264$ses_id:1380534378638%3Bexp-session; TS50f921_77=6487_b8824fd7e25d22fc_rsb_0_rs_https%3A%2F%2Fwww.digikey.com%2Fclassic%2FRegisteredUser%2FLogin.aspx%3FReturnUrl%3D%252fclassic%252fregistereduser%252fmydigikey.aspx%253fsite%253dus%2526lang%253den%26site%3Dus%26lang%3Den_rs_0; WT_FPC=id=36664e24-272b-40b3-9c92-2bce365f251d:lv=1380484152114:ss=1380483959856"
}
#postdata = {
# "__EVENTARGUMENT":"",
# "__EVENTTARGET":"",
# "__EVENTVALIDATION":"BcGpYOslmB3LGgxIVeQ+h35cvehYPZQcz1tM4jAlXqyYqV/g1blGRZnSJ4itN0YHd4C7aQtlJT0qWTL7vspdqVLEZtyljs5BJJuR+NhrIxCG0sdcfegZ1ZR1hdl/qIcNf1qpWfClikXsLCYWLe1N/Q6P1kU=",
# "__LASTFOCUS":"",
# "__SCROLLPOSITIONX":0,
# "__SCROLLPOSITIONY":0,
# "ctl00$ctl00$mainContentPlaceHolder$mainContentPlaceHolder$btnLogin":"Log In",
# "ctl00$ctl00$mainContentPlaceHolder$mainContentPlaceHolder$txtPassword":"xxxxx",
# "ctl00$ctl00$mainContentPlaceHolder$mainContentPlaceHolder$txtUsername":"xxxxx",
# }
#postdata=urllib.urlencode(postdata)
req = urllib2.Request(teurl,postdata,headers)
res = urllib2.urlopen(req)

没想到最后解决的方法居然这么简单,甚至可以使用动态ip的方式抓取。但不知道以后会不会出现cookie过期的情况。

至于获取不到cookie的情况,或许因为302页面跳转原因。

这样可以考虑用LWPCookieJar或MozillaCookieJar将获取的cookie存到文件中,再load()载入。

评论和分享

方法一 直接编辑配置文件

以开机自动启动pidgin为例:

这里有一种方法可以使你自己的任意程序,随着Gnome3桌面的登陆自动启动。

创建一个独立的文件在~/.config/autostart目录下,如果此目录不存在,则你应该自己创建它。(~代表当前用户的home目录)

为这个文件起个名字叫做:appname.desktop; appname 是指你想伴随Gnome3启动的程序的名字。

例如:我想 pidgin (一个免费的通讯工具)当Gnome3启动时自动启动,在我的Fedora系统中,所以我给这个文件命名:pidgin.desktop,并保存它在~/.config/autostart目录。

现在在这个文件中输入如下内容:

1
2
3
4
5
6
7
8
9
[Desktop Entry]
Type=Application
Exec=/usr/bin/pidgin
Hidden=false
X-GNOME-Autostart-enabled=true
Name[en_US]=pidgin
Name=pidgin
Comment[en_US]=pidgin comm tool
Comment=pidgin comm tool

保存并关闭文件。就这些。下次当你启动Gnome3,这个程序就自动启动了.

方法二,图形方式一个快捷的方式增加自动启动程序

Alt+F2 然后在运行输入框中输入: gnome-session-properties 然后回车。

然后点 “添加”,并填写相应信息即可。

其实这只是一个图形化的方式,其本质仍然和上面那个方案是相同的,只是更加方便了。

去查看 ~/.config/autostart 目录的文件,就会发现你新添加的东西。

评论和分享

python traceback模块

发布在 Python

如果我们再程序中写出一个会引发异常的代码,比如1/0,就会引发一个异常,程序中止。

输出:

1
2
3
4
5
Traceback (most recent call last):
File "trackbacks.py", line 1, in <module>
1/0
ZeroDivisionError: integer division or modulo by zero
Shell 已返回1

为了程序的健壮性,我们常常加入异常处理。

1
2
3
4
try:
1/0
except:
print "error"

这样,程序就会输出error后正常退出。那么为了能处理异常而又看见异常信息的话就要用到traceback模块了:

1
2
3
4
5
6
import traceback
try:
1/0
except:
print "error"
print traceback.format_exc()

结果:

1
2
3
4
5
error
Traceback (most recent call last):
File "trackbacks.py", line 4, in <module>
1/0
ZeroDivisionError: integer division or modulo by zero

也可以把信息输出到文件中,具体看 http://docs.python.org/2/library/traceback.html

评论和分享

python lxml模块

发布在 Python

继续分析那个爬虫程序,今天从中学习了lxml模块的基本使用。这个模块用来解析XML、HTML内容,据说速度上秒杀了”美丽的汤”。

官网: http://lxml.de/api/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#coding=utf-8
'''
Created on 2013年11月13日
学习使用lxml
@author: dear_shen
'''
from lxml import etree
if __name__ == '__main__':
broken_str = '''
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<title>i'm title</title>
</head>
<body>
<div style="text-align: center">
<b>is a test

</body>
</html>
'''
html = etree.HTML(broken_str)
result = etree.tostring(html,pretty_print = True,method="html") #变成str
print type(result)
print result
div = html.xpath("//div") #从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
print type(div),len(div)
print type(div[0]) #div是一个包含所有div的列表,由于html代码比较简单我就直接访问了,真正抓取时候用for循环处理
print div[0].attrib.get("style","no this attribute")#取得style属性
b = html.xpath("//b")#取得为b的节点
print b[0].text#打印b节点内容

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<type 'str'>
<html>
<head>
<meta charset="utf-8">
<title>i'm title</title>
</head>
<body>
<div style="text-align: center">
<b>is a test
</b>

</body>
</html>
<type 'list'> 1
<type 'lxml.etree._Element'>
text-align: center
is a test

可以看出,自动修复了不规则的html代码并且获得了我们想要的数据。

http://lxml.de/api/lxml.html.clean.Cleaner-class.html

这个清理,默认会把page_structure=True,清理掉html、title等,所以使用时候要注意。

make_links_absolute(self, base_url=None, resolve_base_href=True)这个也很有用,提供一个base_url,将页面中所有url都转化为绝对(加上base_url)路径

至于div = html.xpath(“//div”)为什么用//,这个和xpath有关,下面列出了最有用的路径表达式:

表达式描述
nodename选取此节点的所有子节点。
/从根节点选取。
//从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
.选取当前节点。
..选取当前节点的父节点。
@选取属性。

在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:

路径表达式结果
bookstore选取 bookstore 元素的所有子节点。
/bookstore选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!
bookstore/book选取属于 bookstore 的子元素的所有 book 元素。
//book选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。
//@lang选取名为 lang 的所有属性。

评论和分享

记得学操作系统这门课的时候就打算自己写一个,居然一眨眼过了一年才写,真是对不起老师阿!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#coding=utf-8
'''
Created on 2013年11月13日
生产者与消费者python版
@author: dear_shen
'''
import threading
import Queue
import time
condition = threading.Condition() #设置条件变量
class Producter(threading.Thread):
def __init__(self,puc_pool,i):
threading.Thread.__init__(self)
self.puc_pool = puc_pool
self.setName("生产者"+str(i))
def run(self):
while True:
if condition.acquire():
if self.puc_pool.qsize() < 10:
print "%s 开始生产产品 %s" % (self.name,self.puc_pool.qsize()+1)
self.puc_pool.put(self.name+"-"+str(self.puc_pool.qsize()+1))
condition.notify() #唤醒阻塞的线程
else:
print "产品数量多于10个,停止生产,生产者休息!"
condition.wait() #阻塞自己
condition.release()
time.sleep(1)
class Consumer(threading.Thread):
def __init__(self,puc_pool,i):
threading.Thread.__init__(self)
self.puc_pool = puc_pool
self.setName("消费者"+str(i))
def run(self):
while True:
if condition.acquire():
if self.puc_pool.qsize()>0:
tem = self.puc_pool.get()
print "%s 使用了产品 %s,还剩%s个" % (self.name,tem,self.puc_pool.qsize())
self.puc_pool.task_done() #产品队列中减少一个
condition.notify()
else:
print "没有产品了,消费者休息!"
condition.wait()
condition.release()
time.sleep(2)
if __name__ == "__main__":
print "main begin"
puc = Queue.Queue()
for i in range(2):
p = Producter(puc,i)
p.setDaemon(True)
p.start()
time.sleep(5)
for i in range(5):
c = Consumer(puc,i)
c.setDaemon(True)
c.start()
puc.join()#当产品队列为空时候再打印main over,如果注释掉了上面的setDaemon,虽然打印了main over,但是子线程并没有结束。
print "main over"

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
main begin
生产者0 开始生产产品 1
生产者1 开始生产产品 2
生产者0 开始生产产品 3
生产者1 开始生产产品 4
生产者0 开始生产产品 5
生产者1 开始生产产品 6
生产者0 开始生产产品 7
生产者1 开始生产产品 8
生产者0 开始生产产品 9
生产者1 开始生产产品 10
产品数量多于10个,停止生产,生产者休息!
消费者0 使用了产品 生产者0-1,还剩9个
生产者0 开始生产产品 10
消费者1 使用了产品 生产者1-2,还剩9个
消费者2 使用了产品 生产者0-3,还剩8个
消费者3 使用了产品 生产者1-4,还剩7个
消费者4 使用了产品 生产者0-5,还剩6个
生产者0 开始生产产品 7
生产者1 开始生产产品 8
消费者0 使用了产品 生产者1-6,还剩7个
消费者4 使用了产品 生产者0-7,还剩6个
消费者2 使用了产品 生产者1-8,还剩5个
消费者1 使用了产品 生产者0-9,还剩4个
消费者3 使用了产品 生产者1-10,还剩3个
生产者0 开始生产产品 4
生产者1 开始生产产品 5
生产者0 开始生产产品 6
生产者1 开始生产产品 7
消费者0 使用了产品 生产者0-10,还剩6个
消费者4 使用了产品 生产者0-7,还剩5个
消费者2 使用了产品 生产者1-8,还剩4个
消费者3 使用了产品 生产者0-4,还剩3个
消费者1 使用了产品 生产者1-5,还剩2个
生产者0 开始生产产品 3
生产者1 开始生产产品 4
生产者0 开始生产产品 5
生产者1 开始生产产品 6
消费者4 使用了产品 生产者0-6,还剩5个
消费者0 使用了产品 生产者1-7,还剩4个
消费者3 使用了产品 生产者0-3,还剩3个
消费者2 使用了产品 生产者1-4,还剩2个
消费者1 使用了产品 生产者0-5,还剩1个
生产者1 开始生产产品 2
生产者0 开始生产产品 3
生产者1 开始生产产品 4
生产者0 开始生产产品 5
消费者4 使用了产品 生产者1-6,还剩4个
消费者0 使用了产品 生产者1-2,还剩3个
消费者1 使用了产品 生产者0-3,还剩2个
消费者3 使用了产品 生产者1-4,还剩1个
生产者0 开始生产产品 2
生产者1 开始生产产品 3
消费者2 使用了产品 生产者0-5,还剩2个
生产者0 开始生产产品 3
生产者1 开始生产产品 4
消费者4 使用了产品 生产者0-2,还剩3个
消费者3 使用了产品 生产者1-3,还剩2个
消费者1 使用了产品 生产者0-3,还剩1个
消费者0 使用了产品 生产者1-4,还剩0个
main over

因为使用了setDaemon(True),所以看不见消费者else分支的输出,想看效果的话把其改成False即可。

评论和分享

原文链接找不到了,故整理格式后记录于此。

常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'''''  
Created on 2012-9-7
@author: walfred
@module: thread.ThreadTest3
@description:
'''
import threading
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
print "I am %s" % (self.name)
if __name__ == "__main__":
for i in range(0, 5):
my_thread = MyThread()
my_thread.start()

name相关

你可以为每一个thread指定name,默认的是Thread-No形式的,如上述实例代码打印出的一样:

1
2
3
4
5
I am Thread-1
I am Thread-2
I am Thread-3
I am Thread-4
I am Thread-5

当然你可以指定每一个thread的name,这个通过setName方法,代码:

1
2
3
def __init__(self):  
threading.Thread.__init__(self)
self.setName("new" + self.name)

join方法

join方法原型如下,这个方法是用来阻塞当前上下文,直至该线程运行结束。

setDaemon方法

当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程就分兵两路,当主线程完成想退出时,会检验子线 程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是,只要主线程完成了,不管子线程是否完成,都要和主线程一起退 出,这时就可以用setDaemon方法,并设置其参数为True。

使用Lock互斥锁

假设各个线程需要访问同一公共资源,我们的代码该怎么写?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'''''
Created on 2012-9-8
@author: walfred
@module: thread.ThreadTest3
'''
import threading
import time
counter = 0
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global counter
time.sleep(1);
counter += 1
print "I am %s, set counter:%s" % (self.name, counter)
if __name__ == "__main__":
for i in range(0, 200):
my_thread = MyThread()
my_thread.start()

解决上面的问题,我们兴许会写出这样的代码,我们假设跑200个线程,但是这200个线程都会去访问counter这个公共资源,并对该资源进行处理(counter += 1),代码看起来就是这个样了,但是我们看下运行结果:

1
2
3
4
I am Thread-69, set counter:64I am Thread-73, set counter:66I am Thread-74, set counter:67I am Thread-75, set counter:68
I am Thread-76, set counter:69I am Thread-78, set counter:70I am Thread-77, set counter:71I am Thread-58, set counter:72
I am Thread-60, set counter:73I am Thread-62, set counter:74I am Thread-66, set counter:75I am Thread-70, set counter:76
I am Thread-72, set counter:77I am Thread-79, set counter:78I am Thread-71, set counter:78

打印结果我只贴了一部分,从中我们已经看出了这个全局资源(counter)被抢占的情况,问题产生的原因就是没有控制多个线程对同一资源的访问, 对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。在开发过程中我们必须要避免这种情况,那怎么避免?这就用到了我们在综述中提 到的互斥锁了。

互斥锁概念

Python编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为” 互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。在Python中我们使用threading模块提供的Lock类。
我们对上面的程序进行整改,为此我们需要添加一个互斥锁变量mutex = threading.Lock(),然后在争夺资源的时候之前我们会先抢占这把锁mutex.acquire(),对资源使用完成之后我们在释放这把锁 mutex.release()。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'''
Created on 2012-9-8
@author: walfred
@module: thread.ThreadTest4
'''
import threading
import time
counter = 0
mutex = threading.Lock()
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global counter, mutex
time.sleep(1);
if mutex.acquire():
counter += 1
print "I am %s, set counter:%s" % (self.name, counter)
mutex.release()
if __name__ == "__main__":
for i in range(0, 100):
my_thread = MyThread()
my_thread.start()

同步阻塞

当一个线程调用Lock对象的acquire()方法获得锁时,这把锁就进入“locked”状态。因为每次只有一个线程1可以获 得锁,所以如果此时另一个线程2试图获得这个锁,该线程2就会变为“blo同步阻塞状态。直到拥有锁的线程1调用锁的release()方法释放锁之后, 该锁进入“unlocked”状态。线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
进一步考虑
通过对公共资源使用互斥锁,这样就简单的到达了我们的目的,但是如果我们又遇到下面的情况:

  1. 遇到锁嵌套的情况该怎么办,这个嵌套是指当我一个线程在获取临界资源时,又需要再次获取;
  2. 如果有多个公共资源,在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源;

上述这两种情况会直接造成程序挂起,即死锁,下面我们会谈死锁及可重入锁RLock。

死锁的形成

如果有多个公共资源,在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,这会引起什么问题?

死锁概念

所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生 了死锁,这些永远在互相等待的进程称为死锁进程。 由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'''''
Created on 2012-9-8
@author: walfred
@module: thread.TreadTest5
'''
import threading
counterA = 0
counterB = 0
mutexA = threading.Lock()
mutexB = threading.Lock()
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
self.fun1()
self.fun2()
def fun1(self):
global mutexA, mutexB
if mutexA.acquire():
print "I am %s , get res: %s" %(self.name, "ResA")
if mutexB.acquire():
print "I am %s , get res: %s" %(self.name, "ResB")
mutexB.release()
mutexA.release()
def fun2(self):
global mutexA, mutexB
if mutexB.acquire():
print "I am %s , get res: %s" %(self.name, "ResB")
if mutexA.acquire():
print "I am %s , get res: %s" %(self.name, "ResA")
mutexA.release()
mutexB.release()
if __name__ == "__main__":
for i in range(0, 100):
my_thread = MyThread()
my_thread.start()

代码中展示了一个线程的两个功能函数分别在获取了一个竞争资源之后再次获取另外的竞争资源,我们看运行结果:

1
2
3
I am Thread-1 , get res: ResA
I am Thread-1 , get res: ResB
I am Thread-2 , get res: ResAI am Thread-1 , get res: ResB

可以看到,程序已经挂起在那儿了,这种现象我们就称之为”死锁“。

避免死锁

避免死锁主要方法就是:正确有序的分配资源,避免死锁算法中最有代表性的算法是Dijkstra E.W 于1968年提出的银行家算法

可重入锁RLock

考虑这种情况:如果一个线程遇到锁嵌套的情况该怎么办,这个嵌套是指当我一个线程在获取临界资源时,又需要再次获取。

根据这种情况,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
'''''
Created on 2012-9-8
@author: walfred
@module: thread.ThreadTest6
'''
import threading
import time
counter = 0
mutex = threading.Lock()
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global counter, mutex
time.sleep(1);
if mutex.acquire():
counter += 1
print "I am %s, set counter:%s" % (self.name, counter)
if mutex.acquire():
counter += 1
print "I am %s, set counter:%s" % (self.name, counter)
mutex.release()
mutex.release()
if __name__ == "__main__":
for i in range(0, 200):
my_thread = MyThread()
my_thread.start()

这种情况的代码运行情况如下:

1
I am Thread-1, set counter:1

之后就直接挂起了,这种情况形成了最简单的死锁。

那有没有一种情况可以在某一个线程使用互斥锁访问某一个竞争资源时,可以再次获取呢?在Python中为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。这个RLock内部维护着一个Lock和一个counter变量,counter 记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上 面的例子如果使用RLock代替Lock,则不会发生死锁:mutex = threading.RLock()

使用Condition实现复杂同步

目前我们已经会使用Lock去对公共资源进行互斥访问了,也探讨了同一线程可以使用RLock去重入锁,但是尽管如此我们只不过才处理了一些程序中简单的同步现象,我们甚至还不能很合理的去解决使用Lock锁带来的死锁问题。所以我们得学会使用更深层的解决同步问题。

Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了wait和notify方法。

使用Condition的主要方式为:线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改 变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。

下面我们通过很著名的“生产者-消费者”模型来来演示下,在Python中使用Condition实现复杂同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
'''''
Created on 2012-9-8
@author: walfred
@module: thread.TreadTest7
'''
import threading
import time
condition = threading.Condition()
products = 0
class Producer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global condition, products
while True:
if condition.acquire():
if products < 10:
products += 1;
print "Producer(%s):deliver one, now products:%s" %(self.name, products)
condition.notify()
else:
print "Producer(%s):already 10, stop deliver, now products:%s" %(self.name, products)
condition.wait();
condition.release()
time.sleep(2)
class Consumer(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global condition, products
while True:
if condition.acquire():
if products > 1:
products -= 1
print "Consumer(%s):consume one, now products:%s" %(self.name, products)
condition.notify()
else:
print "Consumer(%s):only 1, stop consume, products:%s" %(self.name, products)
condition.wait();
condition.release()
time.sleep(2)
if __name__ == "__main__":
for p in range(0, 2):
p = Producer()
p.start()
for c in range(0, 10):
c = Consumer()
c.start()

代码中主要实现了生产者和消费者线程,双方将会围绕products来产生同步问题,首先是2个生成者生产products ,而接下来的10个消费者将会消耗products,代码运行如下:

1
2
3
4
5
6
7
8
9
10
11
12
Producer(Thread-1):deliver one, now products:1
Producer(Thread-2):deliver one, now products:2
Consumer(Thread-3):consume one, now products:1
Consumer(Thread-4):only 1, stop consume, products:1
Consumer(Thread-5):only 1, stop consume, products:1
Consumer(Thread-6):only 1, stop consume, products:1
Consumer(Thread-7):only 1, stop consume, products:1
Consumer(Thread-8):only 1, stop consume, products:1
Consumer(Thread-10):only 1, stop consume, products:1
Consumer(Thread-9):only 1, stop consume, products:1
Consumer(Thread-12):only 1, stop consume, products:1
Consumer(Thread-11):only 1, stop consume, products:1

另外:Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个 RLock;除了notify方法外,Condition对象还提供了notifyAll方法,可以通知waiting池中的所有线程尝试acquire 内部锁。由于上述机制,处于waiting状态的线程只能通过notify方法唤醒,所以notifyAll的作用在于防止有线程永远处于沉默状态。

使用Event实现线程间通信

使用threading.Event可以实现线程间相互通信,之前我们已经初步实现了线程间通信的基本功能,但是更为通用的一种做法是使用threading.Event对象。

使用threading.Event可以使一个线程等待其他线程的通知,我们把这个Event传递到线程对象中,Event默认内置了一个标志,初始值为 False。一旦该线程通过wait()方法进入等待状态,直到另一个线程调用该Event的set()方法将内置标志设置为True时,该Event会 通知所有等待状态的线程恢复运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    '''''
Created on 2012-9-9
@author: walfred
@module: thread.TreadTest8
'''
import threading
import time
class MyThread(threading.Thread):
def __init__(self, signal):
threading.Thread.__init__(self)
self.singal = signal
def run(self):
print "I am %s,I will sleep ..."%self.name
self.singal.wait()
print "I am %s, I awake..." %self.name
if __name__ == "__main__":
singal = threading.Event()
for t in range(0, 3):
thread = MyThread(singal)
thread.start()
print "main thread sleep 3 seconds... "
time.sleep(3)
singal.set()
```
运行效果如下:

I am Thread-1,I will sleep …
I am Thread-2,I will sleep …
I am Thread-3,I will sleep …
main thread sleep 3 seconds…
I am Thread-1, I awake…I am Thread-2, I awake…
I am Thread-3, I awake…
```

评论和分享

作者的图片

Roy

微信公众号:hi-roy


野生程序猿


China