线程

内容:

1.线程概念

2.全局解释器锁GIL

3.threading模块使用

4.锁

5.信号量、事件、条件与计时器

6.线程队列与线程池

参考:https://www.cnblogs.com/Eva-J/articles/8306047.html

1.线程概念

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。

线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位

由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

实例:

内存中的线程: 

进程和线程的区别:

线程概念详细介绍:https://www.cnblogs.com/wyb666/p/9636270.html

2.全局解释器锁GIL

Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

在多线程环境中,Python 虚拟机按以下方式执行:

  1. 设置 GIL
  2. 切换到一个线程去运行
  3. 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0))
  4. 把线程设置为睡眠状态
  5. 解锁 GIL
  6. 再次重复以上所有步骤

在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换),编写扩展的程序员可以主动解锁GIL

3.threading模块使用

(1)前言 - python线程模块的选择

Python提供了几个用于多线程编程的模块,包括thread、threading和Queue:

  • thread和threading模块允许程序员创建和管理线程
  • thread模块提供了基本的线程和锁的支持
  • threading提供了更高级别、功能更强的线程管理的功能
  • Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构

注:创建线程使用threading模块,避免使用thread模块,另外multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而下面详细介绍threading模块的用法,直接上代码

(2)创建线程

 1 # 方法1
 2 import time
 3 from threading import Thread
 4 
 5 def func(n):
 6     time.sleep(1)
 7     print(n)
 8 
 9 if __name__ == '__main__':
10     for i in range(10):
11         t = Thread(target=func, args=(i,))
12         t.start()
 1 # 方法2
 2 from threading import Thread
 3 import time
 4 
 5 class MyThread(Thread):
 6     def __init__(self, name):
 7         super().__init__()
 8         self.name = name
 9 
10     def run(self):
11         time.sleep(2)
12         print('%s hello' % self.name)
13 
14 if __name__ == '__main__':
15     t = MyThread('wyb')
16     t.start()

(3)内存数据共享问题

 1 from threading import Thread
 2 from multiprocessing import Process
 3 import os
 4 
 5 def work():
 6     global n
 7     n = 0
 8 
 9 if __name__ == '__main__':
10     n = 100
11     p = Process(target=work)
12     p.start()
13     p.join()
14     print('', n)  # 100 -> 子进程p已将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
15 
16     n = 66
17     t = Thread(target=work)
18     t.start()
19     t.join()
20     print('', n)  # 0 -> 同一进程内的线程之间共享进程内的数据 子进程改了 父进程也跟着改了 同理其他子进程共享的该数据也改了

(4)多进程和多线程的效率比较

 1 import time
 2 from threading import Thread
 3 from multiprocessing import Process
 4 
 5 def func(n):
 6     n + 1
 7 
 8 if __name__ == '__main__':
 9     # 多线程:
10     start = time.time()
11     t_lst = []
12     for i in range(10):
13         t = Thread(target=func, args=(i,))
14         t.start()
15         t_lst.append(t)
16     for t in t_lst:
17         t.join()
18     t1 = time.time() - start
19     
20     # 多进程:
21     start = time.time()
22     t_lst = []
23     for i in range(10):
24         t = Process(target=func, args=(i,))
25         t.start()
26         t_lst.append(t)
27     for t in t_lst:
28         t.join()
29     t2 = time.time() - start
30     print(t1, t2)

结果:

(5)Thread类的其他方法

1 Thread实例对象的方法
2   # isAlive(): 返回线程是否活动的。
3   # getName(): 返回线程名。
4   # setName(): 设置线程名。
5 
6 threading模块提供的一些方法:
7   # threading.current_thread(): 返回当前的线程变量。  threading.get_ident(): 返回当前的线程的id(线程号)
8   # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
9   # threading.active_count(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
 1 import time
 2 import threading
 3 
 4 def hello(n):
 5     time.sleep(0.5)
 6     print(n, threading.current_thread(), threading.get_ident())
 7 
 8 for i in range(10):
 9     threading.Thread(target=hello, args=(i,)).start()
