python --- 基础多线程编程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python --- 基础多线程编程相关的知识,希望对你有一定的参考价值。

在python中进行多线程编程之前必须了解的问题:


1. 什么是线程?
  答:线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。

2. 什么是多线程?
  答:在单个程序中同时运行多个线程完成不同的工作,称为多线程。

3. 多线程编程的目的?
  答:多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

4. 如何再python中执行多线程编程?
  答:在python2.x的版本中提供了thread(这个模块为多线程提供了一个底层 、原始的操作[也可以成为light-weight processes 或者 tasks) — 多个控制线程共享全局数据空间。为了多线程同步,提供了简单的锁(也称呼为 mutexes 后者 binary semaphores) 。]和threading(本模块的高层线程接口构建在低层的thread模块上)两个模块用于线程操作;而在python3.x中,官方只给出了threading模块的文档,对于底层线程造作放在了_thread模块中(即不建议使用)。是故在python中使用threading模块编程即可。

 

例一(一个简单的双线程程序):

技术分享
 1 import threading
 2 import time
 3 
 4 def run(n):
 5     print("task-%s" % n)
 6     time.sleep(5)
 7 
 8 #实例化一个线程对象,target传入任务名,args以元组的形式传入任务函数的参数
 9 task1 = threading.Thread(target=run, args=(1,))
10 task2 = threading.Thread(target=run, args=(2,))
11 
12 task1.start()   #线程启动
13 task2.start()
test_threads_1

  注:执行上面这个程序一共花费5秒左右的时间,而如果分别调用两次run怎需要10秒(证明两个任务同时运行)。

 

例二(用面向对象编程实现例二,并讲解threading模块中的一些常用方法):

技术分享
 1 import threading
 2 import time
 3 
 4 class Task(threading.Thread):
 5     def __init__(self, n):
 6         super(Task, self).__init__() #重构了__init__(), 所以先执行父类的构造函数
 7         self.n = n     #构造函数中传入任务参数参数
 8 
 9     #任务函数
10     def run(self):
11         """
12         不同于例一,在类中,任务函数只能以run命名,参数从构造函数中传入
13         :return: 
14         """
15         print("task-%s" % self.n)
16         # print(threading.current_thread())   #threading.current_thread()返回当前的Thread对象,对应于调用者控制的线程。
17         time.sleep(2)
18 
19 #实例化并启动
20 t1 = Task(1)
21 t2 = Task(2)
22 # t1.setDaemon(True)     #将t1设置成为守护线程
23 # t2.setDaemon(True)     
24 
25 t1.start()
26 #t1.join(timeout=5)  #等待t1结束再运行t2,timeout代表等待时间,超过5秒便不再等待。
27 t2.start()
28 
29 # print(threading.current_thread())
30 # print(threading.active_count())  #threading.active_count()返回当前处于alive状态的Thread对象的个数。
test_threads_2

  :建议使用例二这种编程方式,在类中,任务函数只能以run命名,参数从构造函数中传入。

  注:有时候难免会遇到一个问题,主程序的执行需要某个子线程(即使用start启动的线程,默认子线程一旦启动主程序将继续运行,两者不再有关联)的执行结果,这时使用join即可,例如例二中的t1.join()。

  守护线程:一种特殊的子线程,与主线程(即本文中的主程序,由程序使用者启动)存在一种“主死仆死”的关系,即主线程意外停止或运行结束,不管守护线程是否运行完毕都得终止(非守护线程的子线程启动后与主线程没有联系,不论主线程是否还在运行,子线程不受影响),一个线程是否为守护线程继承于创建它的线程(将一个子线程设置成为守护线程可参考例二,去掉22,23行注释,则主程序结束t1,t2也结束,不会再sleep)。

 

  在进行多线程编程时,经常会有多个线程同时对同一份数据修改,这便会导致最终得得到的数据不是预期结果。所以需要在一个线程修改数据时将数据锁定,只允许当前线程修改,当修改完成后,解锁让其他线程修改。在threading模块中使用Lock Objects解决此问题。

Lock Objects:   

  A primitive lock is a synchronization primitive that is not owned by a particular thread when locked. In Python, it is currently the lowest level synchronization primitive available, implemented directly by the _thread extension module.

A primitive lock is in one of two states, “locked” or “unlocked”. It is created in the unlocked state. It has two basic methods, acquire() and release(). When the state is unlocked, acquire() changes the state to locked and returns immediately. When the state is locked, acquire() blocks until a call to release() in another thread changes it to unlocked, then the acquire() call resets it to locked and returns. The release() method should only be called in the locked state; it changes the state to unlocked and returns immediately. If an attempt is made to release an unlocked lock, a RuntimeError will be raised.

  Locks also support the context management protocol.

  When more than one thread is blocked in acquire() waiting for the state to turn to unlocked, only one thread proceeds when a release() call resets the state to unlocked; which one of the waiting threads proceeds is not defined, and may vary across implementations.

  All methods are executed atomically.

  例三(多个线程对一个全局变量进行加1操作):

