1. 线程和进程的区别

名称 执行点 地址空间 状态保存位置
进程 process 一个进程有多个线程,多个执行点 一个进程一个地址空间 Process Control Block 进程控制块
线程 thread 一个执行点 多个线程共享一个地址空间 Thread Control Block 线程控制块

2. Concurrency 并发

  • critcal section 临界区:访问共享资源的一段代码;
  • Race condition 竞态条件:多个执行线程大致同时进入临界区时,都试图更新共享资源;
  • Indeterminate 不确定性:出现由一个或多个竞态条件组成,出现的输出因运行而异;
  • Mutual exclusion 互斥:互斥原语,可以保证只有一个线程进入临界区,避免出现竞态;

3. Lock 锁

一个朴素的锁代码示例如下,是不安全的,所以需要硬件的支持;

typedef struct lock_t { int flag; } lock_t;

void init(lock_t *mutex) {
// 0 -> lock is available, 1 -> held
mutex->flag = 0;
} //这里的lock方法时不安全的,因为多个线程可能同时执行while条件判断,都是false,所以这个lock时不安全的
void lock(lock_t *mutex) {
while (mutex->flag == 1) // TEST the flag
; // spin-wait (do nothing)
mutex->flag = 1; // now SET it!
} void unlock(lock_t *mutex) {
mutex->flag = 0;
}
3.1 test-and-set instruction

TestAndSet逻辑如下:

//该指令是硬件提供的,执行过程时原子的
int TestAndSet(int *old_ptr, int new) {
int old = *old_ptr; // fetch old value at old_ptr
*old_ptr = new; // store 'new' into old_ptr
return old; // return the old value
}

通过该指令实现lock如下:

typedef struct lock_t {
int flag;
} lock_t; void init(lock_t *lock) {
// 0 indicates that lock is available, 1 that it is held
lock->flag = 0;
} void lock(lock_t *lock) {
//当flag为0时上锁成功,为1时,说明锁已经被占用,自旋等待
while (TestAndSet(&lock->flag, 1) == 1)
; // spin-wait (do nothing)
} void unlock(lock_t *lock) {
lock->flag = 0;
}
3.2 compare-and-exchange instruction

和compare-and-set instruction相比多了expected参数,即old value和expected相等才会更新;其他没有区别,所以只需要对lock()函数轻微修改即可;


int CompareAndSwap(int *ptr, int expected, int new) {
int actual = *ptr;
if (actual == expected)
*ptr = new;
return actual;
}
void lock(lock_t *lock) {
while (CompareAndSwap(&lock->flag, 0, 1) == 1)
; // spin
}
3.3 load-linked and store conditioinal instruction

load-linked指令用于加载指定地址的值到register中,store-conditional用来判断加载后,内存中的值是否有被修改过,没有就修改并返回1,有就直接返回0

//将值从内存中加载到寄存器中
int LoadLinked(int *ptr) {
return *ptr;
} int StoreConditional(int *ptr, int value) {
//判断加载到寄存器后,内存中该值是否有被修改过
if (no one has updated *ptr since the LoadLinked to this address) {
*ptr = value;
return 1; // success!
} else {
return 0; // failed to update
}
}

其对应的加锁解锁代码如下:

void lock(lock_t *lock) {
while (1) {
//当flag为1时,说明已经被其他线程占用
while (LoadLinked(&lock->flag) == 1)
; // spin until it's zero
//校验是否有被修改,并修改
if (StoreConditional(&lock->flag, 1) == 1)
return; // if set-it-to-1 was a success: all done
// otherwise: try it all over again
}
} void unlock(lock_t *lock) {
lock->flag = 0;
}
3.4 fetch-and-add instruction

获取并增加指令,能原子地返回该地址的旧值,并让该值自增一;

int FetchAndAdd(int *ptr) {
int old = *ptr;
*ptr = old + 1;
return old;
}

基于该指令的lock代码有些特殊,通过两个变量,每个线程加载自己的ticket,通过判断turn变量是否是自己的ticket来判断,当前自生是否可以运行;

typedef struct lock_t {
int ticket;
int turn;
} lock_t; void lock_init(lock_t *lock) {
lock->ticket = 0;
lock->turn = 0;
} void lock(lock_t *lock) {
//每个线程线性获取自己唯一ticket即myturn
int myturn = FetchAndAdd(&lock->ticket);
//只有myturn和lock.turn相等时才会跳出自旋
while (lock->turn != myturn)
; // spin
} void unlock(lock_t *lock) {
//执行完毕后,递增lock.turn,让下一个线程执行
FetchAndAdd(&lock->turn);
}

