• 操作系统 Concurrency 并发


    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 两阶段锁,第一阶段会先自旋一段时间,如果第一阶段没有获取到锁,第二阶段会让线程睡眠,直到锁可用。

  • 相关阅读:
    linux 系统管理(2) 文件或目录数量统计
    linux系统管理(1)之 内核编译选项查看
    apt 命令大全
    system命令
    ubuntu 登陆闪回
    网络知识之ipset
    mac 系统配置(一)
    windows下的qt编译器配置
    QT5.14.1+qwt-6.1.4编译
    无法打开源文件QtWidgets/QApplication
  • 原文地址:https://www.cnblogs.com/IC1101/p/16045037.html
Copyright © 2020-2023  润新知