Chinaunix首页 | 论坛 | 博客
  • 博客访问: 10959
  • 博文数量: 8
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 90
  • 用 户 组: 普通用户
  • 注册时间: 2013-08-27 10:50
文章分类
文章存档

2013年(8)

我的朋友
最近访客

分类: 其他平台

2013-09-10 17:43:33

57. 58. self.name = name 59. 60. # Set the maxsize to the parameter, or to a random maxsize. 61. if maxsize is None: 62. self.maxsize = random.randint(4, 10) 63. 64. # Have a small chance of a super long worm. 65. if random.randint(0,4) == 0: 66. self.maxsize += random.randint(10, 20) 67. else: 68. self.maxsize = maxsize 69. 70. # Set the color to the parameter, or to a random color. 71. if color is None: 72. self.color = (random.randint(60, 255), random.randint(60, 255), random.randint(60, 255)) 73. else: 74. self.color = color 75. 76. # Set the speed to the parameter, or to a random number. 77. if speed is None: 78. self.speed = random.randint(20, 500) # wait time before movements will be between 0.02 and 0.5 seconds 79. else: 80. self.speed = speed

上面的代码设置一个有随机大小、颜色和速度的虫子,除非参数指定了值,它都用默认的值。

26. # Constants for some colors. 27. # R G B 28. WHITE = (255, 255, 255) 29. BLACK = ( 0, 0, 0) 30. DARKGRAY = ( 40, 40, 40) 31. BGCOLOR = BLACK # color to use for the background of the grid 32. GRID_LINES_COLOR = DARKGRAY # color to use for the lines of the grid RGB tuples are kind of hard to read, so I usually set up some constants for them. 33. 34. # Calculate total pixels wide and high that the full window is 35. WINDOWWIDTH = CELL_SIZE * CELLS_WIDE 36. WINDOWHEIGHT = CELL_SIZE * CELLS_HIGH 37. 38. UP = 'up' 39. DOWN = 'down' 40. LEFT = 'left' 41. RIGHT = 'right'

还有更多的简单常量。我使用如DOWN和RIGHT这些常量代替字符串“down”和“right”因为如果我错误使用使用常量(如DOWN)然后Python会直接由于一个NameError异常而崩溃。这比我用错误的值如“down”更好,它不会直接程序崩溃但以后会导致一些bug,让它更难去跟踪。

GRID_LOCK = threading.Lock()

在threading模块里的Lock类有acquire()和release()方法。我们会新建一个Lock对象和把它存放在名为GRID_LOCK的全局变量里。(因为类似网格的屏幕和被单元占据的状态值会存储在名为GRID的全局变量里。这两种方式有点意外。)

如果GRID[x][y]设置为None,然后单元就不会被占据。否则的话,GRID[x][y]会设置为一个RGB三元组。(这个信息对在屏幕上画网格的时候有用。)

# A global variable that the Worm threads check to see if they should exit. WORMS_RUNNING = True

我没的WORMS_RUNNING全局变量通常由 worm线程来检查是否应该退出。调用sys.exit()不会停止程序,因为它只是退出调用它的线程。只要有其他线程仍然在运行程序还会继续。在我们程序里的主线程(负责Pygame渲染和时间处理)会在它调用pygame.quit()和sys.exit()前设置WORMS_RUNNING为 False,直到实际最后的线程退出然后程序终止,它就会退出。

大多数多线程程序共享访问相同的变量,但这就是棘手的东西。

点击查看大版本图片

询问消费者要哪个位置。 检查列表看下座位是不是可以用。 获取该座位的票。 从列表上移出改座位。

然而,如果我们把范围从100改到100000,我可能期待第二个数字是200000,一旦每个线程为TOTAL增加100000同时有两个线程。但当我们运行程序的时候,一些像这样的东西会出现(你的数字可能有点不同):

这里是一个常用的比喻:告诉你有两个售票机器人。它们的任务很简单:

一旦完成对共享变量的操作,线程就会“释放”这个锁。这样其他等待这个锁的线程就能获取它了。

在我们进入Threadworm代码前,先看下一个废弃了的简单多线程程序:

当run()的调用结束时, (或者sys.exit()这个函数在这个线程中被调用),这个线程会被销毁。在一个程序结束之前,它里面所有的线程需要被销毁。只要这个程序中有一个还在运行的线程,这个程序就不会结束。

