RPC前世今生

Does developer convenience really trump correctness, scalability, performance, separation of concerns, extensibility, and accidental complexity?” Vinoski (2008) 开发者的便利性真的比正确性、可扩展性、性能、关注点分离、可扩展性和偶然复杂度更重要吗? 大纲 RPC 基础介绍 RPC 发展历程 RPC 介绍 远程过程调用(Remote Procedure Call,RPC)是一种允许两个实体通过通用请求/响应机制的通信通道进行通信的设计范例。RPC 的定义在过去三十年中发生了重大的变化和演变,因此 这里RPC 范式是一个广义的分类术语,指的是过去四十年中出现的所有 RPC 式系统。RPC 的定义经过几十年的发展。它已经从一个简单的客户端-服务器设计转移到一组相互连接的服务。虽然最初的 RPC 实现被设计为将计算外包给分布式系统中的服务器的工具,但 RPC 经过多年的发展,已经构建了一个与语言无关的应用程序生态系统。RPC 范式已经成为创建真正革命性的分布式系统的驱动力的一部分,并且在不同系统之间产生了各种通信方案和协议。 最简单的 RPC 实现如图1所示。在这种情况下,客户端(或调用方)和服务器(或被调用方)被一个物理网络分开。系统的主要组件是客户端例程/程序、客户端存根、服务器例程/程序、服务器存根和网络例程。存根是一个小程序,通常用作较大程序的替代程序(或接口)。客户端存根向客户端例程公开服务器例程提供的功能,而服务器存根向服务器例程提供类似于客户端的程序。客户端存根从客户端程序获取输入参数并返回结果,而服务器存根向服务器程序提供输入参数并获取结果。客户端程序只能与客户端存根交互,后者为客户端提供远程服务器的接口。这个存根还序列化客户端例程发送到存根的输入参数。类似地,服务器存根为服务器例程提供客户端接口,并处理发送到客户端的数据序列化。 当客户端例程执行远程过程时,它调用客户端存根,该存根序列化输入参数。这个序列化数据使用 OS 网络例程(TCP/IP)发送到服务器。然后,服务器存根将数据反序列化,并使用给定的参数提供给服务器例程。来自服务器例程的返回值再次序列化,并通过网络发送回客户端,在那里客户端存根对其进行反序列化,并显示给客户端例程。这个远程过程通常对客户端例程隐藏,并作为本地过程显示给客户端。RPC 服务还需要一个发现服务/主机解析机制来引导客户端和服务器之间的通信。 完整的 RPC 框架 在一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。 RPC 的发展历程 1969年11月,ARPAnet 开始建立。 1969年,美国国防部高级研究计划管理局(ARPA全称:Advanced Research Projects Agency)开始建立一个命名为ARPAnet的网络。最开始只有4个结点,分别是洛杉矶的加利福尼亚州大学洛杉矶分校、加州大学圣巴巴拉分校、斯坦福大学、犹他州大学四所大学的4台大型计算机。选择这四个节点的一个原因是要测试不同类型主机联网的兼容性。 1974年:Jon Postel 和 Jim White发表了RFC674 过程调用最早可以追溯到 Jon Postel 和 Jim White 在1974 年发表的 Procedure Call Protocol Documents Version 2(RFC674)。这个协议试图定义一种通用的方法,用于解决 NSW 项目中多个计算节点通信的问题。 ...

2023-05-11 · 5 min · 872 words

学习单元测试,告别祈祷式编程

