AI之路(一)——程序、进程、多线程

程序、进程和线程的概念、区别;使用进程、线程的意义

进程: process
线程: thread

程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行,这种执行的程序称之为进程,也就是说进程是系统进行资源分配和调度的一个独立单位,每个进程都有自己单独的地址空间。所以说程序与进程的区别在于,程序是指令的集合,是进程运行的静态描述文本,而进程则是程序在系统上顺序执行时的动态活动

CPU负责控制管理进程,进程是操作系统分配资源(比如内存)的最基本单元,线程是操作系统能够进行调度和分派的最基本单元。
进程下管理的最底层单位是线程,线程是进程的最小执行和分配单元,不能独立运行,必须依附于进程。 在同一个进程的多个线程之间,是内存共享的。
一个程序,可以有多个进程,
一个进程,可以有多个线程,
一个进程里面至少有一个线程
线程是程序运行的最小单位
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
此外,各线程也有自己的局部变量,该局部变量只有线程自己能看见,对于同一进程的其它线程是不可访问的。这些线程的局部变量,称为ThreadLocal

一个程序运行后至少有一个进程,
一个进程中可以有多个线程。

进程的特点:
1.独立性independence
2.并发性concurrency

例子:
打开Microsoft word,这是一个程序
打开一个word文档,这是word程序创建的一个进程;
对word文档进行多种操作,如编辑文档、统计文字数目、修改页面布局、审阅批注等,
每一项操作都是一个线程。

在打开第一个word文档的同时,又打开了第二个word文档,word程序会创建第二个进程。

例子2:
打开网页浏览器,如Chrome,Firefox,IE,360浏览器,QQ浏览器,这是一个程序,
打开一个网页,创建一个进程,
再打开一个网页,创建第二个进程。
【注意,这里不同的应用程序具体实现多任务的方式不同(“单进程多线程/单进程单线程(默认)/多进程中每个进程单线程/多进程多线程”这四种方式),比如,实际上有些网页浏览器都是打开一个网页,就创建一个新线程,而不是进程。
所以这里的例子只是为简单说明,帮助大家理解,实际情况会更复杂。】

1.使用线程的意义
一般程序设计有:“单进程多线程/单进程单线程(默认)/多进程中每个进程单线程/多进程多线程”,这里只讨论“单进程多线程”
  线程在程序设计中是十分重要的,例如在电脑上看电影,就必须由一个线程播放视频,另一个线程播放音频,否则,单线程实现的话就只能先把视频播放完再播放音频,或者先把音频播放完再播放视频,这显然是不行的。
这里需要用到操作系统的知识,一个线程本身是不拥有计算机资源的,进程控制块(PCB)是计算机拥有资源的最小单位(可以理解为进程是拥有计算机资源的最小单位,所谓资源例如cpu使用权,内存空间,寄存器等)。线程只能在进程之中运行,它们根据一定策略轮换的使用进程的资源运行,因此,对于一个应用程序,我们的线程就是活在它的生命周期中,当进程撤销,线程也消失。因为线程不占据资源而是使用进程的资源,所以它的创建销毁的开销都远远小于进程,速度也更快。
  
通俗的理解:
就好比一个干活的项目团队,对于每一个进程,操作系统都会给它分配相应的系统资源,就像项目团队要干活就必须有办公场所、办公桌、办公电脑及相关配套设施等等。
进程里面的线程就好比项目团队里面的很多员工。线程使用的是进程的资源,就好比项目团队里的所有员工都是用的团队的办公场所和办公设备。
所以,操作系统创建和销毁线程的开销,要远小于进程,速度也更快。
就像项目团队里面,来一个人走一个人相对容易,
但是如果一个公司项目来了就招项目团队,团队干完活了又把整个团队都开了(相应的租来的办公场所和办公设备也都还回去了,),下次再有项目再招人租场所,这样会非常慢,开销也很大。

因为在同一个进程中,所以多个线程共享相同的内存空间,不同的线程可以存取内存中的同一个变量,所以,程序中的所有线程都可以读或写进程程序代码中声明过的全局变量,这在一方面是一种非常快捷的通信方式。(进程之间由于不共享内存空间,通信需要专门的方法)

在这里插入图片描述
每个线程都有他自己的一组CPU寄存器,称为线程上下文,线程上下文反映了线程上次运行该线程的CPU寄存器的状态用于恢复环境。指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程在线程上下文更是在进程上下文中运行的。
2.使用线程
  Python的标准库提供了两个模块:thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
以下介绍threading模块基本用法

1.返回当前运行线程的线程名
threading.current_thread().name

2.创建线程:
t = threading.Thread( target=函数名,args=传递给函数的参数 )

3.启动线程
t.start()