这问题是由于代码的TOTAL=TOTAL+1这行不是原子的。上下文切换切换可以刚好在这行执行的中间发生。我们需要在代码周围加上锁让它成为一个原子操作。

import threading TOTAL = 0 MY_LOCK = threading.Lock() class CountThread(threading.Thread): def run(self): global TOTAL for i in range(100000): MY_LOCK.acquire() TOTAL = TOTAL + 1 MY_LOCK.release() print('%s\n' % (TOTAL)) a = CountThread() b = CountThread() a.start() b.start()

当我们运行这段代码时候,下面就是输出结果了(你的第一个数字可能有一点不同):

46. # A global variable that the Worm threads check to see if they should exit. 47. WORMS_RUNNING = True

随着一个线程在运行,程序会继续地执行。负责渲染屏幕的主线程会检查用户什么时候点击了窗口上的关闭按钮或者按ESC键,所以它需要一个方法来告诉虫子线程退出。我们会编写虫子线程代码来定期检查 WORMS_RUNNING。如果WORMS_RUNNING是False的,线程就会终止自己。

理性地思考一会儿。 拿起你左边的叉子。 等待你右边的叉子能用。 拿起右边的叉子。 吃面条。 放下叉子。 跳到步骤1.

在多线程编程中,一个防范bug的办法是使用锁。在一个线程读取或者修改该某个共享变量前,它先试图获得一个锁。如果获得了这个锁,这个线程将继续对这个共享变量进行读写。反之,它将一直等待直到这个锁再次可用。

上面的问题会发生是因为机关两个机器人(或者说,两个线程)都在独立执行,他们两者都在读写一个共享的列表(或者说,一个变量)。你的程序可能很难去修复这种很难重现的bug,因为Python的线程执行切换具有非确定性,那就是,程序每次运行都在做不同的东西。我们不习惯变量里的数据“魔术地”从一行转到下一个仅仅是因为线程在他们之间执行。

需要明白的东西:使用start()新开一个线程,但是这个线程会执行run()里面的代码。我们不需要自己实现start(),因为它是从 threading.Thread中继承的。既然线程要执行run()中的代码,我们要自己实现run()。

49. class Worm(threading.Thread): # "Thread" is a class in the "threading" module. 50. def __init__(self, name='Worm', maxsize=None, color=None, speed=None): 这里是我们的Worm类,它是threading.Thread的子类。每个虫子都有一个名字(在虫子碰撞的时候会显示,帮助我们分辨出是那个线程崩溃),一个大小,颜色和速度。都提供了默认值,但如果喜欢我们能够指定这些属性的值。

56. threading.Thread.__init__(self) # since we are overriding the Thread class, we need to first call its __init__() method.

一旦我们重载了__init__()方法,我们需要调用父类的__init__()方法以便于它能够初始化所有线程内容。(我们不需要知道它怎么工作,只要记住调用它。)

写代码时,如果忘记对锁进行释放,就可能引入bug。这将导致死锁情况的发生,因为另外一个等待该锁释放的线程会一直挂在那里无事可做。   Python中的线程

143294 149129

第二个数字不是200000!它比实际数字要小。这样会发生的原因是因为我们没有在代码读写TOTAL变量周围使用锁,而它是在多线程中共享的。

这三个步骤用到了你电脑上的不同资源:下载用到了网络,排序用到了CPU,保存文件用到了硬盘。同时,这三个操作都可能被延缓。例如,你下在文件的服务器可能很慢,或者你的带宽很小。

但是事实上,它们并不是同时的。你的手指在交替着移动。拥有多核心(multicore)的处理器可以真正意义上的同时执行两条指令,但是Python程序有一个叫做 GIL (全局解释器锁 global interpreter lock) 东西,它会限制Python程序单核执行。

一件要了解的事情是Pygame用一个三个整数的元组来代表颜色。每个这些整数的范围是从0到255并且代表了RGB(红-绿-蓝)颜色的值。所以(0,0,0)是黑和(255,255,255)是白,同时(255,0,0)是红和(255,0,255)是紫等。

import threading

Python的线程库名为threading的模块,所以首先要导入这个模块。

  

