31、并发编程(进程、线程、协程)


31.1、操作系统:

1、为什么要有操作系统:

(1)介绍:现代计算机系统是由一个或者多个处理器,主存,磁盘,打印机,键盘,鼠标显示器,网络接口以及各种其他输入

输出设备组成的复杂系统,每位程序员不可能掌握所有系统实现的细节,并且管理优化这些部件是一件挑战性极强的工作。

所以,我们需要为计算机安装一层软件,成为操作系统,任务就是用户程序提供一个简单清晰的计算机模型,并管理以上

所有设备。

(2)操作系统的定义:操作系统是一个用来协调、管理和控制计算机硬件和软件资源的系统程序,它位于硬件和应用程序之间。

程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。

(3)操作系统的内核的定义:操作系统的内核是一个管理和控制程序,负责管理计算机的所有物理资源,其中包括:文件系统、

内存管理、设备管理和进程管理。


2、操作系统的历史:

(1)真空管与穿孔卡片(无操作系统);

(2)晶体管和批处理系统;

(3)集成电路芯片和多道程序设计;

(4)个人计算机:随着大规模集成电路的发展,每平方厘米的硅片芯片上可以集成数千个晶体管,个人计算机的时代就此到来了。


31.2、进程和线程:

1、进程:

(1)进程定义:

1)进程就是一个程序在一个数据集上的一次动态执行过程。

2)进程一般由程序、数据集、进程控制块三部分组成。

3)我们编写的程序用来描述进程要完成哪些功能以及如何完成。

4)数据集则是程序在执行过程中所需要使用的资源。

5)进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在

的唯一标志。

6)在操作系统中,一个运行的应用程序至少有一个进程,单核cpu在同一时刻只能处理一个进程,如果有多个进程,操作系统只

能让cpu内核在不同的进程之间进行切换(这叫做程序的并发),因为cpu的速度非常快,所以我们在单核cpu下开多个应用程序时,

感觉不到程序的延迟,就像多个程序并行执行一样,如果应用程序开的较多就会感觉到应用到程序的延迟。

如果是多核cpu,那么不同的内核在同一时刻处理不同的程序进程(这叫做程序的并行),但是由于操作系统的进程数量远远多于CPU

的核心数量,所以,操作系统会调用多核cpu同时轮流处理多个进程,因此电脑的核心数越多,电脑的性能越好。

(以上对于线程也是同样的道理)

(2)举一例说明进程:

想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、

香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),而做蛋糕的各种原料就

是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。现在假设计算机科学家的儿子哭着跑了进来,

说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的

指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食

谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。


2、线程:

(1)线程的定义:

1)线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆

栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。

2)线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程同一时间只能干一样事的缺陷,使进程内并发成为

可能。

(2)举一例说明线程:

假设一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,就会造成同一时间只能干

一样事的尴尬(当通过键盘输入内容时就无法在屏幕上显示内容,也不能将信息保存到磁盘中)。若有多个进程,每个进程负责一个任

务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C

间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西-------文本内容,不停的切换造成性能上的损失。若有一种机制,可以

使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。这种

机制就是线程。


3、进程和线程的关系:

(1)一个程序至少有一个进程,一个进程至少有一个线程。(进程可以理解成线程的容器)

(2)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

(3)进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进

程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一

点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线

程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

(进程是操作系统上的概念,是操作系统进行资源(包括cpu、内存、磁盘IO等)分配的最小单位;CPU看到的是线程,线程是程序执行的

最小单位。)

(4)如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。


4、python的GIL:

(1)什么是GIL:

GIL的全称是 Global Interpreter Lock,全局解释器锁。之所以叫这个名字,是因为Python的执行依赖于解释器。Python最初的设计理念

在于为了解决多线程之间数据完整性和状态同步的问题,设计为在任意时刻只有一个线程在解释器中运行。而当执行多线程程序时,由GIL

来控制同一时刻只有一个线程能够运行。即Python中的多线程是表面多线程,不是真正的多线程。同一时刻只有一个线程能够运行,那么

python是怎么执行多线程程序的呢,其实原理很简单,解释器的分时复用。即多个线程的代码,轮流被解释器执行只不过切换的频繁很

快,给人一种多线程“同时”在执行的错觉,其实就是“并发”。Python为什么要保证同一时刻只有一个线程在解释器中运行呢,答案是

为了Python解释器中原子操作的线程安全。

(无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行,可以理解为单核cpu下的多线程

的处理,是程序的并发,无法发挥多核cpu的性能。)

(2)“并发”和“并行”的概念:

1)普通解释:

并发:交替做不同事情的能力(单核cpu轮流处理多个进程或多个线程的能力;多核cpu同时轮流处理多个进程或多个线程的能力)

并行:同时做不同事情的能力(多核处cpu同时处理多个进程或多个线程的能力)

2)专业术语:

并发:不同的代码块交替执行

并行:不同的代码块同时执行


5、python的线程和threading模块:

(1)线程的调用(直接调用):

1)threading 模块建立在thread 模块之上。thread模块以低级、原始的方式来处理和控制线程,而threading模块通过对thread进行二次封

装,提供了更方便的api来处理线程。

2)示例:

import threading

import time


def sayhi(num): # 定义每个线程要运行的函数

print("running on number:%s" % num)

time.sleep(3)


if __name__ == '__main__':

t1 = threading.Thread(target=sayhi, args=(1,))

# 生成一个线程实例,target:要运行的函数,

t2 = threading.Thread(target=sayhi, args=(2,))

# 生成另一个线程实例


t1.start()

