操作系统线程理论

进程

进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

线程

60 年代,在 OS 中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端

  1. 是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;
  2. 是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。

因此在 80 年代,出现了能独立运行的基本单位——线程(Threads)。
注意:进程是资源分配的最小单位, 线程是 CPU 调度的最小单位. 每一个进程中至少有一个线程。 

进程和线程的关系

python并发编程:线程 - 图1
线程与进程的区别可以归纳为以下 4 点:

  1. 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  2. 通信:进程间通信 IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
  3. 调度和切换:线程上下文切换比进程上下文切换要快得多。
  4. 在多线程操作系统中,进程不是一个可执行的实体。

使用线程的实际场景

开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程, 如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。

内存中的线程

python并发编程:线程 - 图2
线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:

  1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程
  2. 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?

因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。

python 使用线程

全局解释器锁 GIL

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

  1. 设置 GIL;
  2. 切换到一个线程去运行;
  3. 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
  4. 把线程设置为睡眠状态;
  5. 解锁 GIL;
  6. 再次重复以上所有步骤。
      在调用外部代码 (如 C/C++ 扩展函数) 的时候,GIL 将会被锁定,直到这个函数结束为止 (由于在这期间没有 Python 的字节码被运行,所以不会做线程切换) 编写扩展的程序员可以主动解锁 GIL。

python 线程模块的选择

Python 提供了几个用于多线程编程的模块,包括 thread、threading 和 Queue 等。thread 和 threading 模块允许程序员创建和管理线程。thread 模块提供了基本的线程和锁的支持,threading 提供了更高级别、功能更强的线程管理的功能。Queue 模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
避免使用 thread 模块,因为更高级别的 threading 模块更为先进,对线程的支持更为完善,而且使用 thread 模块里的属性有可能会与 threading 出现冲突;其次低级别的 thread 模块的同步原语很少(实际上只有一个),而 threading 模块则有很多;再者,thread 模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少 threading 模块能确保重要的子线程退出后进程才退出。
thread 模块不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。而 threading 模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。

threading 模块

线程的创建

  1. from threading import Thread
  2. import time
  3. def sayhi(name):
  4. time.sleep(2)
  5. print('%s say hello' %name)
  6. if __name__ == '__main__':
  7. t=Thread(target=sayhi,args=('aaron',))
  8. t.start()
  9. print('主线程')

Python
Copy
另一种创建进程的方式

  1. from threading import Thread
  2. import time
  3. class Sayhi(Thread):
  4. def __init__(self,name):
  5. super().__init__()
  6. self.name=name
  7. def run(self):
  8. time.sleep(2)
  9. print('%s say hello' % self.name)
  10. if __name__ == '__main__':
  11. t = Sayhi('aaron')
  12. t.start()
  13. print('主线程')

Python
Copy

多线程与多进程

  1. from threading import Thread
  2. from multiprocessing import Process
  3. import os
  4. def work():
  5. print('hello',os.getpid())
  6. if __name__ == '__main__':
  7. #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
  8. t1=Thread(target=work)
  9. t2=Thread(target=work)
  10. t1.start()
  11. t2.start()
  12. print('主线程/主进程pid',os.getpid())
  13. #part2:开多个进程,每个进程都有不同的pid
  14. p1=Process(target=work)
  15. p2=Process(target=work)
  16. p1.start()
  17. p2.start()
  18. print('主线程/主进程pid',os.getpid())

Python
Copy
效率对比

  1. from threading import Thread
  2. from multiprocessing import Process
  3. import os
  4. def work():
  5. print('hello')
  6. if __name__ == '__main__':
  7. #在主进程下开启线程
  8. t=Thread(target=work)
  9. t.start()
  10. print('主线程/主进程')
  11. '''
  12. 打印结果:
  13. hello
  14. 主线程/主进程
  15. '''
  16. #在主进程下开启子进程
  17. t=Process(target=work)
  18. t.start()
  19. print('主线程/主进程')

Python
Copy
内存数据共享

  1. from threading import Thread
  2. from multiprocessing import Process
  3. from threading import Thread
  4. import os
  5. def work():
  6. global n
  7. n=0
  8. if __name__ == '__main__':
  9. # n=100
  10. # p=Process(target=work)
  11. # p.start()
  12. # p.join()
  13. # print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
  14. n=1
  15. t=Thread(target=work)
  16. t.start()
  17. t.join()
  18. print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据

Python
Copy

多线程实现 socket

服务端

  1. import multiprocessing
  2. import threading
  3. import socket
  4. s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  5. s.bind(('127.0.0.1',8080))
  6. s.listen(5)
  7. def action(conn):
  8. while True:
  9. data=conn.recv(1024)
  10. print(data)
  11. conn.send(data.upper())
  12. if __name__ == '__main__':
  13. while True:
  14. conn,addr=s.accept()
  15. p=threading.Thread(target=action,args=(conn,))
  16. p.start()

Python
Copy
客户端

  1. import socket
  2. s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  3. s.connect(('127.0.0.1',8080))
  4. while True:
  5. msg=input('>>: ').strip()
  6. if not msg:continue
  7. s.send(msg.encode('utf-8'))
  8. data=s.recv(1024)
  9. print(data)