10 print(threading.active_count())  # 11
11 print(threading.current_thread())
12 print(threading.enumerate())

(6)守护线程

无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行

1 # 注意:
2 #    对主进程来说,运行完毕指的是主进程代码运行完毕
3 #    对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程(子线程)统统运行完毕,主线程才算运行完毕
1 # 详细解释:
2 #    主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束
3 #    主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束
 1 # 实例
 2 import time
 3 from threading import Thread
 4 
 5 def func1():                    # 守护线程
 6     while True:
 7         print('*'*10)
 8         time.sleep(3)
 9 
10 def func2():                    # 子线程
11     print("in func2")
12     time.sleep(5)
13 
14 t = Thread(target=func1, )
15 t2 = Thread(target=func2,)
16 t.daemon = True  # 设置守护线程 -> 主线程结束 守护线程随之结束
17 t.start()
18 t2.start()
19 print("主线程")
20 
21 
22 # 守护进程随着主进程代码的执行结束而结束
23 # 守护线程会在主线程结束之后等待其他子线程的结束才结束
24 
25 # 主进程在执行完自己的代码之后不会立即结束 而是等待子进程结束之后 回收子进程的资源

4.锁

(1)锁与GIL

为什么需要锁:防止多个线程抢占资源

多个线程抢占资源实例:

 1 from threading import Thread
 2 import time
 3 
 4 def work():
 5     global n
 6     temp = n
 7     time.sleep(0.1)
 8     n = temp - 1
 9 
10 if __name__ == '__main__':
11     n = 100
12     l = []
13     for i in range(100):
14         p = Thread(target=work)
15         l.append(p)
16         p.start()
17     for p in l:
18         p.join()
19 
20     print(n)  # 结果为99 不为0 -> 这是因为这多个线程的操作(拿值)都在0.1秒之内 然后都拿的是100

GIL锁(即全局解释器锁) -> 在Cpython解释器下的python程序 在同一时刻 多个线程中只能有一个线程被CPU执行 -> GIL中只是给线程加锁

 1 Python代码的执行由Python 虚拟机(CPython)来控制,Python在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制
 2 在多线程环境中,Python 虚拟机按以下方式执行:
 3   设置GIL
 4   切换到一个线程去运行
 5   运行:
 6       指定数量的字节码指令,或者
 7        线程主动让出控制(可以调用time.sleep(0))
 8   把线程设置为睡眠状态
 9    解锁GIL
10    再次重复以上所有步骤
11 
12 简单说 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行
13 
14 注意这种情况注意不是由python本身造成的,而是由CPython解释器造成的

注意:即使有GIL锁,我们在多线程的代码中依然要使用锁,这是因为GIL锁只是限制了同一时刻只能有一个线程运行,但是无法避免时间片轮转带来的数据不安全性,所以为了保险起见还是有时候还是要加锁!

(2)锁的使用 

 1 import threading
 2 
 3 lock = threading.Lock()
 4 
 5 lock.acquire()   # 拿钥匙
 6 
 7 '''
 8 对公共数据的操作
 9 '''
10 
11 lock.release()    # 把钥匙放回去

锁使用实例 - 多个线程抢占资源问题的解决方案:

 1 from threading import Thread, Lock
 2 import time
 3 
 4 def work(lk):
 5     global n
 6     lk.acquire()
 7     temp = n
 8     time.sleep(0.1)
 9     n = temp - 1
10     lk.release()
11 
12 if __name__ == '__main__':
13     n = 100
14     p_list = []
15     lock = Lock()
16     for i in range(100):
17         p = Thread(target=work, args=(lock, ))
18         p_list.append(p)
19         p.start()
20     for p in p_list:
21         p.join()
22 
23     print(n)  # 结果为0

