18、Python之多线程
一、进程与线程概念
首先说一下进程和程序的区别:程序是静态的,是存在磁盘上的,而进程是在执行中的程序,是在内存中的。起初没有线程的概率,只有进程,一个进程它有独立的资源,这就好比我们把一个班级看做一个进程,黑板,桌椅都是这个班级进程的资源,别的进程(班级)无法享用。对于外界(CPU等)而言班级是一个进程,而实际上在班级中还有很多学生,他们是一个个独立的个体,共享着班级的资源,但由于外界并不知道,一旦班级这个进程被阻塞了,班级内部的学生都无法进行活动了,这显然不适合计算机的高效,因而产生了线程(学生)被外界所认可(cpu)。由此我们可以得出进程和线程的结论为:
1、进程是作为最小资源分配单位
2、线程是独立运行的最小单位(被cpu调用)
3、一个进程下有多个线程,且他们共享进程的资源
4、一个进程至少有一个线程
GIL(Global Interpreter Lock):全局解释器锁
可能有些人在学python之前就听说过,python有一个缺点就是无法执行多线程,这正是由于GIL的原因。但是,需要明确的一点是GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL
归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
二、Python中线程调用的2种方式
方式一、直接调用
1 import threading 2 def run(n): 3 print(n) 4 5 t1 = threading.Thread(target=run,args=("test1",)) 6 t2 = threading.Thread(target=run,args=("test2",)) 7 t1.start() 8 t2.start()
上面程序中一共有3个线程,t1,t2以及运行t1,t2的主线程,我们可以使用语句threading.activeCount()打印出当前活跃线程的数量。
方式二、继承式调用
1 import threading 2 3 class MyThread(threading.Thread): 4 def __init__(self,name): 5 super(MyThread,self).__init__() 6 self.__name = name 7 def run(self): 8 print(self.__name) 9 10 t1 = MyThread("test1") 11 t2 = MyThread("test2") 12 t1.start() 13 t2.start()
三、多线程中的注意点
1、多线程中,只有一个主线程,主线程可创建多个子线程,子线程一旦被创建后,就与主线程没有半毛钱的关系。因而我们将上面的代码稍微改造一下。
1 import threading,time 2 3 class MyThread(threading.Thread): 4 def __init__(self,name): 5 super(MyThread,self).__init__() 6 self.__name = name 7 def run(self): 8 time.sleep(1) 9 print("线程%s运行完毕" % self.__name) 10 11 t1 = MyThread("test1") 12 t2 = MyThread("test2") 13 t1.start() 14 t2.start() 15 print("主线程运行完毕")
运行结果为:
主线程运行完毕
线程test1运行完毕
线程test2运行完毕
由此可见,一旦主线程创建了子线程之后,将继续运行自己的代码,而子线程独立运行,但是有时候我们期望等待某个线程的执行结束,再运行主线程,这时候需要使用方法join()。
上述代码要想主线程在2个子线程执行完之后再执行,代码改造如下:
1 import threading,time 2 3 class MyThread(threading.Thread): 4 def __init__(self,name): 5 super(MyThread,self).__init__() 6 self.__name = name 7 def run(self): 8 time.sleep(1) 9 print("线程%s运行完毕" % self.__name) 10 11 t1 = MyThread("test1") 12 t2 = MyThread("test2") 13 t1.start() 14 t2.start() 15 t1.join() 16 t2.join() 17 print("主线程运行完毕")
2、当主线程创建一个守护线程的时,一旦主线程运行结束,守护线程立即结束。在创建线程时使用setDaemon(True)方法就可以将子线程设置为守护线程。
1 import threading,time 2 3 class MyThread(threading.Thread): 4 def __init__(self,name): 5 super(MyThread,self).__init__() 6 self.__name = name 7 def run(self): 8 time.sleep(1) 9 print("线程%s运行完毕" % self.__name) 10 11 t1 = MyThread("test1") 12 t2 = MyThread("test2") 13 t1.setDaemon(True) #将线程t1设置为守护线程 14 t2.setDaemon(True) #将线程t2设置为守护线程 15 t1.start() 16 t2.start() 17 print("主线程运行完毕")
运行结果为:主线程运行完毕,子线程还未运行完毕,就结束了。
3、线程锁:假设有这样一个程序:
1 import threading,time,random 2 3 class MyThread(threading.Thread): 4 count = 0 5 def __init__(self,name): 6 super(MyThread,self).__init__() 7 self.__name = name 8 def run(self): 9 time.sleep(random.randrange(1,10)) 10 MyThread.count += 1 11 print("线程%s运行完毕" % self.__name) 12 @classmethod 13 def print_count(self): 14 print(MyThread.count) 15 16 obj_list = [] 17 for i in range(50): 18 t = MyThread(i) 19 t.start() 20 obj_list.append(t) 21 for obj in obj_list: 22 obj.join() 23 MyThread.print_count()#打印总共的线程数 24 print("主线程运行完毕")
50个线程一旦被创建后,都会去修改count的值,如果不加锁的话,就会导致count最终的值是错误的(据说python三种做了处理,但是没有官方声明)。为了使count的值最终正确,我们需要给这段代码加一个锁。假设的三步骤:1、申请一把锁:lock = threading.Lock() 2、加锁 lock.acquire() 3、还锁 lock.release()。
所以正确的代码应该是:
1 import threading,time,random 2 3 lock = threading.Lock() #申请锁 4 class MyThread(threading.Thread): 5 count = 0 6 def __init__(self,name): 7 super(MyThread,self).__init__() 8 self.__name = name 9 def run(self): 10 time.sleep(random.randrange(1,10)) 11 lock.acquire() #加锁 12 MyThread.count += 1 13 lock.release() #释放锁 14 print("线程%s运行完毕" % self.__name) 15 @classmethod 16 def print_count(self): 17 print(MyThread.count) 18 19 obj_list = [] 20 for i in range(50): 21 t = MyThread(i) 22 t.start() 23 obj_list.append(t) 24 for obj in obj_list: 25 obj.join() 26 MyThread.print_count()#打印总共的线程数 27 print("主线程运行完毕")
四、信号量
有时候我们系统共享的资源可能有多个,这时候需要用到信号量,信号量的使用代码示例如下:
1 import threading,time 2 3 semaphore = threading.BoundedSemaphore(5) 4 def run(n): 5 semaphore.acquire() 6 time.sleep(2) 7 print(n+1) 8 semaphore.release() 9 10 for i in range(23): 11 t = threading.Thread(target=run,args=(i,)) 12 t.start() 13 while threading.active_count()!=1: 14 pass 15 else: 16 print("over")
五、递归锁
我们申请的每一把锁都应该记住其对应的对象地址,否则当有多把锁时,就会容易导致程序死锁。示例代码如下:
1 import threading 2 n = 0 3 lock = {} 4 lock["1"] = threading.Lock() 5 lock["2"] = threading.Lock() 6 lock["3"] = threading.Lock() 7 #应该记清楚每一把锁 8 def run1(): 9 global n 10 lock["1"].acquire() 11 print("run1") 12 n = n + 1 13 lock["1"].release() 14 15 def run2(): 16 global n 17 lock["2"].acquire() 18 print("run2") 19 n = n + 1 20 lock["2"].release() 21 22 def run3(): 23 global n 24 lock["3"].acquire() 25 run1() 26 run2() 27 lock["3"].release() 28 29 t = threading.Thread(target=run3) 30 t.start() 31 while threading.active_count()!=1: 32 print(threading.active_count()) 33 else: 34 print("over")
六、事件
事件其本质就是设置一个标志位,当线程在运行的时候,判断这个标志位,如果标志位未被设置就阻塞。下面是红绿灯的一个代码示例:
1 import threading,time,random 2 3 event = threading.Event()#获取一个event对象 4 5 def lighter(): 6 count = 0 7 event.set() #设置标志位 8 while True: 9 if count >=0 and count<10: 10 event.clear() #清除标志位 11 print("\033[41;1mred light is on...\033[0m") 12 elif count>=10 and count<20: 13 event.set() 14 print("\033[42;1mgreen light is on...\033[0m") 15 else: 16 count = 0 17 time.sleep(1) 18 count = count + 1 19 20 t = threading.Thread(target=lighter) 21 t.start() 22 23 def car(n): 24 while True: 25 if event.is_set():#判断标志位是否被设置 26 print("the car is passing the accross") 27 time.sleep(random.randrange(1,10)) 28 print("the %s car has accrosee the across..." % n) 29 break 30 else: 31 print("the %s car is wait..." % n) 32 event.wait() 33 34 for i in range(20): 35 t = threading.Thread(target=car,args=(i,)) 36 t.start()