class Worm(threading.Thread): def __ init__(self, name='Worm', maxsize=None, color=None, speed=None): threading.Thread.__init__(self) self.name = name 线程代码必需充Thread类的子类开始(在threading模块里的)。我们的线程子类会名为Worm,因为它负责控制虫子。 但由于我们的Worm类会先使用我们需要调用threading.Thread的___init__()方法,所以你不需要一个__init()__函数。同样也可以选择重载该命名方法。我们的__init__()函数使用字符串"Worm'作为默认,但我们能够提供每个线程一个独立的名字。Python会在线程崩溃的时候在错误信息里显示线程的名字。

GRID_LOCK.acquire() # ...some code that reads or modifies GRID... GRID_LOCK.release()

在我们读获取修改GRID变量里的值前,线程代码应该去尝试申请锁。如果锁不可用,方法对acquire()调用不会返回同时直到锁能用前都会“阻塞”。线程在这种情况下会展厅。这样,我们就知道在acquire()后的代码的调用只会在线程申请到锁后才发生。

(在计算机科学里,“头”通常指向列表或队列的第一个元素,而“尾”指向任何一个在头后面的元素。所以我使用“屁股”来指向最后一个元素。我同样有点笨。)

82. GRID_LOCK.acquire() # block until this thread can acquire the lock 83. 84. while True: 85. startx = random.randint(0, CELLS_WIDE - 1) 86. starty = random.randint(0, CELLS_HIGH - 1) 87. if GRID[startx][starty] is None: 88. break # we've found an unoccupied cell in the grid 89. 90. GRID[startx][starty] = self.color # modify the shared data structure 91. 92. GRID_LOCK.release()

我们需要决定一个虫子的随机开始的位置。要让这个简单点,所有虫子都从一个身体部分来开始同时增长直到他们到达最大值。但我们需要确保网格上的随机位置还没被占据。这取决对GRID全局变量的读和修改,所以我们需要在做之前申请和释放GRID_LOCK锁。

一旦我们创建两个CountThread对象,无论哪个完成了都会显示200.每个线程为TOTAL增加100,并且有两个线程。当我们运行这个程序的时候,我们会看到:

TOTAL = TOTAL + 1

如果TOTAL设置为99,然后你会认为TOTAL+1等于99+1然后就是100了,然后100作为新的值存入到TOTAL里。然后在下一次迭代,TOTAL+1会是100+1或101,它都会作为新的值存到TOTAL里。

在对GRID变量的操作完成之后,使用release方法释放这个锁:

这是一篇为初学者准备的关于 线程 和Python中的多线程编程的指南。 如果你有一些 类(class)的基础知识 (什么是类,如何定义方法(method),还有方法总是将self作为他的第一个参数,子类是什么以及子类如何从父类继承一个方法,等等)这篇指南会对你有所帮助。 这里有一篇较为深入地介绍类(class)的指南。

如果你知道线程相关的知识,那就跳过这一章节,看看线程在Python中如何使用。

在一段代码中获取和释放一个锁,可以保证当前线程在执行这段代码的时候,其他线程不会执行这段代码。这会让这段代码变成“原子的”,因为这段代码总会被当成一个整体。

9. import random, pygame, sys, threading 10. from pygame.locals import * 11. 12. # Setting up constants 13. NUM_WORMS = 24 # the number of worms in the grid 14. FPS = 30 # frames per second that the program runs 15. CELL_SIZE = 20 # how many pixels wide and high each "cell" in the grid is 16. CELLS_WIDE = 32 # how many cells wide the grid is 17. CELLS_HIGH = 24 # how many cells high the grid is

代码的最上部分导入一些我们程序需要的模块同时定义一些常量值。会很容易地修改这些常量。增加或减少FPS的值不会影响到虫子跑得有多快,它只是改变屏幕刷新频率。如果你把这个值设置得很低,它看起来这些虫子都会瞬间移动,因为它们在每次屏幕刷新的时候移动了多个位置。

20. GRID = [] 21. for x in range(CELLS_WIDE): 22. GRID.append([None] * CELLS_HIGH)

全局变量GRID会包含网格状态跟踪数据。它是一些简单的列表所以GRID[x][y]会指向X和Y坐标的单元。(在编程里,(0,0)远点在屏幕左上方。X增加会往右移动(就像在数学课里)但Y增加会往下移动。)

 

43. HEAD = 0 44. BUTT = -1 # negative indexes count from the end, so -1 will always be the last index

每个虫子会用一个像{'x':42,'y':7}的字典列表来表示。每一个字典都代表了虫子身体的一部分。列表前面的字典(索引为0)是头部,而在最后的字典(索引-1,使用Python的负数索引来从后面开始计数)是虫子的屁股。