4.返回线程名
threading.Thread.getName(线程对象名)
【例子1】

import threading
import time
# 1.创建和启动线程

print( threading.current_thread().name)  # 输出主线程的线程名
time.sleep(3)  # 主线程内运行sleep函数
t=threading.Thread( target=time.sleep ,args=(4,))  # 创建子线程
# t.start() #启动子线程t
print( threading.Thread.getName(t) )  # 主线程内输出子线程t的线程名

print(threading.current_thread().name,'结束!')  # 主线程结束

threading.Thread( target=函数名,args=(i,), name=‘自定义子线程名’)
第一个参数是子线程具体运行的函数/方法名,第二个参数args是一个元组,用于存放传递给函数的参数,如果只传递一个值,就只需要i, 如果需要传递多个参数,那么还可以继续传递下去其他的参数。特别注意其中的逗号不能少,少了就不是元组了,就会报错。
第三个参数name是自定义子线程名,需要是字符串。
默认的子线程名,形式为’Thread-N’的唯一的名字,其中N 是比较小的十进制数。

上面的例子中,注意到主线程结束后,又过了一会,整个程序才结束。
但是,因为子线程运行的是sleep函数,在前台没有什么效果,下面我们将其改为自定义输出函数,进一步考察主线程和子线程运行的关系。

【例子2】
1----创建单线程

def fun(num):
    print("子线程%d执行"%num)
	# print( threading.current_thread().name )
    time.sleep(2)
    print("线程%d执行完毕" % num)
    
# 创建1个子线程
t= threading.Thread(target=fun,args=(1,))
t.start()   # 启动子线程

print('\n主线程结束!\n\n')   # 主线程在执行完for循环后,就直接结束了

2----也可以一次性创建多个线程

def fun(num):
    print("线程%d执行"%num)
    time.sleep(2)
    print("线程%d执行完毕" % num)
    
for i in range(5):  # 主线程中运行for循环,创建5个子线程
    t= threading.Thread(target=fun,args=(i+1,))
    t.start()   # 每创建一个,就同时启动一个

print('\n主线程结束!\n\n')   # 主线程在执行完for循环后,就直接结束了

从运行结果可见,主线程结束以后,子线程仍没有结束,
直到fun函数全部语句都运行完成后,子线程才结束。

  1. setDaemaon()函数的作用
    线程名.setDaemon(True)
    线程名.setDaemon(False) # 该参数默认是False

【例子3】

def fun(num):
    print("线程%d执行"%num)
    time.sleep(2)
    print("线程%d执行完毕" % num)
for i in range(5):
    t= threading.Thread(target=fun,args=(i+1,))
    # t.setDaemon(True)
    t.setDaemon(False)  # 该参数默认是False
    t.start()

print('\n主线程结束!')

setDaemon():
主线程A启动了子线程B,设置setDaemon的参数为True之后,主线程和子线程会同时运行,但主线程A结束运行后,无论子线程B结束与否,都会和主线程一起结束。

1、setDaemon() 的参数,默认是False(也就是说,主线程结束后,子线程仍旧继续运行)
2、设置了t.setDaemon(True) 时,称线程t为守护线程。
3、注意,设置t.setDaemon() 必须要在线程启动之前,即t.start之前

如果你设置一个线程为守护线程,,就表示你在说这个线程是不重要的,在进程退出的时候,
不用等待这个线程退出。
如果你的主线程在退出的时候,不用等待那些子线程完成,那就设置这些线程的daemon属性。即,在线程开始(thread.start())之前,调用setDeamon()函数,设定线程的daemon标志为True。(thread.setDaemon(True))就表示这个线程“不重要”。

如果你想等待子线程完成再退出,那就什么都不用做。,或者显示地调用thread.setDaemon(False),设置daemon的值为false。
新的子线程会继承父线程的daemon标志。整个Python会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。

  1. join()函数的作用
    thread.join([timeout])方法:线程阻塞,用于等待线程终止。

【例子4】

def fun(num):
    print("线程%d执行"%num)
    time.sleep(5)
    print("线程%d执行完毕" % num)

t= threading.Thread(target=fun,args=(1,))
# 设置setDaemon的参数为True,主线程A结束运行后,无论子线程B结束与否,都会和主线程一起结束
t.setDaemon(True)
t.start()
# t.join() 

print('\n主线程结束!')

join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
如在一个线程A中调用thread_b.join()方法,则只有在thread_b结束后,
线程A才会接着thread_b.join()往后运行。
join([timeout])里面的参数时可选的,代表线程运行的最大时间,
即如果超过这个时间,不管这个子线程有没有执行完毕都会被回收,
然后主线程或函数再接着执行。

7.为多线程创建线程列表

【例子5】

