[python]生动理解进程与线程

182 阅读5分钟

1/前言

计算机中的CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
假定工厂的电力有限,一次只能供给一个车间。
也就是说,一个车间开工的时候,其他车间都必须停工。

背后的含义就是,单核CPU一次只能运行一个任务(应用程序,进程)。
这就是现在人们想买多核cpu笔记本的原因。

进程就好比工厂的车间,它代表cpu所能处理的单个任务。
任一时刻,单核cpu总是运行一个进程,其他进程处于非运行状态。
cpu在多个进程之间快速切换,给人一种“同时执行”的错觉。
但是如果进程很多,在切换的时候就会造成电脑卡顿。

一个车间里,可以有很多工人,他们协同完成一个任务。(当然也可以只有一个工人) 

线程就好比某个车间里的工人,一个进程可以包括多个线程(或只有一个线程)。
一个人也可以完成一件时间,多个人也可以协同完成一件事情。

车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。
这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

可是,每间房间的大小不同,有些房间最多只能容纳一个人。

比如厕所,里面有人的时候,其他人就不能进去了。这代表一个线程在使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

一个防止他人进入的简单方法,就是门口加一把锁。
先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。
这就叫”互斥锁”,防止多个线程同时读写某一块内存区域。

还有些房间,可以同时容纳n个人,比如厨房。
也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
这种情况的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。
后到的人发现钥匙架空了,就知道必须在门口排队等着了。
这种做法叫做”信号量”(Semaphore),用来保证多个线程不会互相冲突。

不难看出,互斥锁是信号量的一种特殊情况(n=1时)。
也就是说,完全可以用后者替代前者。但是,因为mutex(互斥锁)较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

2/总结

操作系统的设计,因此可以归结为三点:
<1>以多进程形式,允许多个任务同时运行(并行);
<2>以多线程形式,允许单个任务分成不同的部分运行;
<3>提供协调机制,一方面防止进程之间和线程之间产生冲突,
   另一方面允许进程之间和线程之间共享资源。

Python获取多线程返回值的两种方式

通过复写Thread类,自定义一个get_result()方法
  from threading import Thread
  
  def cal_sum(a,b):
      return a+b
  
  class MyThread(Thread): # 继承Thread类
      def __init__(self, func, args):
          super(MyThread, self).__init__()
          self.func = func
          self.args = args
           
      def run(self):
          self.result = self.func(*self.args)
          
      def get_result(self):
          try:
              return self.result
          except Exception:
              return None
              
  if __name__ == "__main__":
      # 实例化对象
      # 实例化出来2个子线程
      subt1 = MyThread(cal_sum,args=(1,5))
      subt2 = MyThread(cal_sum,args=(6,10))
      subt1.start()
      subt2.start()
      
      subt1.join()
      subt2.join()
      
      res1 = subt1.get_result()
      res2 = subt2.get_result()
      
      print("主线程开始......")
      print(res1 + res2)
      print("主线程结束......")

为什么要使用多线程

线程是并发的执行流,与独立的进程相比,一个进程中各个线程之间的隔离程度小,容易切换。
他们共享内存,文件句柄和其他进程应有的状态
因为线程的划分尺度小于进程,使得多线程程序的并发性高,也就是多个线程之间切换。
进程在执行的时候拥有独立的内存单元,而多个线程共享内存,从而极大的提高了程序的运行效率。

主线程,子线程,守护线程

<1>看下面的2个例子,
   这里使用setDaemon(True)把所有的子线程都变成了主线程的守护线程,
   因此当主线程结束后,子线程也会随之结束。当主线程结束后,整个程序就退出了。
   根据最后的输出结果,我们可以看到,当主线程结束,子线程也将立即结束。
   
    import threading
    import time
    
    def f(n):
      print("task",n)
      time.sleep(1)
      
      print(3)
      time.sleep(1)
      
      print(2)
      time.sleep(1)
      print(1)
    
     if __name__ == "__main__":
        subt = threading.Thread(target=f,args=("t1",))
        subt.setDaemon(True)
        subt.start()
        
        print("end")
  上面代码的执行结果是:
    task,t1
    end

image.png

<2>主线程等待子线程结束。
   为了让守护线程执行结束之后,主线程再结束,我们可以使用join方法,
   让主线程等待子线程执行。
   
    import threading
    import time
    
    def main(n):
      print("task",n)
      time.sleep(1)
      print(3)
      time.sleep(1)
      print(2)
      time.sleep(1)
      print(1)
    
     if __name__ == "__main__":
        t = threading.Thread(target=main,args=("t1",))
        t.setDaemon(True)
        t.start()
        print("end")
上面代码的执行结果是
 task t1
 3
 2
 1
 end

多线程代码实例

<1> 多线程得到返回结果
      from threading import Thread
      
      class MyThread(Thread):
          def __init__(self,func,args=()):
              super(MyThread,self).__init__()
              self.func = func
              self.args = args
              
          def run(self):
              self.result = self.func(*self.args)
              
          def get_result(self):
              try:
                  # 如果子线程不使用join方法,此处可能会报没有self.result的错误
                  return self.result  
              except Exception:
                  return None
                  
      def f(raw_list, n):
          for i in range(0, len(raw_list), n):
              yield raw_list[i:i + n]
              
              
      threads_pool = []  # 线程池
      for i in temp:
          subt = MyThread(f,args=(final_data_df,i))  # 创建线程对象
          threads_pool.append(subt)  # 添加到线程池

      for t in threads_pool:
          # 把每一个子线程都设置为守护线程,这样当主线程结束后,子线程也就结束了,
          # 不然当主线程结束了,子线程会一直被挂起
          t.setDaemon(True) 
          t.start()  # 执行子线程

      # 一定要join,目的是堵塞主线程,不然主线程比子线程跑的快,
      # 子线程就会跟着主线程的结束而结束,得不到子线程的结果
      # 每一个子线程都要堵塞主线程
      iterative_df_list = []
      for t in threads:
          t.join()
          iterative_df_list.append(t.get_result())  # 得到子线程的返回结果
      
      final_data_df = reduce(lambda a,b: pd.concat([a,b],sort=Fasle),iterative_df_list).reset_index(drop=True)