看这行:

24. GRID_LOCK = threading.Lock() # pun was not intended

第24行创建一个Lock对象,我们的线程代码会在读或修改GRID的时候申请和释放这个锁。

一个简单的多线程例子

举个例子,你要写个函数,这个函数会下载一个内容全是名字的文件,然后将这个文件的内容排序,然后将排序好的内容存为另一个文件。如果这里有上百个这样的文件,那么你可能会在一个循环中调用这个函数来处理每个文件:下载,排序,保存,下载,排序,保存,下载,排序,保存...

这种情况先,使用多个线程,每个线程处理一个文件是一个比较明智的选择。这不仅能更好的利用你的带宽,而且当你的CPU工作的时候,网络也在工作。这将更有效的利用你的电脑。

一个顾客问机器A要42号座位的票。机器A从列表中检查和发现座位可以用,因此它获取到那张票。但在机器A能从列表中删除改座位的前,机器B被不同顾客询问42号座位。机器B检查列表也看到了座位仍然可以用,所以它尝试获取到这个座位的票。但是机器B不能找到42号座位的票。这计算不了了,同时机器B的电子大脑也爆炸了。机器A在后来把42号座位的票从列表上删除了。

上面的虫子能用这样的列表能表示:[{'x': 7, 'y': 2}, {'x': 7, 'y': 3}, {'x': 7, 'y': 4}, {'x': 8, 'y': 4}, {'x': 9, 'y': 4}, {'x': 10, 'y': 4}, {'x': 11, 'y': 4}, {'x': 11, 'y': 3}, {'x': 11, 'y': 2}]

这篇教程的代码可以从此处下载:threadworms.py 或者GitHub。这份代码兼容Python2和Python3, 另外运行该代码需要安装Pygame.

然而,使用多个线程就像将第二跟手指放到屏幕上。每个手指还是像之前那样移动,但是它们现在是同时在移动。

这也存在死锁的问题,通常用哲学家就餐问题的比喻来解释。五个哲学家围坐一个桌子吃意大利面条,但需要两个叉子。在每个哲学家之间有一个叉子(总共有5个)。哲学家用这个方法来吃面条:

import threading TOTAL = 0 class CountThread(threading.Thread): def run(self): global TOTAL for i in range(100): TOTAL = TOTAL + 1 print('%s\n' % (TOTAL)) a = CountThread() b = CountThread() a.start() b.start()

这个程序定义一个叫做CountThread的新类。当一个CountThread对象的start()方法被调用,一个会循环100次同时在每次循环迭代中为TOTAL全局变量的值加1的新的线程会被创建(在变量之间共享的)。

Python的解释器会一会儿执行一个线程,一会儿执行另一个线程。但是这切换的速度如此之快,快的让你根本无法察觉,以至于这些线程看起来像是同时执行。

这里是上述在我们的threadworms.py程序里线程相关的代码:

我们用到的例子是 “贪吃蛇” 的克隆,它有很多条在一个格子状的区域前行的蠕虫,每一条虫子在一个单独的线程里运行。

新的代码修复了这个问题:

还存在一种被称为 活锁的情况。当这种情况发生时,所有的线程都让出资源,导致任务不能继续进行下去。就像在大厅里迎面走近的两个人,他们都站到一边,等待对方先过去,结果两个人都卡住了。然后他们又同时试图走到对面,又互相阻碍了对方。他们持续地这样让开-走近,直到他们都筋疲力尽。 在多线程编程中,还可能存在其他一些问题,比如饥饿(不是真的肚子饿的问题,只是大家都这么叫它)。这些问题在计算机科学中普遍归类于" 并行性"这个范畴。不过在此我们只会处理一个简化的实例。   锁  

OK,现在让我们来写一段python程序来说明如何使用线程和锁。这段程序基于一个贪吃蛇游戏,是我在拙著《Making Games with Python & Pygame》第六章中克隆的一个版本。这条蛇只会在屏幕上跑来跑去,不会吃苹果。另外,程序中有不止一条蛇。每一条蛇由不同的线程控制。共享变量中的数据结构记录了屏幕上哪个位置(在这个程序中被成为"格子")被一条蛇占据.如果一条蛇已经在某个格子里了,则其他蛇不能前进到此处并占据这个格子。我们将使用锁来保证两条蛇不会占据同一个格子。