list_thread = []  # 存储线程的列表

def fun(num):
    print("线程%d执行"%num)
time.sleep(2)
    print("线程%d执行完毕" % num)
for i in range(5):
    t= threading.Thread(target=fun,args=(i+1,))
    list_thread.append(t)
    # t.setDaemon(True)

for i in list_thread :
    i.start()
# for i in list_thread:
    i.join()  # 将join函数放在线程启动的for循环中


将join函数放在线程启动的for循环中,会导致主线程顺次等待子线程。
因此,在多线程中,应另用一个for循环完成线程的join()。

for i in list_thread :
    i.start()

for i in list_thread:   # 单独用一个for循环实现线程的join
    i.join()

  1. 线程锁
    threading.Lock()方法

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

由于线程之间可以共享数据,而线程交替被送上CPU运行,这时很容易出现的一个问题就是,一个全局变量在被某一个线程修改时,可能还没有达成我们想要得到的结果,就被撤下CUP。这时,下一个被送上CUP的线程也需要取得这个变量的值,这时候,这个值的结果其实并不是我们期望的那个结果了。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

多线程问题一般思路(步骤):

1.首先确定问题中的全局变量是什么,
即,某一任务中所共同访问(修改、读取)的变量是什么。
确定好后,定义并赋值该全局变量。

2.其次,确定该问题中的共同任务是什么,
即,多个线程所需要完成的共同的任务。
确定好后,将任务用自定义函数实现。
(1)在函数开始部分,声明需要用到的全局变量
(2)在函数中对全局变量的访问前后要加线程锁

3.创建并启动多线程

4.调试错误

进程池与线程池
进程(线程)池的意义:
1大型任务中可节省时间
2可以对进程(线程)数进行控制

为什么要使用线程池?
对于任务数量不断增加的程序,每有一个任务就生成一个线程,最终会导致线程数量的失控,例如,整站爬虫,假设初始只有一个链接a,那么,这个时候只启动一个线程,运行之后,得到这个链接对应页面上的b,c,d,,,等等新的链接,作为新任务,这个时候,就要为这些新的链接生成新的线程,线程数量暴涨。在之后的运行中,线程数量还会不停的增加,完全无法控制。所以,对于任务数量不端增加的程序,固定线程数量的线程池是必要的。
既然多线程可以缩短程序运行时间,那么,是不是线程数量越多越好呢?

显然,并不是,每一个线程的从生成到消亡也是需要时间和资源的,太多的线程会占用过多的系统资源(内存开销,cpu开销),而且生成太多的线程时间也是可观的,很可能会得不偿失。

thread.join()方法:线程阻塞

join()方法,用于等待线程终止。join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞。

线程

线程(thread)是进程(process)中的一个实体,一个进程至少包含一个线程。比如,对于视频播放器,显示视频用一个线程,播放音频用另一个线程。如果我们把进程看成一个容器,则线程是此容器的工作单位。

进程和线程的区别主要有:

进程之间是相互独立的,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,但互不影响;而同一个进程的多个线程是内存共享的,所有变量都由所有线程共享;
由于进程间是独立的,因此一个进程的崩溃不会影响到其他进程;而线程是包含在进程之内的,进程中某一个线程的崩溃有可能会导致该进程的崩溃,同一进程内的其他线程也崩溃。

在 Python 中,进行多线程编程的模块有两个:thread 和 threading。其中,thread 是低级模块,threading 是高级模块,对 thread 进行了封装,一般来说,我们只需使用 threading 这个模块。

由于同一个进程之间的线程是内存共享的,所以当多个线程对同一个变量进行修改的时候,就会得到意想不到的结果。

让我们先看一个简单的例子:

from threading import Thread, current_thread

num = 0

def calc():
    global num
    print 'thread %s is running...' % current_thread().name
    for _ in xrange(10000):
        num += 1
    print 'thread %s ended.' % current_thread().name

if __name__ == '__main__':
    print 'thread %s is running...' % current_thread().name

    threads = []
    for i in range(5):
        threads.append(Thread(target=calc))
        threads[i].start()
    for i in range(5):
        threads[i].join()

    print 'global num: %d' % num
    print 'thread %s ended.' % current_thread().name

在上面的代码中,我们创建了 5 个线程,每个线程对全局变量 num 进行 10000 次的 加 1 操作,这里之所以要循环 10000 次,是为了延长单个线程的执行时间,使线程执行时能出现中断切换的情况。现在问题来了,当这 5 个线程执行完毕时,全局变量的值是多少呢?是 50000 吗?

让我们看下执行结果:

