并发的复杂性


本文谈到的并发是指单程序、单节点并发,区别于并发系统,并发系统的一个更加流行的词是分布式系统,并发系统更有可能是并行的,因为其中的多个程序一般可以同时在不同的硬件环境上运行。分布式系统可以看我另一篇文章《微服务架构的复杂性》。 并发指的是多个任务几乎被同时发起运行,但是在同一时刻这些任务不一定都处于运行状态,这取决于 CPU 核心或者 CPU 数量。并行指的是在同一时刻可以有多个任务真正地同时运行。并行运行的必要条件是多 CPU 核心或者多 CPU 的计算环境。 在功能开发中,非并发程序往往未能充分利用服务器的性能,为用户提供服务基本都是排队处理。而并发程序有时可以代替集群,其性能提高对整个系统是至关重要的作用,如果我们将单个节点的性能提高 30%,或者甚至超过 100%,那么我们可以节省多少台机器呢?不过同时并发却带来了编程的复杂性。不同程序单元之间的远程过程调用可以参考另一篇文章《go 网络编程》。Go 语言的特点是通过内部调度可以最大限度地利用单机的计算能力。然而在分布式计算方面,它本身其实并没有提供什么现成的东西,还需要使用一些第三方的框架或工具,或者自己编写和搭建。

1   为什么并发很难?

并发的困难在于通信,通信有两个要保证:

本文主要讲解线程间并发,但这里也简单的列举进程间的通信方式:

2   原子性

如果某个东西是原子的,隐含的意思就是它在并发环境中是安全的。 谈论原子性必然要谈“上下文(context)”这个词,上下文的概念很多,有函数层级的上下文(函数栈),有程序界限的上下文(php和redis),有用户程序和系统程序或CPU的上下文(程序的运算和内核的运算),原子性可能在某个上下文中有些东西是原子性的,而在另一个上下文中却不是。在考虑原子性时,经常第一件需要做的事就是定义上下文或范围然后再考虑这操作是否是原子性的。一切都应该遵循这个原则。 比如 go 中 i++ 是由三步组成的,是不可分割的,但是可中断的。

3   互斥量 Mutex

3.1   C 语言的互斥量函数:

pthread_mutex_t mutex; int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutexattr_t * attr); 成功时返回 0,失败时返回其他值。 int pthread_mutex_destroy(pthread_mutex_t * mutex); 成功时返回 0,失败时返回其他值。 int pthread_mutex_lock(pthread_mutex_t * mutex); 成功时返回 0,失败时返回其他值。 int pthread_mutex_unlock(pthread_mutex_t * mutex); 成功时返回 0,失败时返回其他值。

4   条件变量 Conditions

是对互斥量的补充,因为互斥量只有两种状态。条件变量进入阻塞,等待通知。

5   信号量 Semaphore

信号量和互斥量很相似,只是用 0 和 1 (二进制信号量)控制,信号量不能为负数,否则便阻塞。 sem_t sem; int sem_init(sem_t * sem, int pshared, unsigned int value); 成功时返回 0,失败时返回其他值。value 为初始信号量。 int sem_destroy(sem_t * sem); 成功时返回 0,失败时返回其他值。 int sem_wait(sem_t * sem); 成功时返回 0,失败时返回其他值。相当于 lock,信号量 value 值 -1。 int sem_post(sem_t * sem); 成功时返回 0,失败时返回其他值。相当于 unlock,信号量 value 值 +1。

6   并发模式