-
线程=“执行线程”
- 线程允许一个程序同时做很多事
- 每一个线程就像普通非线程程序一样,都是串行执行
- 线程之间共享内存
- 每个线程都包含每个线程的状态,所以它们有
- 程序计数器,寄存器,堆栈,就绪挂起执行等状态
-
线程的好处
- I/O并发
- 客户端并行的向多个服务器发送请求并且等待响应
- 服务器端处理多个客户端请求;每个请求都可能阻塞
- 当等待从磁盘读取客户机X的数据时,处理来自客户端Y的请求
- 多核性能
- 在多个核心上并行执行代码
- 便捷性
- 在后台,每秒检查一次每个worker是否还活着
- I/O并发
-
除了线程有可替代方案吗
-
有:在单线程中编写明确交错活动的代码。通常称其为“事件驱动”
-
保存每个活动的状态表。 eg:每个客户端的请求
-
One "event" loop that:checks for new input for each activity (e.g. arrival of reply from server),does the next step for each activity,updates state.
-
事件驱动可以实现I/O并发,并且消除了线程的成本(可观),但无法获得多核的速度,并且编程很麻烦
-
-
编写线程的挑战
- 安全的共享数据
- 如果两个线程同时执行n=n+1或者一个线程读取数据,另一个线程递增此数据。这就是所谓的竞争——通常是一个常见的错误,
- 可以通过使用锁来避免
- 可以刻意避免共享可变数据
- 如果两个线程同时执行n=n+1或者一个线程读取数据,另一个线程递增此数据。这就是所谓的竞争——通常是一个常见的错误,
- 线程之间的协调
- 一个线程在生成数据,另一个线程在消费数据(消费者如何等待(并释放CPU),生产者如何唤醒消费者)
- 使用GO中的channels
- sync.Cond or sync.WaitGroup
- 一个线程在生成数据,另一个线程在消费数据(消费者如何等待(并释放CPU),生产者如何唤醒消费者)
- 死锁
- 通过锁和通信进行循环(如RPC或者Go channels)
- 安全的共享数据
-
最简单的故障处理方案:“尽最大努力交付”的RPC
- Call() 等待响应一段时间
- 如果没有回应重新发送请求
- 这样做几次
- 然后放弃并返回错误
-
“尽最大努力交付”对应用程序来说如何?
- 一种特别糟糕的情况
- 客户端执行:
- put(“k”,10);
- put(“k”,20);
- 均成功(但网络延迟)
- 当get(“k”)
- [diagram, timeout, re-send, original arrives late]
- 客户端执行:
- 一种特别糟糕的情况
-
什么时候用“尽最大努力交付”
- 只读操作
- 重复执行无任何反作用的操作
- eg:DB检查记录是否已被插入
-
更好的RPC行为:“最多执行一次的”RPC
-
理念:如果没有resp,客户端重新发送请求
- 服务器端的RPC代码检测重复请求
- 返回之前的回复,而不需要重新处理
-
如何检测重复请求
-
客户端每次请求时包含唯一性索引 ID(XID),再次发送时使用相同的XID
-
服务器端:
-
server:if seen[xid]:r = old[xid]elser = handler()old[xid] = rseen[xid] = true
-
-
-
-
如何避免庞大的seen[xid]表
- 想法:
- 每个客户端有一个唯一性ID(也许是一个大随机数),为每一个RPC分配递增的序列号
- 客户端在每次RPC中包括“已看到所有回复<=X”
- 就类似于TCP序列号和ACKs,或者只允许客户端同时有一个未完成的RPC
- 这样服务器就能保持O(客户端数),而不是O(XID)
- 想法:
-
服务器最终必须丢弃旧RPC或者旧客户端的信息,何时丢弃是安全的,当原始请求仍在执行时,如何处理重复请求
- 为每一个正在执行的RPC设置“待处理”标志;等待或者忽略
-
当线程并行执行的时候,如果内核数量少于可运行的线程数,运行时将抢先在线程之间分配内核
-
单线程和多线程在程序内存地址空间的区别
-
- 如果两个线程在单核CPU上运行,当从运行一个线程 (T1) 切换到运行另一个线程 (T2) 时,必须进行上下文切换。线程之间的上下文切换与进程之间的上下文切换非常相似,因为在运行T2之前必须保存T1的寄存器状态并恢复T2的寄存器状态。对于进程,我们将状态保存到进程控制块(PCB);现在,我们需要一个或多个线程控制块(TCB)来存储进程的每个线程的状态。与进程相比,我们在线程之间执行的上下文切换有一个主要区别:地址空间保持不变(即不需要切换我们正在使用的页表)。
- 线程和进程的另一个主要区别是堆栈,每个线程都有一个堆栈。假设我们有一个多线程进程,其中有两个线程;生成的地址空间看起来有所不同(图 26.1,右)。
- 以前,堆栈和堆可以独立增长,只有当地址空间空间不足时才会出现问题。在这里,我们不再有这么好的情况了。幸运的是,这通常是可以的,因为堆栈通常不必很大。
-
协程与线程的区别在于协程是完全在应用程序内 (低特权运行级) 实现的,不需要操作系统的支持,占用的资源通常也比操作系统线程更小一些。协程可以随时切换执行流的特性,用于实现状态机、actor model, goroutine 等。