thread MainThread is running…
thread Thread-34 is running…
thread Thread-34 ended.
thread Thread-35 is running…
thread Thread-36 is running…
thread Thread-37 is running…
thread Thread-38 is running…
thread Thread-35 ended.
thread Thread-38 ended.
thread Thread-36 ended.
thread Thread-37 ended.
global num: 30668
thread MainThread ended.

我们发现 num 的值是 30668,事实上,num 的值是不确定的,你再运行一遍,会发现结果变了。

原因是因为 num += 1 不是一个原子操作,也就是说它在执行时被分成若干步:

计算 num + 1,存入临时变量 tmp 中;
将 tmp 的值赋给 num.
由于线程是交替运行的,线程在执行时可能中断,就会导致其他线程读到一个脏值。

为了保证计算的准确性,我们就需要给 num += 1 这个操作加上锁。当某个线程开始执行这个操作时,由于该线程获得了锁,因此其他线程不能同时执行该操作,只能等待,直到锁被释放,这样就可以避免修改的冲突。创建一个锁可以通过 threading.Lock() 来实现,代码如下:

from threading import Thread, current_thread, Lock

num = 0
lock = Lock()

def calc():
    global num
    print 'thread %s is running...' % current_thread().name
    for _ in xrange(10000):
        lock.acquire()    # 获取锁
        num += 1
        lock.release()    # 释放锁
    print 'thread %s ended.' % current_thread().name

if __name__ == '__main__':
    print 'thread %s is running...' % current_thread().name

    threads = []
    for i in range(5):
        threads.append(Thread(target=calc))
        threads[i].start()
    for i in range(5):
        threads[i].join()

    print 'global num: %d' % num
    print 'thread %s ended.' % current_thread().name

让我们看下执行结果:

thread MainThread is running…
thread Thread-44 is running…
thread Thread-45 is running…
thread Thread-46 is running…
thread Thread-47 is running…
thread Thread-48 is running…
thread Thread-45 ended.
thread Thread-47 ended.
thread Thread-48 ended.
thread Thread-46 ended.
thread Thread-44 ended.
global num: 50000
thread MainThread ended.

GIL 锁
讲到 Python 中的多线程,就不得不面对 GIL 锁,GIL 锁的存在导致 Python 不能有效地使用多线程实现多核任务,因为在同一时间,只能有一个线程在运行。

GIL 全称是 Global Interpreter Lock,译为全局解释锁。早期的 Python 为了支持多线程,引入了 GIL 锁,用于解决多线程之间数据共享和同步的问题。但这种实现方式后来被发现是非常低效的,当大家试图去除 GIL 的时候,却发现大量库代码已重度依赖 GIL,由于各种各样的历史原因,GIL 锁就一直保留到现在。

小结

一个程序至少有一个进程,一个进程至少有一个线程。
进程是操作系统分配资源(比如内存)的最基本单元,线程是操作系统能够进行调度和分派的最基本单元。
在 Python 中,进行多线程编程的模块有两个:thread 和 threading。其中,thread 是低级模块,threading 是高级模块,对 thread 进行了封装,一般来说,我们只需使用 threading 这个模块。
在执行多线程操作时,注意加锁。

两个例子:

import threading
import time

def aa():
    print(threading.currentThread().name+"   aaa")
    time.sleep(3)
    print(threading.currentThread().name+"   bbb")

thread = threading.Thread(target=aa,)

thread.start()
thread.join(4)
print(threading.currentThread().name+"   111")

import threading
list_ticket=[]
lock=threading.Lock() #获得多线程锁
num=100
for i in range(1,num+1):
    ticket_num="0"*(len(str(num))-len(str(i)))+str(i) #001,003,013
    list_ticket.append(ticket_num)

def seel_ticket():
   global list_ticket
   while len(list_ticket)>0:
       lock.acquire()
       print("正在打印票")
       thre = list_ticket[0]
       del list_ticket[0]
       print("出票成功,票号为:",thre )
       lock.release()
#新建线程
list_thread=[]
for i in range(10):
    thread=threading.Thread(target=seel_ticket,)
    list_thread.append(thread)
for i in list_thread:
    i.start()
i.join()

threading模块的其它函数:

threading.enumerate()
Return a list of all Thread objects currently alive
返回当前存在的所有线程对象的列表

threading.Condition()
可以把Condition理解为一把高级的琐,它提供了比Lock, RLock更高级的功能,允许我们能够控制复杂的线程同步问题。
threading.active_count()
返回当前存活的线程对象的数量(返回当前处于alive状态的Thread对象数量);通过计算len(threading.enumerate())长度而来

threading.get_ident()
返回线程pid

实例方法:
  isAlive(): 返回线程是否在运行。正在运行指启动后、终止前。
  get/setName(name): 获取/设置线程名。

【多进程和多线程】
多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。

在这里插入图片描述

在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值