# 启动线程

t2.start()

# 启动另一个线程


print(t1.getName())

# 获取线程名

print(t2.getName())

print('the end')

"""

输出结果:

running on number:1

running on number:2

Thread-1

Thread-2

the end

# 上面的内容同时打印,然后等待3秒后退出程序;

"""

(2)线程实例的方法:

1)join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞,必须在start()方法调用之后设置。

2)setDaemon(True):将子线程声明为守护线程,必须在start()方法调用之前设置,只要主线程完成了,

不管子线程是否完成,都要和主线程一起退出。(父线程等待没有设置守护线程的子线程)

3)默认:父子线程同时运行,如果子线程没有运行完成,父线程将等待子线程。

4)其它方法:

run(): 线程被cpu调度后自动执行线程对象的run方法

start(): 启动线程活动。

isAlive(): 返回线程是否活动的。

getName(): 返回线程名。

setName(): 设置线程名。

threading模块提供的一些方法:

threading.currentThread(): 返回当前的线程变量。

threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。

threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

5)示例:

示例一:

import threading

from time import ctime,sleep


def ListenMusic(name):

print ("Begin listening to %s. %s" %(name,ctime()))

sleep(3)

print("end listening %s"%ctime())


def RecordBlog(title):

print ("Begin recording the %s! %s" %(title,ctime()))

sleep(5)

print('end recording %s'%ctime())


threads = []

t1 = threading.Thread(target=ListenMusic,args=('水手',))

t2 = threading.Thread(target=RecordBlog,args=('python线程',))

threads.append(t1)

threads.append(t2)


if __name__ == '__main__':

# t2.setDaemon(True)

# 注意:一定在start之前设置


for t in threads:

t.start()

t1.join()

# 注意:一定在start之后设置

print ("all over %s" %ctime())

"""

输出结果:

Begin listening to 水手. Wed Oct 9 17:11:08 2019

Begin recording the python线程! Wed Oct 9 17:11:08 2019

# 同时输出上面2行内容,3秒后同时输出下面2行内容;

end listening Wed Oct 9 17:11:11 2019

all over Wed Oct 9 17:11:11 2019

# 2秒后输出下面一行内容;

end recording Wed Oct 9 17:11:13 2019

"""


示例二:

import threading

from time import ctime,sleep


def ListenMusic(name):

print ("Begin listening to %s. %s" %(name,ctime()))

sleep(3)

print("end listening %s"%ctime())


def RecordBlog(title):

print ("Begin recording the %s! %s" %(title,ctime()))

sleep(5)

print('end recording %s'%ctime())


threads = []

t1 = threading.Thread(target=ListenMusic,args=('水手',))

t2 = threading.Thread(target=RecordBlog,args=('python线程',))

threads.append(t1)

threads.append(t2)


if __name__ == '__main__':

t2.setDaemon(True)

# 注意:一定在start之前设置


for t in threads:

t.start()

# t1.join()

# 注意:一定在start之后设置

print ("all over %s" %ctime())

"""

输出结果:

Begin listening to 水手. Wed Oct 9 17:45:00 2019

Begin recording the python线程! Wed Oct 9 17:45:00 2019

all over Wed Oct 9 17:45:00 2019

# 同时输出上面3行内容,3秒后同时输出下面1行内容;

end listening Wed Oct 9 17:45:03 2019

"""


示例三:

import threading

from time import ctime,sleep


def ListenMusic(name):

print ("Begin listening to %s. %s" %(name,ctime()))

sleep(3)

print("end listening %s"%ctime())


def RecordBlog(title):

print ("Begin recording the %s! %s" %(title,ctime()))

sleep(5)

print('end recording %s'%ctime())


threads = []

t1 = threading.Thread(target=ListenMusic,args=('水手',))

t2 = threading.Thread(target=RecordBlog,args=('python线程',))

threads.append(t1)

threads.append(t2)


if __name__ == '__main__':

t2.setDaemon(True)

# 注意:一定在start之前设置


for t in threads:

t.start()

t1.join()

# 注意:一定在start之后设置

print ("all over %s" %ctime())


"""

# 输出结果:

Begin listening to 水手. Mon Sep 23 00:21:19 2019

Begin recording the python线程! Mon Sep 23 00:21:19 2019

# 同时输出上面2行内容,3秒后同时输出下面2行内容;

end listening Mon Sep 23 00:21:22 2019

all over Mon Sep 23 00:21:22 2019

"""



31.3、补充:

1、线程和进程图示:


2、核数和线程数:

所谓的4核8线程,4核指的是物理核心。通过超线程技术,用一个物理核模拟两个虚拟核,每个核两个线程,总数为8线程。

四核八线程采用的超线程技术,是指每个CPU核心没有满负荷运载时,其剩余用量可以模拟成虚拟的核心。单个物理核同一

时间点只能处理一个线程,通过超线程技术可以实现单个物理核实现线程级别的并行计算,但是比不上性能两个物理核。

CPU看到的是线程,线程是安排CPU执行的最小单位。

进程是操作系统上的概念,是操作系统进行资源(包括cpu、内存、磁盘IO等)分配的最小单位。


3、多线程调用同一个函数问题:

(1)局部变量是不会被修改的,因为对于一个局部变量应该是各线程独立的,一个线程不会改变另一个线程的临时变量;


(2)全局变量则很容易就被修改了,所以多线程同时访问全局变量时需要加锁,否则会出现一些莫名其妙的情况;















猜你喜欢

转载自www.cnblogs.com/LiuChang-blog/p/12320596.html
今日推荐