[TOC] 祈祷式编程 祈祷式编程 如果代码中包含以下代码 或者上线后进行这种活动 那么这种编程方式就是祈祷式编程。 用流程图表示基本就是这个样子。 祈祷式编程有什么危害呢? 累,每次写完代码还需要再祈祷 不受控,代码运行结果主要看运气,大仙忙的时候可能保佑不了 解决这个问题有好多种方法,单元测试是其中之一。 单元测试 什么是单元测试 单元测试是由开发人员编写的,用于对软件基本单元进行测试的可执行的程序。 单元(unit)是一个应用程序中最小的课测试部分。(比如一个函数,一个类 google 把测试分成小型测试、中型测试和大型测试。单元测试基本和小型测试的作用类似,但是通常也会使用mock或者stub 的方式模拟外部服务。 理想情况下,单元测试应该是相互独立、可自动化运行的。 目的: 通常用单元测试来验证代码逻辑是否符合预期。完整可靠的单元测试是代码的安全网,可以在代码修改或重构时验证业务逻辑是否正确,提前发现代码错误,减少调试时间。设计良好的单元测试某些情况下可以比文档更能反应出代码的功能和作用。 单元测试这么多优点为什么有人不喜欢写单元测试呢? 单元测试太费时间了,对于编写单元测试不熟练的新手来说,编写单元测试可能比写代码的还费时间 单元测试运行时间太长(这通常是单元测试设计不合理或者代码可测试性较差造成的 祖传代码,看都看不懂怎么写单元测试(这个确实优点棘手。。可以考虑先给新代码加单元测试 不会写单元测试 这篇文章主要关注第四个问题,如何写单元测试。 单元测试的结构 首先看一下单元测试的结构,一个完整的单元测试主要包括Arrange-Act-Assert(3A) 三部分。 Arrange–准备数据 Act–运行代码 Assert–判断结果是否符合预期 比如我们要给下面这段代码(golang)加单元测试: func Add(x, y int) int { return x + y } 单元测试代码如下: import "testing" func TestAdd(t *testing.T) { // arrange 准备数据 x, y := 1, 2 // act 运行 got := Add(x, y) //assert 断言 if got != 3 { t.Errorf("Add() = %v, want %v", got, 3) } } 如何编写好的单元测试 什么样的单元测试才是好的单元测试呢? ...

2019-10-07 · 3 min · 625 words

PostgreSQL jsonb 使用入门

json 类型 说明 根据RFC 7159中的说明,JSON 数据类型是用来存储 JSON(JavaScript Object Notation)数据的。这种数据也可以被存储为text,但是 JSON 数据类型的优势在于能强制要求每个被存储的值符合 JSON 规则。也有很多 JSON 相关的函数和操作符可以用于存储在这些数据类型中的数据 PostgreSQL支持两种 JSON 数据类型:json 和 jsonb。它们几乎接受完全相同的值集合作为输入。两者最大的区别是效率。json数据类型存储输入文本的精准拷贝,处理函数必须在每 次执行时必须重新解析该数据。而jsonb数据被存储在一种分解好的二进制格式中,因为需要做附加的转换,它在输入时要稍慢一些。但是 jsonb在处理时要快很多,因为不需要重新解析。 重点:jsonb支持索引 由于json类型存储的是输入文本的准确拷贝,存储时会空格和JSON 对象内部的键的顺序。如果一个值中的 JSON 对象包含同一个键超过一次,所有的键/值对都会被保留(** 处理函数会把最后的值当作有效值**)。 jsonb不保留空格、不保留对象键的顺序并且不保留重复的对象键。如果在输入中指定了重复的键,只有最后一个值会被保留。 推荐把JSON 数据存储为jsonb 在把文本 JSON 输入转换成jsonb时,JSON的基本类型(RFC 7159 )会被映射到原生的 PostgreSQL类型。因此,jsonb数据有一些次要额外约束。 比如:jsonb将拒绝除 PostgreSQL numeric数据类型范围之外的数字,而json则不会。 JSON 基本类型和相应的PostgreSQL类型 JSON 基本类型 PostgreSQL类型 注释 string text 不允许\u0000,如果数据库编码不是 UTF8,非 ASCII Unicode 转义也是这样 number numeric 不允许NaN 和 infinity值 boolean boolean 只接受小写true和false拼写 null (无) SQL NULL是一个不同的概念 json 输入输出语法 -- 简单标量/基本值 -- 基本值可以是数字、带引号的字符串、true、false或者null SELECT '5'::json; -- 有零个或者更多元素的数组(元素不需要为同一类型) SELECT '[1, 2, "foo", null]'::json; -- 包含键值对的对象 -- 注意对象键必须总是带引号的字符串 SELECT '{"bar": "baz", "balance": 7.77, "active": false}'::json; -- 数组和对象可以被任意嵌套 SELECT '{"foo": [true, "bar"], "tags": {"a": 1, "b": null}}'::json; -- "->" 通过键获得 JSON 对象域 结果为json对象 select '{"nickname": "goodspeed", "avatar": "avatar_url", "tags": ["python", "golang", "db"]}'::json->'nickname' as nickname; nickname ------------- "goodspeed" -- "->>" 通过键获得 JSON 对象域 结果为text select '{"nickname": "goodspeed", "avatar": "avatar_url", "tags": ["python", "golang", "db"]}'::json->>'nickname' as nickname; nickname ----------- goodspeed -- "->" 通过键获得 JSON 对象域 结果为json对象 select '{"nickname": "goodspeed", "avatar": "avatar_url", "tags": ["python", "golang", "db"]}'::jsonb->'nickname' as nickname; nickname ------------- "goodspeed" -- "->>" 通过键获得 JSON 对象域 结果为text select '{"nickname": "goodspeed", "avatar": "avatar_url", "tags": ["python", "golang", "db"]}'::jsonb->>'nickname' as nickname; nickname ----------- goodspeed 当一个 JSON 值被输入并且接着不做任何附加处理就输出时, json会输出和输入完全相同的文本,而jsonb 则不会保留语义上没有意义的细节 ...

2019-05-30 · 10 min · 1984 words

创建高质量的代码--软件构建中的设计

《代码大全》读书笔记 太长不看版 软件构建中的设计 软件设计是一项明确的活动 设计中的挑战 软件设计一词意味着去构思、创造或发明一套方案,把一份计算机软件的规格说明书要求转变为可实际运行的软件。 设计就是把需求分析和编码调试连接在一起的活动。 好的高层词设计能够提供一个可以稳妥容纳多个较低层次设计的结构。 设计是一个险恶的问题 险恶(wicked)的问题就是那种只有通过解决或部分解决才能被明确的问题。 Tacoma Narrows 大桥是一个险恶问题的好例子,因为直到这座桥坍塌,工程师才知道不应该只考虑桥的负荷,还需要充分的考虑空气动力学因素(只有建造大桥,才能从中学到需要考虑额外的环节)。 设计是一个了无章法的过程(即使它能得处清爽的成果) 是因为在设计的过程中可能会采用很多错误的步骤,多次出错 因为设计的优劣差异往往非常微妙 因为不能判断设计是否足够好 设计就是确定取舍和调整顺序的过程 现实世界中,设计者工作的一个关键内容就是衡量彼此冲突的各项设计特性,并尽力在其中寻求平衡。响应速度优先和开发时间短优先得出的设计结果可能是不同的。 设计受到诸多限制 设计的要点一部分是在创造可能发生的事情,另一部分是在限制可能发生的事情。 如果一个人有无限空间和资源来建造房子,可能会建造出无法控制的建筑。正是因为有了限制,才得出了简单的结果。软件设计也是一样。 设计是不确定的 每个人设计的结果可能是不同的,并且可能用起来都不错。设计没有标准答案。 设计是一个启发式的过程 设计过程中充满了不确定性,因此设计技术也趋于具有探索性–“经验法则”或者“试试没准能行”–而不是保证能产生预期结果的可重复的过程。 设计是自然而然形成的 设计不是在谁的头脑中直接跳出来的,它是在不断的设计评估、非正式讨论、写试经验以及修改试验代码中演化和完善的。 关键的设计概念 软件的首要技术使命:管理复杂度 本质的难题和偶然的难题 偶然的难题可以理解为bug,编程语言笨拙的语法,等易于发现容易解决的问题。 本质的难题则比较复杂,本质上说,软件开发就是不断去发掘错综复杂,相互关连的整套概念的所有细节。本质困难就是: 要面对复杂、无序的现实世界; 精确而完成的识别出各种依赖关系和外部情况 设计出完全正确而不是大致正确的解决方案 。。。 管理复杂度的重要性 一个失败的项目如果是由于技术原因而失败,通常都是因为软件复杂度失控了。如果复杂度失控,那么软件就会变得极端复杂,没有人知道它能做什么,它出了问题如何解决。 管理复杂度是软件开发中最为重要的技术话题。 在软件架构层次上,可以通过把大的系统分解为多个子系统来降低问题的复杂度,多个简单的问题比一个复杂的大问题更容易理解。 子系统相互间应该减少依赖; 子系统的关注点应该是相互分离的。 如何应对复杂度 高代价、低效率的设计源于下面三种根源: 用复杂的方法解决简单的问题 用简单但错误的方法解决复杂的问题 用不恰当的复杂的方法解决复杂的问题 用下面的方法管理复杂度 把任何人在同一时间需要处理的本质复杂度降到最低 不要让偶然性的复杂度无谓的增长 理想的设计特征 最小的复杂度 易于维护 松散耦合 可扩展性 可重用性 高扇入:让大量的类使用某个给定的类。(意味着设计出的系统很好的利用了在较低层次上的工具类 低扇出:让一个类少量或始终的使用其他类。(高扇出(7个)意味着一个类过多的使用了其他类,可能会变得过于复杂 可移植性 精简性:没有多余的部分 层次性:比如一个新系统会用到很多设计不佳的旧系统,这时就应该为新系统编写一个负责同就代码交互的层(代理模式) 层次性能把低劣的代码紧闭起来 如果能最终抛弃或重构旧代码,旧不必修改处交互层之外的任何新代码。 标准技术:用到的外来的、古怪的东西越多,也越难理解。 设计的层次 1. 软件系统(Software System) 2. 分解为子系统或包(Division into Subsystems or Packages) 这一层的主要目的是确定如何把程序分为主要的子系统,并定义清楚允许各子系统如何使用其他子系统。 ...

2019-05-29 · 2 min · 305 words

JWT RefreshToken 实践

Json web token (JWT), 根据官网的定义,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。 详细介绍可以查看这篇文章 理解JWT(JSON Web Token)认证及实践 JWT 特点 优点 体积小,因而传输速度快 传输方式多样,可以通过URL/POST参数/HTTP头部等方式传输 严格的结构化。它自身(在 payload 中)就包含了所有与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且 payload 支持为你的应用而定制化。 支持跨域验证,可以应用于单点登录。 存在的问题 JWT 自身(在 payload 中)就包含了所有与用户相关的验证消息,所以通常情况下不需要保存。这种设计存在几个问题: Token不能撤销–客户端重置密码后之前的JWT依然可以使用(JWT 并没有过期或者失效 不支持refresh token,JWT过期后需要执行登录授权的完整流程 无法知道用户签发了几个JWT 针对第一个问题,可能的解决方法有: 保存JWT到数据库(或Redis),这样可以针对每个JWT单独校验 在重置密码等需要作废之前全部JWT时,把操作时间点记录到数据库(或Redis),校验JWT时同时判断此JWT创建之后有没有过重置密码等类似操作,如果有校验不通过 当然,这种解决方法都会多一次数据库请求,JWT自身可校验的优势会有所减少,同时也会影响认证效率。 这篇文章主要介绍解决第二个问题(不支持refresh token)的思路。 refresh token refresh token是OAuth2 认证中的一个概念,和OAuth2 的access token 一起生成,表示更新令牌,过期所需时间比access toen 要长,可以用来获取下一次的access token。 如果JWT 需要添加 refresh token支持,refresh token需要满足的条件有一下几项: 和JWT一起生成返回给客户端 有实效时间,有效时间比JWT要长 只能用来换取下一次JWT,不能用于访问认证 不能重复使用(可选) refresh token 获取流程 refresh token 使用流程 代码示例 import jwt import time # 使用 sanic 作为restful api 框架 def create_token(account_id, username): payload = { "iss": "gusibi.mobi", "iat": int(time.time()), "exp": int(time.time()) + 86400 * 7, "aud": "www.gusibi.mobi", "sub": account_id, "username": username, "scopes": ['open'] } token = jwt.encode(payload, 'secret', algorithm='HS256') payload['grant_type'] = "refresh" refresh_token = jwt.encode(payload, 'secret', algorithm='HS256') return True, { 'access_token': token, 'account_id': account_id, "refresh_token": refresh_token } # 验证refresh token 出否有效 def verify_refresh_token(token): payload = jwt.decode(token, 'secret', audience='www.gusibi.com', algorithms=['HS256']) # 校验token 是否有效,以及是否是refresh token,验证通过后生成新的token 以及 refresh_token if payload and payload.get('grant_type') == 'refresh': # 如果需要标记此token 已经使用,需要借助redis 或者数据库(推荐redis) return True, payload return False, None # 验证token 是否有效 def verify_bearer_token(token): # 如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数 payload = jwt.decode(token, 'secret', audience='www.gusibi.com', algorithms=['HS256']) # 校验token 是否有效,以及不能是refresh token if payload and not payload.get('grant_type') == 'refresh': return True, payload return False, None 参考链接 理解JWT(JSON Web Token)认证及实践 理解OAuth 2.0[1] References [1] 理解OAuth 2.0: http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html ...

2019-04-29 · 1 min · 204 words

SQLAlchemy in 空列表问题分析

SQLAlchemy in 空列表问题 问题场景 有model Account,SQLAlchemy 查询语句如下: query = Account.query.filter(Account.id.in_(account_ids)).order_by(Account.date_created.desc()) 这里 account_ids 如果为空,执行查询会有如下警告: /usr/local/lib/python2.7/site-packages/sqlalchemy/sql/default_comparator.py:35: SAWarning: The IN-predicate on "account.id" was invoked with an empty sequence. This results in a contradiction, which nonetheless can be expensive to evaluate. Consider alternative strategies for improved performance. return o[0](self, self.expr, op, *(other + o[1:]), **kwargs) 这里的意思是使用一个空的列表会花费较长的时间,需要优化以提高性能。 为什么会有这个提示呢?一个空列表为什么会影响性能呢? 首先打印 query 可得到如下 sql 语句: SELECT * // 字段使用 “*” 代替 FROM account WHERE account.id != account.id ORDER BY account.date_created DESC 会发现生成的语句中过滤条件是 WHERE account.id != account.id,使用 PostgreSQL Explain ANALYZE 命令, ...

2018-10-04 · 3 min · 542 words

使用github+travis将Python包部署到Pypi

我在 github 托管 Python 代码,然后将包发布到 Pypi,通常的操作步骤是,更新完代码将提交到 github ,然后手动将包更新到 pypi,这样比较繁琐,就想到了使用github+travis-ci 构建一个自动部署环境。 注册 pypi 访问https://pypi.org 点击Register注册账号,记住自己的用户名密码。 创建 setup.py 文件 setup.py 文件放置于包的根目录,示例内容如下: #!/usr/bin/env python from setuptools import setup, find_packages with open("README.md", "r") as fh: long_description = fh.read() with open('requirements.txt') as f: requirements = [l for l in f.read().splitlines() if l] setup(name="python-weixin", # 项目名 version="0.3.2", # 版本号 description="Python Weixin API client support wechat-app", #简介 long_description=long_description, # 长简介 这里使用的 readme 内容 long_description_content_type="text/markdown", license="BSD", # 授权 install_requires=requirements, # 依赖 author="gusibi", # 作者 author_email="[email protected]", # 邮箱 url="https://github.com/gusibi/python-weixin", # 地址 download_url="https://github.com/gusibi/python-weixin/archive/master.zip", packages=find_packages(), keywords=["python-weixin", "weixin", "wechat", "sdk", "weapp", "wxapp"], zip_safe=True) 以上特别需要注意的是 packages参数,用来申明你的包里面要包含的目录,这里使用setuptools自动决定要包含哪些包。 ...

2018-07-23 · 2 min · 308 words

newrelic python agent 源码分析-1

Newrelic 是APM(Application Performance Management)(应用性能管理/监控)解决方案提供商。项目中,通常用它来追踪应用的性能。最近看了一下 newrelic-python-agent 源码,这是查看源码过程中的一些记录。 目录结构 newrelic 目录结构如下: newrelic ├── admin # 常用命令 ├── api # 探针 ├── bootstrap ├── common ├── core ├── extras │ └── framework_django │ └── templatetags ├── hooks # 数据库 web 各个库的一些探针 │ ├── framework_tornado │ ├── framework_tornado_r3 │ └── framework_tornado_r4 ├── network ├── packages │ ├── requests │ │ └── packages │ │ ├── chardet │ │ └── urllib3 │ │ ├── packages │ │ │ └── ssl_match_hostname │ │ └── util │ └── wrapt └── samplers 命令 使用 newrelic-admin help 可以列出所有命令: ...

2018-05-16 · 3 min · 509 words

《理解 unix 进程》笔记-1

UNIX 进程 系统调用 Unix 系统是由用户空间(userland)和内核组成。Unix 内核位于计算机硬件之上,是与硬件交互的中介。这些交互包括通过问卷系统进程读/写、在网络上发送数据、分配内存,以及通过扬声器播放音频。这些都是用户应用程序所不能涉及的,只能通过系统调用来完成。 系统调用为内核和用户空间搭建了桥梁。规定了程序和计算机硬件直接所允许发生的一切交互。 进程是 Unix 系统的基石,所有的代码都是在进程中运行。 unix 中的进程创建是通过内核系统调用 fork() 实现的。当一个进程产生一个 fork 请求时,操作系统执行以下功能: 为新进程在进程表中分配一个空项 为子进程赋一个唯一的进程标识符 为一个父进程上下文的逻辑副本,不包括共享内存区 增加父进程拥有的所有文件的计数器,以表示有一个另外的进程现在也用户这些文件。 把子进程置为就绪态 向父进程返回子进程的进程号;对子进程返回0。 所有这些操作都在父进程的内核态下完成。 进程皆有标识 在系统中运行的所有进程都有一个唯一的进程标识符,称为 pid。 pid 并不传达关于进程本身的任何信息,仅仅是一个数字标识 在 python 中查看当前进程 pid 可以使用 getpid() 方法。 >>> import os >>> print os.getpid() 26164 在实际应用中,pid 可以加入都日志信息中,这样当多个进程向同一个文件写入日志的时候,就可以知道哪一行是由哪个进程写入的。 进程皆有父 系统中运行的每一个进程都有对应的父进程。每个进程都知道它父进程的标识符(ppid)。 在 python 中查看当前进程 pid 可以使用 getppid() 方法。 >>> import os >>> print os.getpid() 26164 >>> print os.getppid() 26125 进程皆有文件描述符 在 Unix 中,一切都是文件。 ...

2018-03-25 · 3 min · 518 words

操作系统线程描述

这是操作系统进程系列文章第三篇-操作系统线程描述 文章是《操作系统-精髓与设计原理》学习笔记 线程(thread) 什么是线程 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 关于进程的两个概念: 资源所有权:一个进程包括一个存放进程映像的虚拟地址空间(进程映像是程序、数据、栈和进程控制块中定义的属性的集合)。一个进程总是拥有对资源的控制或所有权,这些资源包括内存、I/O 通道,I/O 设备和文件。 调度/执行:一个进程沿着通过一个或多个程序的一条执行路径执行,其执行过程可能与其他进程的执行过程交替执行。一个进程具有一个执行状态和一个分片的优先级,并且是一个可被操作系统调度和分配的实体。 这两个概念是独立的,操作系统可以独立的处理。 现代操作系统通常把分派单位称为线程(或轻量级进程),拥有资源所有权的单位称为进程。 多线程 多线程是指操作系统在单个进程内支持多个并发执行路径的能力。每个进程中只有一个线程在执行的方法称为单线程方法。进程支持多个线程的情况被称作多线程。 在多线程环境中,进程被定义成资源分配的单位和一个被保护的单位,与进程相关联的有: 存放进程映像的虚拟地址空间 受保护的对处理器、其他进程、文件和 I/O 资源的访问 在一个进程中,可能有一个或多个线程,每个线程有: 线程的执行状态(运行,就绪) 在未运行时保存的线程上下文 一个执行栈 用于每个线程局部变量的静态存储空间 与进程内的其他线程共享的对进程的内存和资源的访问 进程 VS 线程 下图说明了进程和线程的区别: 在单线程模型中,进程的标出包括他的进程控制块和用户地址空间,以及在进程执行中管理调用/返回 行为的用户栈和内核栈。当进程被控制时,处理器寄存器被该进程锁控制;当进程不运行时,这些处理器寄存器的内容被保存。 在多线程环境中,进程仍然只有一个与之关联的进程控制块和用户地址空间。但是每个线程都有一个独立的栈,还有独立的控制块用于包含寄存器值、优先级和其他与线程相关的状态信息。 进程中的所有线程共享该进程的状态和资源,它们驻留在同一块地址空间中,并且可以访问到相同的数据。当一个线程改变了内存中的一个数据项时,其他线程在访问这一数据项时能够看到变化后的结果。 线程的优点 在一个已有的进程中创建一个新的线程比创建一个全新的进程所需时间要少的多。 终止一个线程比终止一个进程花费的时间少 同一个进程内线程间切换比进程间切换花费的时间要少。 线程提高了不同的执行程序间通信的效率。(大多数操作系统中,独立进程间的通信需要内核的介入,由于同一进程中的线程共享内存和文件,它们间的通信无需调用内核) 线程状态 和进程一样,线程的关键状态有运行态、就绪态和阻塞态。挂起是进程级别的概念,一个进程被换出,它的所有线程都被换出。 有4个与线程状态改变相关的操作: 派生:当派生一个新进程时,同时也为改进程派生出一个线程。进程中的线程也可以在同一个进程中派生另一个线程,新的线程拥有自己的寄存器上下文和栈空间,且被放置在就绪队列中。 阻塞:当线程需要等待一个事件时,将被阻塞,此时处理器转而执行另一个就绪线程(可能是同一进程,也可能是不同进程) 解除阻塞:当阻塞一个线程的事件发生时,该线程被转移到就绪队列中 结束:当一个线程完成时,其寄存器上下文和栈都被释放。 用户级线程和内核级线程 线程的实现可以分为两大类:用户级线程(User-Level Thread ULT)和内核级线程(Kernel-Level Thread KLT)。 在用户级线程和内核级线程使用时,通常有以下三种模式: 在一个纯粹的用户级线程程序中,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。 使用用户级线程的优点: 线程切换不需要内核态特权,因此,进程不需要为了线程管理而切换到内核态,这节省了两次状态转换(从用户态到内核态,再从内核态返回用户态)的开销。 调度可以是用户程序相关的。(可以为特定的应用使用特定的调度算法) 用户级线程可以在任何操作系统中运行,不需要对底层内核进行修改以支持用户级线程。 使用用户级线程的缺点: 许多系统调用会被阻塞。因此当用户级线程执行一个系统调用时,不仅这个线程会被阻塞,进程中所有线程都会被阻塞。 不能使用多个处理器。内核一次只把一个进程分配给一个处理器,因此一个进程中只有一个线程可以执行。 解决这两个问题有两种方式: 使用多进程代替多线程,但这样消除了多线程的优势 使用 jacketing 技术。把一个产生阻塞的系统调用转换成一个非阻塞的系统调用。 在一个纯粹的内合辑线程程序中,有关线程管理的所有工作都由内核完成。内核为进程及其内部的每个线程维护上下文信息。调度由内核基于线程完成。 使用内核级线程客服了用户级线程的两个基本缺陷。首先内核可以把同一个进程的多个线程调度到多个处理器;其次一个进程中的线程被阻塞,内核可以调度同一个进程的另一个线程。 ...

2018-03-24 · 1 min · 114 words