zoukankan      html  css  js  c++  java
  • 线程的概念

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

  • 相关阅读:
    正则表达式入门(3)
    正则表达式入门(2)
    正则表达式入门
    函数的参数传递
    python常用库之random
    插入排序与归并排序
    浅拷贝与深拷贝
    装饰器学习小程序
    Python的“is”与“==”
    Oracle中断中止exp/imp和expdp/impdp数据库导入导出
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8446857.html
Copyright © 2011-2022 走看看