Python
Copy

Thread 类的其他方法

  • Thread 实例对象的方法
    • isAlive(): 返回线程是否活动的。
    • getName(): 返回线程名。
    • setName(): 设置线程名。
  • threading 模块提供的一些方法:
    • threading.currentThread(): 返回当前的线程变量。
    • threading.enumerate(): 返回一个包含正在运行的线程的 list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    • threading.activeCount(): 返回正在运行的线程数量,与 len(threading.enumerate()) 有相同的结果。
      1. from threading import Thread
      2. import threading
      3. from multiprocessing import Process
      4. import os
      5. def work():
      6. import time
      7. time.sleep(3)
      8. print(threading.current_thread().getName())
      9. if __name__ == '__main__':
      10. #在主进程下开启线程
      11. t=Thread(target=work)
      12. t.start()
      13. print(threading.current_thread().getName())
      14. print(threading.current_thread()) #主线程
      15. print(threading.enumerate()) #连同主线程在内有两个运行的线程
      16. print(threading.active_count())
      17. print('主线程/主进程')
      Python
      Copy
      使用 join
      1. from threading import Thread
      2. import time
      3. def sayhi(name):
      4. time.sleep(2)
      5. print('%s say hello' %name)
      6. if __name__ == '__main__':
      7. t=Thread(target=sayhi,args=('aaron',))
      8. t.start()
      9. t.join()
      10. print('主线程')
      11. print(t.is_alive())
      Python
      Copy

      守护线程

      无论是进程还是线程,都遵循:守护 xx 会等待主 xx 运行完毕后被销毁。需要强调的是:运行完毕并非终止运行
  1. 对主进程来说,运行完毕指的是主进程代码运行完毕
    主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收), 然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源 (否则会产生僵尸进程),才会结束,
  2. 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
    主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
    1. from threading import Thread
    2. import time
    3. def sayhi(name):
    4. time.sleep(2)
    5. print('%s say hello' %name)
    6. if __name__ == '__main__':
    7. t=Thread(target=sayhi,args=('aaron',))
    8. t.setDaemon(True) #必须在t.start()之前设置
    9. t.start()
    10. print('主线程')
    11. print(t.is_alive())
    Python
    Copy
    1. from threading import Thread
    2. import time
    3. def foo():
    4. print(123)
    5. time.sleep(1)
    6. print("end123")
    7. def bar():
    8. print(456)
    9. time.sleep(3)
    10. print("end456")
    11. if __name__ == '__main__':
    12. t1=Thread(target=foo)
    13. t2=Thread(target=bar)
    14. t1.daemon=True
    15. t1.start()
    16. t2.start()
    17. print("main")
    Python
    Copy

同步锁

没有锁的情况下

  1. from threading import Thread
  2. import os,time
  3. def work():
  4. global n
  5. temp=n
  6. time.sleep(0.1)
  7. n=temp-1
  8. if __name__ == '__main__':
  9. n=100
  10. l=[]
  11. for i in range(100):
  12. p=Thread(target=work)
  13. l.append(p)
  14. p.start()
  15. for p in l:
  16. p.join()
  17. print(n)

Python
Copy
同步锁

  1. import threading
  2. R=threading.Lock()
  3. R.acquire()
  4. '''
  5. 对公共数据的操作
  6. '''
  7. R.release()

Python
Copy

  1. from threading import Thread,Lock
  2. import os,time
  3. def work():
  4. global n
  5. lock.acquire()
  6. temp=n
  7. time.sleep(0.1)
  8. n=temp-1
  9. lock.release()
  10. if __name__ == '__main__':
  11. lock=Lock()
  12. n=100
  13. l=[]
  14. for i in range(100):
  15. p=Thread(target=work)
  16. l.append(p)
  17. p.start()
  18. for p in l:
  19. p.join()
  20. print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全

Python
Copy

  1. #不加锁:并发执行,速度快,数据不安全
  2. from threading import current_thread,Thread,Lock
  3. import os,time
  4. def task():
  5. global n
  6. print('%s is running' %current_thread().getName())
  7. temp=n
  8. time.sleep(0.5)
  9. n=temp-1
  10. if __name__ == '__main__':
  11. n=100
  12. lock=Lock()
  13. threads=[]
  14. start_time=time.time()
  15. for i in range(100):
  16. t=Thread(target=task)
  17. threads.append(t)
  18. t.start()
  19. for t in threads:
  20. t.join()
  21. stop_time=time.time()
  22. print('主:%s n:%s' %(stop_time-start_time,n))

Python
Copy

  1. #不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
  2. from threading import current_thread,Thread,Lock
  3. import os,time
  4. def task():
  5. #未加锁的代码并发运行
  6. time.sleep(3)
  7. print('%s start to run' %current_thread().getName())
  8. global n
  9. #加锁的代码串行运行
  10. lock.acquire()
  11. temp=n
  12. time.sleep(0.5)
  13. n=temp-1
  14. lock.release()
  15. if __name__ == '__main__':
  16. n=100
  17. lock=Lock()
  18. threads=[]
  19. start_time=time.time()
  20. for i in range(100):
  21. t=Thread(target=task)
  22. threads.append(t)
  23. t.start()
  24. for t in threads:
  25. t.join()
  26. stop_time=time.time()
  27. print('主:%s n:%s' %(stop_time-start_time,n))