技术分享
 1 import threading,time
 2 
 3 
 4 n = 5
 5 
 6 class Task(threading.Thread):
 7     def __init__(self, n):
 8         super(Task, self).__init__()
 9         self.name = "Thread-" + str(n)
10 
11     def run(self):
12         """
13         不同于例一,在类中,任务函数只能以run命名,参数从构造函数中传入
14         :return: 
15         """
16         global n
17         time.sleep(0.5)
18         #锁定资源, 此时关于n的操作只能此线程执行
19         if lock.acquire():
20             n += 1
21             print("%s, n = %s" % (self.name, n))
22             time.sleep(0.2)
23             lock.release()   #解锁
24 
25 if __name__ == "__main__":
26 
27     lock = threading.Lock()         #实例化一个锁,此时锁处于打开状态
28 
29     ts = []    #用于存储线程对象
30     #循环启动50个线程
31     for i in range(1, 50+1):
32         t = Task(i)
33         ts.append(t)
34         t.start()
35     #等待所有线程结束
36     start_time = time.time()
37     for t in ts:
38         t.join()
39 
40     print("run time:",time.time() - start_time)
test_threads_3

   注:运行此程序,可发现一共用时10.5(0.2*50 + 0.5)秒左右,可以发现在锁定代码段程序没有并行执行。

  注:有时候需要多层加锁,这时Lock Objects已经满足不了这个需求(当Lock处于locked时,遇到下一个acquire()时会阻塞)。这时我们使用RLock Objects,它的调用同Lock Objects。

 

信号量:信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

  举例说明:以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。

  在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
  信号量的特性:信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。
   简单来说,当使用Lock或RLock的locked期间,没有并发执行(只unlocked后才并行);而使用Semaphore可以让有限个线程共同访问资源,它的使用也是只有acquire()和release()。
  例四(信号量的使用):
技术分享
 1 import threading
 2 import time
 3 
 4 class Task(threading.Thread):
 5     #任务函数
 6     def __init__(self, n):
 7         super(Task, self).__init__()
 8         self.name = "Thread-" + str(n)
 9 
10     def run(self):
11         semaphore.acquire()
12         print("%s" % self.name)
13         time.sleep(2)
14         semaphore.release()
15 
16 
17 if __name__ == "__main__":
18 
19     semaphore = threading.BoundedSemaphore(5)   #只允许5个线程同时访问资源
20 
21     ts = []    #用于存储线程对象
22     #循环启动50个线程
23 
24     for i in range(1, 23+1):
25         t = Task(i)
26         ts.append(t)
27         t.start()
test_threads_4

 

  注:通过运行上面的代码,可以发现在关键代码段最多只有5个线程在同时运行。
    每调用一次acquire(),Semaphore的计数器减1;每调用一次release(),Semaphore的计数器加1,最大不超过设置的计数器初始值。
    
  既然有了多线程编程,那么自然也就有了线程之间的交互。在python中threading模块提供了Event Objocts实现这个功能。
Events Objects:
  This is one of the simplest mechanisms for communication between threads: one thread signals an event and other threads wait for it(事件对象是线程间最简单的通信机制之一:线程可以激活在一个事件对象上等待的其他线程).

  An event object manages an internal flag that can be set to true with the set() method and reset to false with the clear() method. The wait() method blocks until the flag is true(每个事件对象管理一个内部标志,可以在事件对象上调用set() 方法将内部标志设为true,调用 clear() 方法将内部标志重置为false。wait()方法将阻塞直至该标志为真。).

  例五(简单的线程交互程序,演示Event Objects的使用):

技术分享
 1 import threading,time
 2 
 3 class Task1(threading.Thread):
 4     def run(self):
 5         while True:
 6             for i in range(1, 20+1):
 7                 print("i = ", i)
 8                 if i % 5 == 0:
 9                     eve.set()    #到5的倍数,将Event内部标志设置为True
10                 time.sleep(0.5)
11 
12 class Task2(threading.Thread):
13     def run(self):
14         while True:
15             #检测内部标志是否为True
16             if eve.is_set():
17                 print("hello world")
18                 time.sleep(2)
19                 eve.clear()       #重置Event内部标志
20             else:
21                 eve.wait()         #内部标志不为True,此线程处于阻塞状态
22 
23 if __name__ == "__main__":
24     t1 = Task1()
25     t2 = Task2()
26     eve = threading.Event()      #实例化一个Event Objects
27 
28     t1.start()
29     t2.start()
test_threads_5





以上是关于python --- 基础多线程编程的主要内容,如果未能解决你的问题,请参考以下文章

python并发编程之多线程基础知识点

如何理解python的多线程编程

python --- 基础多线程编程

Python并发编程之创建多线程的几种方法

多线程 Thread 线程同步 synchronized

多线程编程