首先理解什么是并发?并发是两个或两个以上的执行实例(进程或线程),在单个CPU上交替执行,A开始执行,由于异常事件的发生
CPU暂停A执行,开始执行B。B由于异常事件也被暂停执行,CPU重新开始执行A。两个执行实例交替执行时在时间上是有交叉的,这种情况称为并发。
他们的执行过程在逻辑上是并发的,但是在物理上依然是串行的。并行是某个时刻有多个执行实例在CPU上执行,并行一般是在多核CPU或者多处理器发生。
现代操作系统提供了3种并发编程的方式:进程级并发,IO多路复用技术,和线程级并发。
基于进程级并发
使用进程并发的常见场景是,一个程序用来处理某类事件A,每当A发生,父进程就创建一个子进程来处理。
进程并发的优点是父子进程可以共享fork之前已经打开文件描述符,但是缺点是由于不共享地址空间,
进程控制(创建销毁)和进程间通信的开销很高。
基于IO多路复用并发
一个服务器程序需要根据用户输入进行响应。在操作系统层面,这个程序需要监听连接和输入两个IO事件。
基于操作系统提供的IO多路复用技术,利用select函数将进程挂起,当事件发生时才将控制权交给应用程序,处理事件。
操作系统提供了select, poll, epoll 等技术来支持多路复用。
这种方式的优点是程序运行在单进程内,每个逻辑流都能访问进程的地址空间,可以很方便的共享数据;同时减少了切换进程等无谓的开销。
但是与进程并发比较,这种方式编码实现上较复杂。
基于线程并发
线程执行模型是这样,操作系统提供了线程,CPU运行的最小单位实际上是某个进程内的某个线程,而同一个进程的线程又可以共享该进程的地址空间的内容,
包括代码数据堆共享库和打开的文件。进程开始执行时是单一线程执行,然后主线程开始创建其他线程,线程开始并发运行(实际上同一时刻还是只要唯一的一个线程运行)。
同一个进程的线程之间是平等关系,组成了一个线程池,大家都能读写相同的数据。
线程这种方式,可以说是前面两种方式的混合,通过代码将不同的逻辑流包装在不同的线程,处理对应的事件,而线程之间可以共享地址空间,又减少进程切换。极大的提高了性能
Posix 线程标准
这是C程序中处理线程的一个标准接口,提供了创建、杀死回收线程等近60个函数。在这里就不在描述接口细节。
多线程程序的共享变量
理想的线程的存储器模型:一组并发线程都运行在进程的上下文,每个线程都有自己的上下文,包括线程ID、栈、栈指针、程序计数器、条件码和寄存器。线程之间共享进程上下文的剩余部分。
实际的模型是,寄存器的值是被保护的,线程不能访问其他线程的寄存器,但是线程栈被保存在进程的虚拟地址空间的栈区域,对其他线程来说是可访问的。
有关于变量的几个概念:
全局变量:定义在函数之外的变量。虚拟存储器上只有一个变量实例,任何进程都可访问。
本地自动变量:定义在函数内,没有被声明为static的变量。运行时每个线程都有自己的本地自动变量的实例,相互隔离。
本地静态变量:定义在函数内部,声明为static属性。同全局变量。
共享变量对多线程的程序很方便,线程之间对共享数据可同步访问,但是引入了同步错误。多个线程在对同一个共享变量操作时,由于线程进入临界区的顺序不一致,无法得到一个期望的运行结果。
为了解决这一问题,需要对共享变量互斥访问。信号量PV操作的提出解决了这一问题。信号量也能解决同步问题。
除了生产者消费者同步问题和临界区互斥问题,还有其他的线程并发问题。
线程安全:
简单来说,一个函数是线程安全,意味着多个并发线程执行这个函数,函数会一直产生正确的结果。以下几类函数就不是线程安全的。有以下4类非线程安全的函数:
1.不保护共享变量的函数
2.函数执行依赖于被修改的全局对象的函数
3.返回指向静态变量指针的函数(在使用时,指针指向的地址的值被修改)
4调用线程不安全函数的函数。
可重入函数
当他们被调用时不会引用任何共享数据。是线程安全函数的真子集。因为不引用任何共享数据,自然是线程安全的。如何判断函数是否是可重入函数呢?如果函数参数都是值传递(没有指针),并且所有数据引用都是本地自动变量,函数就是显示可重入的;而如果函数参数允许传递指针,则是隐式可重入的。因为这依赖于调用函数的代码是否传递了共享数据的指针。
一些已知库函数的使用
大多数Unix库函数都是线程安全,但是有一部分不是。这部分非线程安全的函数,除了rand和strtok,大多数是第三类,调用时需要加锁-拷贝。加锁-拷贝对于繁华复杂结构指针的函数,深层次拷贝比较消耗资源。