从实际上他们会和旁边的人共享叉子(我不喜欢),这方法看起来似乎能有效。但马上或者稍后桌子上每个人最后都会拿着左边的叉子在手挡同时等待右边的叉子,。但因为每个人都拿着他们旁边的人等待的叉子同时也不会在他们吃之前放下他们,这些哲学家就在一个死锁状态。他们会拿着左边的叉子在手上又永远不会拿到右边的叉子,所以他们永远不会吃到面条也用户不会放下他们左手上的叉子。哲学家都要饿死了(除了伏尔泰,它实际上是个机器人。没有意大利面条,他的电子大脑会爆炸)

是什么让多线程编程那么棘手?

线程是什么?为什么线程很有用?

def run(self): # 这里是线程代码

当Worm类(是一个threading.Thread 的字类)的start()方法被调用时,一个线程开始执行。我们不需要自己实现start()方法,因为它是从threading.Thread中继承的。 当调用start()时,会创建一个新的线程,这个新的线程会执行run()方法中的代码。不要直接调用run()方法,这样不会创建一个新的线程。

我打算用程序的threadworms_nocomments.py版本,因为它没有冗长的注释在里面。在每行的开头都包含了行号(它们不是实际Python源代码的一部分)。我跳过很多注释部分因为它们都是一些自我解释。在下面代码不需要你真正了解Pygame。Pygame只是负责创建窗口和画线和方块。

当从一个线程的执行切换到另外一个线程,这就是上下文切换。

回到售票机器人的比喻。一个机器人把座位列表拿起来 (这个列表就是锁),检查后发现客户要求的座位还在,于是把相应的票取出来,把这个座位从列表中删去。最后机器人把列表放回去的动作,就相当于“释放了这个锁“。如果另一个机器人需要查看座位列表但列表不在,它会一直等待直到座位列表再次可用。

当你运行一个普通的Python程序时,这个程序从第一行开始,一行接一行的的执行。循环和函数可能让程序上下跳转,但是给定一行代码的位置,你可以轻易地找到下一行从哪里执行。你可以把一根手指指到你的.py文件中,一行一行的追踪你的程序执行到哪里了。这就是单线程编程(single-threaded programming)。

解释Threadworms程序

199083 200000

第二个数字是20000告诉了我们TOTAL=TOTAL+1这行正确在它运行的200,000次里执行。

你可以在你的Python程序中开几十或者几百个线程(那真是好多的手指)。这并不能让你的程序快上几十上百倍(事实上这些线程还是在使用同一个CPU),但是它能让你的程序更强大,更高效。

CELL_SIZE代表屏幕上的网格的每个方块有多大(像素)。如果你想改变单元的数量,就修改CELLS_WIDE和CELLS_HIGH常量。

本指南的里代码可以在这里下载: threadworms.py ,或者从 GitHub。代码需要 Python 3 或 Python 2 ,同时也需要安装 Pygame 。

来自 Brad Montgomery的图片)

 

100 200

因为第一个数字是100,我们能说出一个线程在上下文切换发生前在整个循环里发生了什么。

所以,当start()被调用时,就是你将一根新的手指放到run()中开始追踪代码的时候。你的第一根手指在执行完start()后会继续追踪代码。

当然,在上面的例子,每个线程只做它自己独立的事情也不需要去和其他线程通信或同步任何东西。你可以只编写简单的下载-排序-写入程序的单线程版本同时独立地运行程序上百遍。(尽管它可能在每次打字和点击来运行每个程序来下载不同文件的时候有点痛苦。)

但在说TOTAL+1等于99+1时,执行切换到另外一个线程,它也是执行TOTAL=TOTAL+1那一行。在TOTAL里的值还是99,所以TOTAL+1在这第二个线程会等价于99+1。

然后,另外一个上下文切换发生的时候又回到第一个线程TOTAL=99+1的执行到中间的地方。整数100会分配到TOTAL。现在执行又切换到第二个线程。

在第二个线程,TOTAL=99+1大概要执行了。即使现在TOTAL是100,在第二个线程里的TOTAL+1这里已经等于是99+1了。所以第二个线程最终也把整数100分配到TOTAL。即使 TOTAL=TOTAL+1被执行了两次(每次又一个线程执行),但TOTAL的真实值只是加了1!

阅读(826) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~