Python
Copy
有的同学可能有疑问: 既然加锁会让运行变成串行, 那么我在 start 之后立即使用 join, 就不用加锁了啊, 也是串行的效果啊
没错: 在 start 之后立刻使用 jion, 肯定会将 100 个任务的执行变成串行, 毫无疑问, 最终 n 的结果也肯定是 0, 是安全的, 但问题是
start 后立即 join: 任务内的所有代码都是串行执行的, 而加锁, 只是加锁的部分即修改共享数据的部分是串行的
单从保证数据安全方面, 二者都可以实现, 但很明显是加锁的效率更高.

  1. from threading import current_thread,Thread,Lock
  2. import os,time
  3. def task():
  4. time.sleep(3)
  5. print('%s start to run' %current_thread().getName())
  6. global n
  7. temp=n
  8. time.sleep(0.5)
  9. n=temp-1
  10. if __name__ == '__main__':
  11. n=100
  12. lock=Lock()
  13. start_time=time.time()
  14. for i in range(100):
  15. t=Thread(target=task)
  16. t.start()
  17. t.join()
  18. stop_time=time.time()
  19. print('主:%s n:%s' %(stop_time-start_time,n))

Python
Copy

死锁与递归锁

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

  1. from threading import Lock as Lock
  2. import time
  3. mutexA=Lock()
  4. mutexA.acquire()
  5. mutexA.acquire() # 上面已经拿过一次key了,这边就拿不到了
  6. print(123)
  7. mutexA.release()
  8. mutexA.release()

Python
Copy
解决方法,递归锁,在 Python 中为了支持在同一线程中多次请求同一资源,python 提供了可重入锁 RLock。
这个 RLock 内部维护着一个 Lock 和一个 counter 变量,counter 记录了 acquire 的次数,从而使得资源可以被多次 acquire。直到一个线程所有的 acquire 都被 release,其他的线程才能获得资源。上面的例子如果使用 RLock 代替 Lock,则不会发生死锁

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

Python
Copy
吃面的问题

  1. import time
  2. from threading import Thread,Lock
  3. noodle_lock = Lock()
  4. fork_lock = Lock()
  5. def eat1(name):
  6. noodle_lock.acquire()
  7. print('%s 抢到了面条'%name)
  8. fork_lock.acquire()
  9. print('%s 抢到了叉子'%name)
  10. print('%s 吃面'%name)
  11. fork_lock.release()
  12. noodle_lock.release()
  13. def eat2(name):
  14. fork_lock.acquire()
  15. print('%s 抢到了叉子' % name)
  16. time.sleep(1)
  17. noodle_lock.acquire()
  18. print('%s 抢到了面条' % name)
  19. print('%s 吃面' % name)
  20. noodle_lock.release()
  21. fork_lock.release()
  22. for name in ['顾客1','顾客2','顾客3']:
  23. t1 = Thread(target=eat1,args=(name,))
  24. t2 = Thread(target=eat2,args=(name,))
  25. t1.start()
  26. t2.start()

Python
Copy
使用递归锁解决问题

  1. import time
  2. from threading import Thread,RLock
  3. noodle_lock = fork_lock = RLock()
  4. def eat1(name):
  5. noodle_lock.acquire()
  6. print('%s 抢到了面条'%name)
  7. fork_lock.acquire()
  8. print('%s 抢到了叉子'%name)
  9. print('%s 吃面'%name)
  10. fork_lock.release()
  11. noodle_lock.release()
  12. def eat2(name):
  13. fork_lock.acquire()
  14. print('%s 抢到了叉子' % name)
  15. time.sleep(1)
  16. noodle_lock.acquire()
  17. print('%s 抢到了面条' % name)
  18. print('%s 吃面' % name)
  19. noodle_lock.release()
  20. fork_lock.release()
  21. for name in ['顾客1','顾客2','顾客3']:
  22. t1 = Thread(target=eat1,args=(name,))
  23. t2 = Thread(target=eat2,args=(name,))
  24. t1.start()
  25. t2.start()

Python
Copy

线程队列

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

  1. import queue
  2. q=queue.Queue()
  3. q.put('first')
  4. q.put('second')
  5. q.put('third')
  6. print(q.get())
  7. print(q.get())
  8. print(q.get())

Python
Copy
先进后出

  1. import queue
  2. q=queue.LifoQueue()
  3. q.put('first')
  4. q.put('second')
  5. q.put('third')
  6. print(q.get())
  7. print(q.get())
  8. print(q.get())

Python
Copy
优先级队列

  1. import queue
  2. q=queue.PriorityQueue()
  3. #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
  4. q.put((20,'a'))
  5. q.put((10,'b'))
  6. q.put((30,'c'))
  7. print(q.get())
  8. print(q.get())
  9. print(q.get())