第一章——线程的介绍

简介: 1 什么是线程 线程,有时被称为轻量级进程,是程序执行的最小单元。一个标准的线程由线程ID、程序计数器(pc)、一组寄存器和堆栈组成。通常,一个进程由多个线程组成,每个线程之间共享进程的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开的文件描述符和信号)。

1 什么是线程

线程,有时被称为轻量级进程,是程序执行的最小单元。一个标准的线程由线程ID、程序计数器(pc)、一组寄存器和堆栈组成。通常,一个进程由多个线程组成,每个线程之间共享进程的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开的文件描述符和信号)。如下图所示:

 

2 线程的访问权限

线程的访问非常自由,它可以访问进程内存里的所有数据,同时线程也拥有自己IDE私有存储空间,包括以下几方面:

1)栈

2)线程局部存储(TLS)。

3)寄存器(包括PC寄存器)

 

 

3 线程调度和优先级

在单处理器对应多线程的情况下,并发是一种模拟出来的。操作系统通过让多个线程轮流使用CPU,这样每个线程就“看起来”在同时执行。

在线程调度中,线程至少有三种状态,分别是:

1)运行:此时线程获得CPU正在执行

2)就绪:此时线程只有获得CPU就可以立刻执行

3)等待:此时线程正在等待某一事件发送,无法执行。

线程转换图:

4 Linux多线程

Linux对多线程的支持颇为贫乏,事实上,在Linux内核中并不存在真正意义上的线程概念。Linux将所有的执行实体(无论是线程还是进程)都称为任务,每一个任务类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。

 

fork函数产生一个和当前进程完全一样的新进程,并和当前进程一样从fork函数里返回。

fork产生新任务的速度非常快,因为fork并不复制原任务的内存空间(这里指的是物理内存,父子进程的虚拟地址空间的独立的),而是和原任务一起共享一个写时复制(COW)的内存空间。所谓写时复制,指的是两个任务可以同时自由地读取内存,但任意一任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其他的任务使用。

fork只能够产生本任务的镜像,如果要启动新任务,则使用exec。exec可以用新的可执行映像替换当前的可执行映像,因此在fork产生了一个新任务后,新任务可以exec来执行新的可执行文件。fork和exec都只用于产生新任务,而如果要产生新线程,则可以使用clone。

5 线程安全

多线程程序处于一个多变的环境中,可访问的全局变量和堆数据随时都可能被其他的线程改变。因此多线程程序在并发时数据的一致性变得非常重要。

5.1 竞争和原子操作

多个线程同时访问一个共享数据,可能造成错误的结果:

例如:

 

在许多体系结构上,++i的实现会如下:

1)读取i到某个寄存器X

2)X++

3)将X的内存存储回i

由于线程1和线程2的并发执行,因此两个线程的执行序列可能如下:

 

从程序的逻辑看,正确的结果应该是i为0.但是由于执行的序列问题,可能出现的结果有0,1,2。可见,两个线程同时操作一个共享数据会出现意想不到的结果。

很明显,这里出现错误的原因主要在于自增(++)操作被操作系统编译为汇编代码之后不止一条指令,因此在多线程环境下就可能出现执行了一半而被调度系统打断,去执行其他的代码。如果单条指令是原子的,则执行就不会被打断。问题是,尽管原子操作非常方便,但是它仅适用于比较简单的场合。

5.2 同步和锁

为了避免多个线程同时读写一个数据而出现不可预料的结果,我们需要将各种线程对同一数据的访问同步。所谓同步,即是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。

同步的最常见方法是加锁。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,访问完后释放锁。

二元信号量是最简单的一种锁,它只有两种状态:占用和非占用。它适合只能被唯一一个线程访问的资源。

对于允许多个线程并发访问的资源,使用多元信号量。一个初始值为N的信号量允许N个线程并发访问。

互斥量和二元信号量类似。

临界区是一段访问临界资源的代码。临界区和互斥量和信号量的区别在于,互斥量和信号量在系统的任何进程都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用仅限于同一进程内的不同线程之间的同步,不能用于进程的同步。

读写锁分为共享的和独占的。

 

条件变量,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生后,所有线程可以一起恢复。

6 可重入与线程安全

一个函数要成为可重入的,必须具有以下几个特点:

1)不使用任何(局部)静态或全局的非const变量

2)不返回任何(局部)静态或全局的非const变量的指针

3)仅依赖于调用方提供的参数

4)不依赖于单个资源的锁(mutex等)

5)不调用任何不可重入的函数

7 过度优化

有时候过度优化也会造成线程安全问题。

例如:

 

由于有锁的保护,x++的行为不会被并发所破坏,那么x似乎必然为2.然而,如果编译器为了提高x的访问速度,把x放入了某个寄存器中,那么我们知道不同线程的寄存器是各自独立的,此时就出现线程安全问题,例如:

 

可见,现在即使加锁也不能保证结果正确。

我们可以使用volatile关键字试图阻止过度优化。volatile可以阻止两件事情:

1)阻止编译器为了提高速度将一个变量缓存在寄存器内而不写回。

2)阻止编译器调整操作volatile变量的指令。

 

相关文章
|
存储 安全 API
[笔记]Windows核心编程《五》线程基础
[笔记]Windows核心编程《五》线程基础
线程基础知识点
本章讲解了线程的相关知识
43 0
|
设计模式 Java 调度
JAVA线程入门简介
JAVA线程入门简介
185 0
|
缓存 安全 Java
线程基础知识总结
@[toc] 1. 认识线程(Thread) 1.1 概念 1.2 创建线程 1.2.1 方法1 继承Thread类 1.2.2 方法2 实现Runnable接口 1.2.3 实现 Callable 接口,使用 FutureTask 接收线程返回值 1.2.4 对比上面两种方法 2. Thread类及常见方法 2.1 Thread的常见构造方法 2.2 Thread的几个常见属性 2.3 启动一个线程-start() 2.4 中断一个线程 2.5 等待一个线程-join() 2.6 获取当前线程的引用 2.7 休眠当前线程 3. 线程的状态 3.1 线程的所有状态 3.2 线程各状态之间的转移
60 0
|
算法 安全 Unix
|
缓存 算法 Java
多线程:第一章:我(线程)这一生
多线程:第一章:我(线程)这一生
133 0
多线程:第一章:我(线程)这一生
|
Linux
linux系统编程(十一)线程同步(完结)(下)
linux系统编程(十一)线程同步(完结)
197 0
linux系统编程(十一)线程同步(完结)(下)
|
Linux 调度 数据库
linux系统编程(十一)线程同步(完结)(上)
linux系统编程(十一)线程同步(完结)
174 0
linux系统编程(十一)线程同步(完结)(上)
|
存储 缓存 监控
Java并发编程系列之二线程基础
上篇文章对并发的理论基础进行了回顾,主要是为什么使用多线程、多线程会引发什么问题及引发的原因,和怎么使用Java中的多线程去解决这些问题。
Java并发编程系列之二线程基础