可以看出基于ticket的lock,每个线程首先都会拿到自己的ticket,然后根据ticket的先后顺序执行,所以是一个 **公平锁**

4. Using Queues: Sleeping Instead Of Spinning 用队列,来实现休眠替代自旋

前面的几个lock都是通过spinning 自旋来暂停线程,虽然可以在自旋中添加yield()来让出CPU,但是每次自旋还是会占用CPU周期,造成浪费,而且也无法避免因为CPU进行线程调度出现线程饿死的情况;

    为了解决这一点,必须显式地控制锁的释放,谁能抢到锁。

    Solaris系统提供两个调用,park()能够让调用线程自身休眠,unpark(threadID)则会唤醒threadID标识的线程。通过queue以及这两个指令来实现:

typedef struct lock_t {
int flag;//是否有线程占用锁的标记
int guard;//是否有线程在执行lock() unlock()函数的标记
queue_t *q;//线程队列
} lock_t; void lock_init(lock_t *m) {
m->flag = 0;
m->guard = 0;
queue_init(m->q);
} void lock(lock_t *m) {
//这里的自旋是为了保证同一时间只有一个线程执行lock()和unlock()方法,所以lock() unlock()方法结束时都会将guard置为0
//因为这里lock()和unlock()函数都会执行很快,所以自旋不会消耗很多CPU周期
while (TestAndSet(&m->guard, 1) == 1)
; //acquire guard lock by spinning
if (m->flag == 0) {
//flag为0 锁是空闲的
m->flag = 1; // lock is acquired
m->guard = 0;
} else {
//锁已经被别的线程占用了,将当前线程id加入队列,并休眠当前线程
queue_add(m->q, gettid());
m->guard = 0;
park();
}
} void unlock(lock_t *m) {
while (TestAndSet(&m->guard, 1) == 1)
; //acquire guard lock by spinning
if (queue_empty(m->q))
m->flag = 0; // let go of lock; no one wants it
else
//如果有别的线程还在排队,那么直接激活队首的线程,flag没必要置为0
unpark(queue_remove(m->q)); // hold lock (for next thread!)
m->guard = 0;
}

只是这里的lock()有一点小问题

                queue_add(m->q, gettid());
m->guard = 0;
park();

m->guard = 0; 之后,可能cpu被调度到另一个线程执行unlock()函数,结果执行unpark发现并没有休眠线程,这被称为唤醒/等待竞争(wakeup/waiting race);

Solaris系统额外提供setpark()调用来解决这个问题,setpark()申明线程自身马上要park,如果刚好另一个线程被调度,并执行了unpark,那么后续的park调用会直接返回,而不是一直睡眠;所以lock()代码可以做如下修改;

                queue_add(m->q, gettid());
setpark(); // new code
m->guard = 0;
park();

Linux提供的futex,也类似Solaris的接口,但提供了更多的内核功能。具体来说每个futex都关联一个特定的物理内存位置,也有一个事先建好的内核队列。调用者通过futex调用来睡眠或者唤醒;

Linux 采用的是一种古老的锁方案,two-phase lock 两阶段锁,第一阶段会先自旋一段时间,如果第一阶段没有获取到锁,第二阶段会让线程睡眠,直到锁可用。