(3)死锁与互斥锁与递归锁

死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

什么时候容易出现死锁:同一个进程或线程用到两把或者两把以上的锁时就容易出现死锁现象(2个锁,一个进程acquire了a锁,另一个进程acquire了b锁,每一方都在等另一方release)

死锁示例:

1 from threading import Lock
2 
3 mutexA = Lock()
4 mutexA.acquire()
5 mutexA.acquire()
6 print(123)
7 mutexA.release()
8 mutexA.release()

互斥锁:就是普通的锁,普通的锁就是互斥锁,被acquire的资源release之前不能被别人acquire了

递归锁

在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock,递归锁可以解决死锁问题

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

1 from threading import RLock as Lock
2 import time
3 
4 mutexA=Lock()
5 mutexA.acquire()
6 mutexA.acquire()
7 print(123)
8 mutexA.release()
9 mutexA.release()

(4)典型问题 - 科学家吃面

 1 # 科学家吃面 -> 死锁
 2 import time
 3 from threading import Lock, Thread
 4 
 5 noodle_lock = Lock()
 6 fork_lock = Lock()
 7 
 8 def eat1(name):
 9     noodle_lock.acquire()
10     print('%s拿到面条啦' % name)
11     fork_lock.acquire()
12     print('%s拿到叉子了' % name)
13     print('%s吃面' % name)
14     fork_lock.release()
15     noodle_lock.release()
16 
17 def eat2(name):
18     fork_lock.acquire()
19     print('%s拿到叉子了' % name)
20     time.sleep(1)
21     noodle_lock.acquire()
22     print('%s拿到面条啦' % name)
23     print('%s吃面' % name)
24     noodle_lock.release()
25     fork_lock.release()
26 
27 Thread(target=eat1, args=('wyb',)).start()
28 Thread(target=eat2, args=('woz',)).start()
29 Thread(target=eat1, args=('xxx',)).start()
30 Thread(target=eat2, args=('abc',)).start()
 1 # 科学家吃面 -> 解决死锁 -> 使用递归锁
 2 import time
 3 from threading import Thread
 4 from threading import RLock  # 递归锁
 5 
 6 fork_lock = noodle_lock = RLock()  # 一个钥匙串上的两把钥匙 -> 拿到一把就相当于拿到另一把 只有把这两把都release了其他人才能开始拿钥匙
 7 
 8 def eat1(name):
 9     noodle_lock.acquire()  # 一把钥匙
10     print('%s拿到面条啦' % name)
11     fork_lock.acquire()
12     print('%s拿到叉子了' % name)
13     print('%s吃面' % name)
14     fork_lock.release()
15     noodle_lock.release()
16 
17 def eat2(name):
18     fork_lock.acquire()
19     print('%s拿到叉子了' % name)
20     time.sleep(1)
21     noodle_lock.acquire()
22     print('%s拿到面条啦' % name)
23     print('%s吃面' % name)
24     noodle_lock.release()
25     fork_lock.release()
26 
27 Thread(target=eat1, args=('wyb',)).start()
28 Thread(target=eat2, args=('woz',)).start()
29 Thread(target=eat1, args=('xxx',)).start()
30 Thread(target=eat2, args=('abc',)).start()

5.信号量、事件、条件与计时器

(1)信号量

同进程的一样

Semaphore管理一个内置的计数器:

  • 调用acquire()时计数器-1
  • 调用release() 时计数器+1
  • 计数器不能小于0
  • 当计数器为0时,acquire()将阻塞线程直到其他线程调用release()

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

 1 from threading import Thread, Semaphore
 2 import threading
 3 import time
 4 
 5 # 信号量的特点: 同一时间只能有n个线程执行这段代码
 6 def func():
 7     sm.acquire()
 8     print('%s get sm' % threading.current_thread().getName())
 9     time.sleep(3)
