码迷,mamicode.com
首页 > 编程语言 > 详细

线程的概念

时间:2018-02-13 17:58:54      阅读:234      评论:0      收藏:0      [点我收藏+]

标签:提高   分享   简单   适用于   依赖   物理   加锁   运行   共享   

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变量的指令。

线程的概念

标签:提高   分享   简单   适用于   依赖   物理   加锁   运行   共享   

原文地址:https://www.cnblogs.com/alantu2018/p/8446857.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!