操作系统 Concurrency 并发的更多相关文章

  1. python3全栈开发-补充UDP的套接字、操作系统、并发的理论基础

    一.基于UDP的套接字 udp套接字简单示例 import socket ip_port=('1.1.1.1',8181) BUFSIZE=1024 udp_server_client=socket. ...

  2. python 并发编程 操作系统 进程 并发.并行 及 同步/异步,阻塞/非阻塞

    操作系统: 多道技术背景: 提高工作效率(充分利用IO阻塞的时间) 同时执行多个任务 多道技术 空间复用:充分的利用内存空间 时间复用:充分利用IO阻塞时间 分时系统: 并发:提高了程序的工作效率 两 ...

  3. 线程安全 Thread Safety Problem scala concurrency 并发

    小结: 1.基于java并发模型 Scala concurrency is built on top of the Java concurrency model. 2. 将每个请求放入一个新的线程 T ...

  4. day30_8.9 操作系统与并发编程

    一.操作系统相关 1.手工操作 1946年第一台计算机诞生--20世纪50年代中期,计算机工作还在采用手工操作方式.此时还没有操作系统的概念. 这时候的计算机是由人为将穿孔的纸带装入输入机,控制台获取 ...

  5. 【windows 操作系统】并发

    并发 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行.其中两种并发关系分别是同步和互斥 微观角度 所有的并发处理都有排队等候,唤醒,执行等 ...

  6. Concurrency并发性

    今天看了有关性能的文章,性能也是客户所看重的. 文章推荐看了软件编程并发性. 就按书上敲了网址看:http://www.gotw.ca/publications/concurrency-ddj.htm ...

  7. java ee Concurrency 并发编程

    https://www.javacodegeeks.com/2014/07/java-ee-concurrency-api-tutorial.html This is a sample chapter ...

  8. Celery-4.1 用户指南: Concurrency (并发)

    简介 Eventlet 的主页对它进行了描述:它是一个python的并发网络库,可以让你更改如何运行你的代码而不是怎么编写代码. 对高可扩展非阻塞IO操作,它使用 epoll或者libevent. C ...

  9. 1.2 操作系统的第二个功能——并发功能 -《zobolの操作系统学习札记》

    1.2 操作系统的第二个功能--并发功能 目录 1.2 操作系统的第二个功能--并发功能 问1:什么是并发功能?并发功能是必要的吗? 问2:并发功能必须要求拥有多核CPU吗? 问3:多核CPU和单核C ...

  10. 操作系统类型&操作系统结构&现代操作系统基本特征

    五大类型操作系统 (1). 批处理操作系统 用户脱机使用计算机 用户提交作业之后直到获得结果之前就不再和计算机打交道. 作业提交的方式可以是直接交给计算中心的管理操作员,也可以是通过远程通讯线路提交. ...

随机推荐

  1. wordpress宕机原因及处理方法

    2020年7月底,查看了网站日志,是wp-cron.php 导致异常. 原来这是WordPress定时任务,禁用即可. 在wp-config.php添加 /* 禁用定时任务 wp-cron */ de ...

  2. ROS librviz库

    1.可视化管理类:rviz::VisualizationManager The VisualizationManager class is the central manager class of r ...

  3. the default discovery settings are unsuitable for production use at least one of...的解决办法

    解决办法 elasticsearch.yml加上 discovery.type: single-node

  4. Python读取保存图像文件

    Python处理图像数据时通常需要对图像文件进行读取.保存等操作,因此将现有的方法归纳了一下. 1. PIL 依赖包:Pillow 安装:pip install Pillow 源码: 1 import ...

  5. Mysql数据库基础第七章:流程控制结构

    Mysql数据库基础系列 软件下载地址 提取码:7v7u 数据下载地址 提取码:e6p9 mysql数据库基础第一章:(一)数据库基本概念 mysql数据库基础第一章:(二)mysql环境搭建 mys ...

  6. PHP面向对象(二)

    构造函数 PHP 5 允行开发者在一个类中定义一个方法作为构造函数.具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作. 代码如下: <?php//类的 ...

  7. C++容器map、unordered_map、set、unordered_set的区别

    1.map: 底层由红黑树实现. Key在红黑树中有序排列,对红黑树进行中序遍历即可得到Key从小到大的排序序列. 使用map可在O(1)的时间复杂度下快速查找到Key. 2.unordered_ma ...

  8. Tomcat配置中的java.lang.IllegalStateException: No output folder问题

    最近运行Tomcat7.0时总会报错:Tomcat安装文件夹下的某个文件拒绝访问. localhost:8080 java.lang.IllegalStateException: No output ...

  9. Win10官方1909版本无法打开windows安全中心中病毒和威胁防护的实时保护解决方案。

    进入手痒难耐,将电脑操作系统重新安装了win10 专业工作站版 1909版,但是装完软件激活后,发现windows安全中心的"病毒和威胁防护"中的所有项目都是关闭的,试着重新安装也 ...

  10. 软件开发流程-路飞项目需求- pip永久换源-虚拟环境-路飞项目前后端创建-包导入-后端项目调整目录

    目录 软件开发流程-路飞项目需求- pip永久换源-虚拟环境-路飞项目前后端创建-包导入-后端项目调整目录 今日内容概要 今日内容详细 1 软件开发流程 2 路飞项目需求 3 pip永久换源 4 虚拟 ...