10     sm.release()
11 
12 if __name__ == '__main__':
13     sm = Semaphore(5)
14     for i in range(23):
15         t = Thread(target=func)
16         t.start()

注:信号量与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

(2)事件

同进程的一样

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

  • event.isSet():返回event的状态值
  • event.wait():如果 event.isSet()==False将阻塞线程
  • event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度
  • event.clear():恢复event的状态值为False
1 # 事件被创建的时候
2 # False状态
3     # wait() 阻塞
4 # True状态
5     # wait() 非阻塞
6 # clear 设置状态为False
7 # set  设置状态为True

实例 - 有多个工作线程连接MySQL,我们想要在连接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作:

 1 import time
 2 import random
 3 from threading import Thread, Event
 4 
 5 def connect_db(e):      # 连接数据库
 6     count = 0
 7     while count < 3:
 8         e.wait(0.5)  # 状态为False的时候,我只等待1s就结束
 9         if e.is_set() is True:
10             print('连接数据库')
11             break
12         else:
13             count += 1
14             print('第%s次连接失败' % count)
15     else:
16         raise TimeoutError('数据库连接超时')
17 
18 def check_web(e):       # 检测与数据库之间的网络是否连通
19     time.sleep(random.randint(0, 3))
20     e.set()     # 设置true
21 
22 
23 event = Event()
24 t1 = Thread(target=connect_db, args=(event,))
25 t2 = Thread(target=check_web, args=(event,))
26 t1.start()
27 t2.start()

(3)条件和定时器

条件:使得线程等待,只有满足某条件时,才释放线程

 1 # 条件 -> 更复杂的锁 -> acquire release
 2 # 一个条件被创建之初 默认有一个False状态 -> False状态会影响wait一直处于等待状态
 3 # notify(int数据类型)  造钥匙
 4 from threading import Thread, Condition
 5 
 6 
 7 def func(con, n):
 8     con.acquire()
 9     con.wait()  # 等钥匙
10     print('在第%s个循环里' % n)
11     con.release()
12 
13 
14 condition = Condition()
15 for i in range(10):
16     Thread(target=func, args=(condition, i)).start()
17 while True:
18     num = int(input('>>>'))
19     condition.acquire()
20     condition.notify(num)  # 造钥匙
21     condition.release()

定时器:指定n秒后执行某个操作

 1 import time
 2 from threading import Timer
 3 
 4 def func():
 5     print('时间同步')
 6 
 7 # 每3s开启一个线程 执行函数
 8 while True:
 9     Timer(3, func).start()      # 3s之后开启线程
10     time.sleep(3)

6.线程队列与线程池

(1)线程队列

queue队列 :使用import queue,用法与进程Queue一样

三种queue队列:

  • class queue.Queue(maxsize=0) #先进先出
  • class queue.LifoQueue(maxsize=0) #last in fisrt out
  • class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列
 1 import queue
 2 
 3 q = queue.Queue()  # 队列 先进先出
 4 q.put(6)
 5 q.put(7)
 6 q.put(8)
 7 print(q.get())
 8 
 9 q = queue.LifoQueue()  # 栈 先进后出
10 q.put(1)
11 q.put(2)
12 q.put(3)
13 print(q.get())
14 print(q.get())
15 
16 q = queue.PriorityQueue()  # 优先级队列 -> 数越小优先级越高
17 q.put((20, 'a'))
18 q.put((10, 'b'))
19 q.put((30, 'c'))
20 q.put((-5, 'd'))
21 q.put((1, '?'))
22 print(q.get())
23 print(q.get())
24 
25 # 其他方法:
26 # q.put_nowait()
27 # q.get_nowait()

(2)线程池

早期的python没有提供线程池,后来出了concurrent.futures模块才能实现线程池,当然也可以用这个模块实现进程池

concurrent.futures模块详细介绍:https://www.cnblogs.com/wyb666/p/9772868.html

原文地址:https://www.cnblogs.com/wyb666/p/9751675.html