精华内容
下载资源
问答
  • 多线程

    2020-06-29 17:34:36
    2.线程:指进程当中每一个独立的小任务 竞争关系。 二、并发编程的要素 1.原子性: 原子性指的是一个或者个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部不执行。 2.可见性: 可见...

      

    一、进程和线程
    1.进程:指程序中的每一个执行文件  exe  有独立的系统内存和资源。
    2.线程:指进程当中每一个独立的小任务    竞争关系。

    二、并发编程的要素

      1.原子性:

                 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部不执行。

      2.可见性:

                  可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

          实现可见性的方法:

                                 synchronization或者Lock:保证同一个时刻只有一个线程 获取程序所执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

      3.有序性:

                   有序性,即程序执行顺序按照代码的先后顺序来执行。

    三、多线程的优势

      1.发挥多核CPU的优势:

                      多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不            互相干扰。

      2.防止阻塞:

                     从程序运行效率来讲,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致上下文的切换,而降低程序整体的效率。但时单核CPU我们还是要应用多线程,就是为了防止阻塞。

    四、创建线程的方式

     1.继承Thread类创建线程类

     2.通过Runnable接口创建线程类

     3.通过Callable和Future创建线程

    Runnable和Callable的区别

     1.Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
     2.Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
     3.Call方法可以抛出异常,run方法不可以。
     4.运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    五、Java线程具有五中基本状态

     1.新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

     2.就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

     3.运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

     4.阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

                  a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

                  b.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

                  c.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等            待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

      5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

      

     

     

                 

                  

    展开全文
  • 多线程的并发问题?

    2018-06-04 22:13:02
    1、描述 在一个CPU上同时运行个线程时,会存在线程竞争CPU资源的问题,但是有时候一段代码是不允许打断,或是出现死锁的状态。 死锁:个线程出现了锁嵌套,形成资源互相等待的状态,使程序无法继续运行。2...

    1、描述

        在一个CPU上同时运行多个线程时,会存在多个线程竞争CPU资源的问题,但是有时候一段代码是不允许打断,或是出现死锁的状态。

        死锁:多个线程出现了锁嵌套,形成资源互相等待的状态,使程序无法继续运行。

    2、解决或避免死锁状态的方法

        ①引入锁对象-----synchronized(同步代码块)

            在需要遵循原子性代码段的地方使用

                    synchronized(){

                    }

            其中锁对象可以是:共享资源,类的字节码,this

            注意:this作为锁对象时,需要使用同一个Runnable对象启动所有的Thread线程

        ②避免死锁:减少线程数量,统一锁对象,减少锁嵌套

        ③等待唤醒机制:使用等待唤醒机制调节线程执行的顺序,如下图:a1,a2为同一类型线程,有资源竞争关系;c1,c2同样。

                初始状态:

                        

                a1线程运行中:

                        

                a1线程运行时,a2线程也需要运行,但是因为资源不足,需要将a2线程设为等待:

                        

                 c1线程运行

                            

                 c2线程需要运行,但是资源被c1占用,于是到等待线程池中等待c1执行完成:

                            

                 a1线程执行完成,唤醒在等待池中的a2:

                            

                c1线程执行完成,唤醒在等待池中的c2:

                            

                最终,a2线程和c2线程都执行完成,这样一个多线程的就有序进行:

                            

    3、线程的5/7种状态关系图:

                                

    4、sleepwait的区别:     

        sleep在使用的时候需要指定休眠时间,到点自然醒。释放执行权,不释放。是一个静态方法,设计在了Thread

        wait在使用的时候可以指定等待时间,也可以不指定,如果不指定等待时间就需要唤醒。释放执行权,释放。是一个非静态 方法,设计在了Object



        

    展开全文
  • 而在多线程里面,这些步骤可能都会被中途打断,导致痴线意想不到的结果。 假如要想实现改变一个资源,必须满足原子性的特征。 二.synchronize到底是给谁加锁。 静态方法前加入synchronize,由于该类可能未被实例化,...

    在这里插入图片描述
    多线程有挺多东西要注意的:
    一.资源竞争问题
    当多个线程,同时访问一个资源的时候,对资源进行更改,可能就会出问题。
    改变一个资源有三步
    1.读取文件获得副本
    2.更改文件副本
    3.将副本写入内存
    而在多线程里面,这些步骤可能都会被中途打断,导致痴线意想不到的结果。
    假如要想实现改变一个资源,必须满足原子性的特征。
    二.synchronize到底是给谁加锁。
    静态方法前加入synchronize,由于该类可能未被实例化,所以加锁的肯定不是this,而是由一开始就创建的该类的字节码文件.class
    public static synchronize void xxx(){ }
    普通方法前加入synchronize,加锁的对象就是该类的实例化对象。
    public synchronize void xxx(){ }
    三.synchronize代码块对不同东西加锁的问题。
    千万不能在线程的run方法前加,因为你的多线程实例化了很多个Thread,每个Thread都会对应不同的锁,相当于无效。
    synchronize代码最好块就是,你想用什么,你就对什么加锁,避免资源竞争问题。或者new 个object来加锁也行。

    展开全文
  • 线程同步及线程

    2018-01-01 15:32:01
    竞争态条件下,线程对同一竞态资源的抢夺会引发线程安全问题。竞态资源是对线程可见的共享资源,主要包括全局(非const)变量、静态(局部)变量、堆变量、资源文件等。 线程之间的竞争,可能带来一些列问题:...

    1 资源竞争与线程同步

    竞争态条件下,多个线程对同一竞态资源的抢夺会引发线程安全问题。竞态资源是对多个线程可见的共享资源,主要包括全局(非const)变量、静态(局部)变量、堆变量、资源文件等。

    线程之间的竞争,可能带来一些列问题:

    • 线程在操作某个共享资源的过程中被其他线程所打断,时间片耗尽而被迫切换到其他线程
    • 共享资源被其他线程修改后的不到告知,造成线程间数据不一致
    • 由于编译器优化等原因,若干操作指令的执行顺序被打乱,造成结果的不可预期

    1.1 原子操作

    原子操作,即不可分割开的操作;该操作一定是在同一个cpu时间片中完成,这样即使线程被切换,多个线程也不会看到同一块内存中不完整的数据。

    原子表示不可分割的最小单元,具体来说是指在所处尺度空间或者层(layer)中不能观测到更为具体的内部实现与结构。对于计算机程序执行的最小单位是单条指令。我们可以通过参考各种cpu的指令操作手册,用其汇编指令编写原子操作。而这种方式太过于低效。

    某些简单的表达式可以算作现代编程语言的最小执行单元 某些简单的表达式,其实编译之后的得到的汇编指令,不止一条,所以他们并不是真正意义原子的。以加法指令操作实现 x += n为例 ,gcc编译出来的汇编形式上如下:

    ...
    movl 0xc(%ebp), %eax
    addl $n, %eax
    movl %eax, 0xc(%ebp)
    ...
    复制代码

    而将它放在所线程环境之中,显然也是不安全的:

    dispatch_group_t group = dispatch_group_create();
        __block int  i = 1;
    for (int k = 0; k < 300; k++) {
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            ++i;
            dispatch_group_leave(group);
        });
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            --i;
            dispatch_group_leave(group);
        });
    }
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"----result=%d  i=%d",self.pro1,i);
    });
    复制代码

    上述例子中,全局变量i理论上应该最后得到1,而实际上却几率性得到0,-1,2,-2,1。

    为了避免错误,很多操作系统或编译器都提供了一些常用原子化操作的内建函数或API,包括把一些实际是多条指令的常用表达式。上述操作中,将i++/i--,替换为 OSAtomicIncrement32(&i) / OSAtomicDecrement32(&i) ,将得到预期的结果1

    下边列举了不同平台上原子操作API的部分例子

    windows API macOS/iOS API gcc内建函数 作用
    InterlockExchange OSAtomicAdd32 AO_SWAP 原子的交换两个值
    InterlockDecrement OSAtomicDecrement32 AO_DEC 原子的减少一个值
    InterlockIncrement OSAtomicIncrement32 AO_INC 原子的增加一个值
    InterlockXor OSAtomicXor32 AO_XOR 原子的进行异或

    在OC中,属性变量的atomoc修饰符,起到的作用跟上述API相似,编译器会通过锁定机制确保所修饰变量的原子性,而且它是默认情况下添加的。而在实际应用场景中,在操作属性值时一般会包含三步(读取、运算、写入),即便写操作是原子,也不能保证线程安全。而ios中同步锁的开销很大(macOS中没有类似问题),所以一般会加上nonatomic修饰。

    @property (nonatomic,assign)int pro1;
    复制代码

    在实际业务中,通常是给核心业务代码加同步锁,使其整体变为原子的,而不是针对具体的属性读写方法。

    1.2 可重入与线程安全

    函数被重入 一个程序被重入,表示这个函数没有执行完成,由于外部因数或内部调用,又一次进入函数执行。函数被重入分两种情况

    • 多个线程同时执行这个函数
    • 函数自身(可能是经过多层调用之后)调用自身

    可重入 一个函数称为可重入的,表明该函数被重入之后没有产生任何不良后果。 可重入函数具备以下特点:

    • 不使用任何局部(静态)非const变量
    • 不使用任何局部(静态)或全局的非const变量的指针
    • 仅依赖调用方法提供的参数
    • 不依赖任何单个资源提供的锁(互斥锁等)
    • 不调用任何不可重入的函数

    可重入是并发的强力保障,一个可重入函数可以在多线程环境下放心使用。也就是说在处理多线程问题时,我们可以讲程序拆分为若干可重入的函数,而把注意的焦点放在可重入函数之外的地方。

    函数式编程范式中,由于整个系统不需要维护多余数据变量,而是状态流方式。所以可以认为全是由一些可重入的函数组成的。所以函数式编程在高并发编程中有其先天的优势。

    1.3 CPU的过度优化

    1.3.1 乱序优化与内存屏障

    cpu有动态调度机制,在执行过程中可能因为执行效率交换指令的顺序。而一些看似独立的变量实际上是相互影响,这种编译器优化会导致潜在不正确结果。

    面对这种情况我们一般采用内存屏障(memory barrier)。其作用就相当于一个栅栏,迫使处理器来完成位于障碍前面的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。确保一个线程的内存操作总是按照预定的顺序完成。为了使用一个内存屏障,你只要在你代码里面需要的地方简单的调用 OSMemoryBarrier() 函数。

    class A {
        let lock = NSRecursiveLock()
        var _a : A? = nil
        var a : A? {
            lock.lock()
            if _a == nil {
                let temp = A()
                
                OSMemoryBarrier()
                
                _a = temp
            }
            lock.unlock()
            return _a
        }
    }
    复制代码

    值得注意的是,大部分锁类型都合并了内存屏障,来确保在进入临界区之前它前面的加载和存储指令都已经完成。

    1.3.2 寄存器优化与volatile变量

    在某些情况下编译器会把某些变量加载进入寄存器,而如果这些变量对多个线程可见,那么这种优化可能会阻止其他线程发现变量的任何变化,从而带来线程同步问题。

    在变量之前加上关键字volatile可以强制编译器每次使用变量的时候都从内存里面加载。如果一个变量的值随时可能给编译器无法检测的外部源更改,那么你可以把该变量声明为volatile变量。在许多原子性操作API中,大量使用了volatile 标识符修饰。譬如 在系统库中,所有原子性变量都使用了

    <libkern/OSAtomic.h>
    
    int32_t	OSAtomicIncrement32( volatile int32_t *__theValue )
    复制代码

    ##2.线程同步的主要方式--线程锁 线程同步最常用的方法是使用(Lock)。锁是一种非强制机制,每一个线程访问数据或资源之前,首先试图获取(Acquireuytreewq)锁,并在访问结束之后释放(release)。在锁已经被占用时获取锁,线程会等待,直到该锁被释放。

    2.1 互斥锁(Mutex)

    2.1.1 基本概念

    互斥锁 是在很多平台上都比较常用的一种锁。它属于sleep-waiting类型的锁。即当锁处于占用状态时,其他线程会挂起,当锁被释放时,所有等待的线程都将被唤醒,再次对锁进行竞争。在挂起与释放过程中,涉及用户态与内核态之间的context切换,而这种切换是比较消耗性能的。

    互斥锁和二元信号量很相似,唯一不同是只能由获取锁的线程释放而不能假手于人。在某些平台中,他是用二元信号量实现的。关于信号量,我们将在2.3中详细介绍。

    互斥锁可以是多进程共享的,也可以是进程内线程可见的。它可以分为分为普通锁、检错锁、递归锁。让我们通过pthread中的pthread_mutex,来详细了解互斥锁的一些用法及注意事项。

    2.1.2 pthread_mutex

    pthread_mutex 是pthread中的互斥锁,具有跨平台性质。pthread是POSIX线程(POSIX threads)的简称,是线程的POSIX标准(可移植操作系统接口 Portable Operation System Interface)。POSIX是unix的api设计标准,兼容各大主流平台。所以pthread_mutex是比较低层的,可以跨平台的互斥锁实现。

    我们先来看看最常规的调用方式:

    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    block();
    pthread_mutex_unlock(&mutex);
    复制代码

    pthread_mutex可以定义它的作用范围,是多进程共享,还是只是进程内可见。默认是后者

    /**
     PTHREAD_PROCESS_SHARE:该进程与其他进程的同步
     PTHREAD_PROCESS_PRIVATE:同一进程内不同的线程之间的同步
    **/
    pthread_mutexattr_setpshared(&mattr,PTHREAD_PROCESS_PRIVATE);
    复制代码

    pthread_mutex又可分为普通锁、检错锁、递归锁。可以通过属性,实现相应的功能。

    /*
    互斥锁的类型:有以下几个取值空间:
    PTHREAD_MUTEX_NORMAL 0: 普通锁(默认)。不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或未锁定,则将产生不确定的行为。
     
    PTHREAD_MUTEX_ERRORCHECK 1: 检错锁,会提供错误检查。如果某个线程尝试重新锁定的互斥锁已经由该线程锁定,则将返回错误。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
     
    PTHREAD_MUTEX_RECURSIVE 2: 嵌套锁/递归锁,该互斥锁会保留锁定计数这一概念。线程首次成功获取互斥锁时,锁定计数会设置为 1。线程每重新锁定该互斥锁一次,锁定计数就增加 1。线程每解除锁定该互斥锁一次,锁定计数就减小 1。 锁定计数达到 0 时,该互斥锁即可供其他线程获取。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
     
    */
    pthread_mutexattr_settype(&mattr ,PTHREAD_MUTEX_NORMAL);
    复制代码

    pthread_mutex还有一种简便的调用方式,使用的是全局唯一互斥锁。实验表明,该锁是所有属性都是默认的,进程内可见,类型是普通锁

    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    block();
    pthread_mutex_unlock(&mutex);
    复制代码

    同时它还提供了一种非阻塞版本pthread_mutex_trylock。若尝试获取锁时发现互斥锁已经被锁定,或则超出了递归锁定的最大次数,则立即返回,不会挂起。只有在锁未被占用时才能成功加锁。

    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    int res = pthread_mutex_trylock(&mutex);
    if(res == 0){
        block();
        pthread_mutex_unlock(&mutex);
    }else if(res == EBUSY){
        printf("由于 mutex 所指向的互斥锁已锁定,因此无法获取该互斥锁。");
    }else if (res == EAGAIN){
        printf("由于已超出了 mutex 的递归锁定最大次数,因此无法获取该互斥锁。");
    }
    复制代码

    2.1.3 NSLock与NSRecursiveLock

    NSLock是iOS中最常用的一种锁,对应着普通类型的互斥锁。另外一个可递归的子类为NSRecursiveLock; 我们先来看看它的官方文档:

    An NSLock object can be used to mediate access to an application’s global data or to protect a critical section of code, allowing it to run atomically.
    
    Warning
    
    The NSLock class uses POSIX threads to implement its locking behavior. When sending an unlock message to an NSLock object, you must be sure that message is sent from the same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior.
    You should not use this class to implement a recursive lock. Calling the lock method twice on the same thread will lock up your thread permanently. Use the NSRecursiveLock class to implement recursive locks instead.
    
    Unlocking a lock that is not locked is considered a programmer error and should be fixed in your code. The NSLock class reports such errors by printing an error message to the console when they occur.
    复制代码

    从文档中我们可以知道:

    • 其实现是基于phthread的
    • 谁持有谁释放,试图释放由其他线程持有的锁是不合法的
    • 如果用在需要递归嵌套加锁的场景时,需要使用其子类NSRecursiveLock。不是所有情况下都会引发递归调用,而NSLock在性能上要优于NSRecursiveLock。而当我们使用NSLock不小心造成死锁时,可以尝试将其替换为NSRecursiveLock。
    • lock与unlock是一一对应的,如果试图释放一个没有加锁的锁,会发生异常崩溃。而lock始终等不到对应的unlock会进入饥饿状态,让当前线程一直挂起

    2.1.4 @synchronized

    @synchronized(self){
    	// your code hear        
    };
    复制代码

    @synchronized在运行时会在代码块前面加上objc_sync_enter,代码块最后插入objc_sync_exit。下面是这两个函数声明文件。

    /** 
     * Begin synchronizing on 'obj'.  
     * Allocates recursive pthread_mutex associated with 'obj' if needed.
     * 
     * @param obj The object to begin synchronizing on.
     * 
     * @return OBJC_SYNC_SUCCESS once lock is acquired.  
     */
    OBJC_EXPORT int
    objc_sync_enter(id _Nonnull obj)
        OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
    
    /** 
     * End synchronizing on 'obj'. 
     * 
     * @param obj The object to end synchronizing on.
     * 
     * @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
     */
    OBJC_EXPORT int
    objc_sync_exit(id _Nonnull obj)
        OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);
    复制代码

    这两个函数位于runtime/objc-sync.mm中,而且是开源的,我们可以 这里看到具体的源码实现。源码中 当你调用 objc_sync_enter(obj) 时,它用 obj 内存地址的哈希值查找合适的 SyncData,然后将其上锁。当你调用 objc_sync_exit(obj) 时,它查找合适的 SyncData 并将其解锁。 SyncData其实是数据链表的一个节点,其数据结构如下:

    typedef struct SyncData {
        struct SyncData* nextData;
        id               object;
        int              threadCount;  // number of THREADS using this block
        recursive_mutex_t        mutex;
    } SyncData;
    
    typedef struct {
        SyncData *data;
        unsigned int lockCount;  // number of times THIS THREAD locked this block
    } SyncCacheItem;
    
    typedef struct SyncCache {
        unsigned int allocated;
        unsigned int used;
        SyncCacheItem list[0];
    } SyncCache;
    复制代码

    加锁代码如下:

    / Begin synchronizing on 'obj'. 
    // Allocates recursive mutex associated with 'obj' if needed.
    // Returns OBJC_SYNC_SUCCESS once lock is acquired.  
    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
    
        if (obj) {
            SyncData* data = id2data(obj, ACQUIRE);
            require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
    	
            result = recursive_mutex_lock(&data->mutex);
            require_noerr_string(result, done, "mutex_lock failed");
        } else {
            // @synchronized(nil) does nothing
            if (DebugNilSync) {
                _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
            }
            objc_sync_nil();
        }
    
    done: 
        return result;
    }
    // End synchronizing on 'obj'. 
    // Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
    int objc_sync_exit(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
        
        if (obj) {
            SyncData* data = id2data(obj, RELEASE); 
            require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
            
            result = recursive_mutex_unlock(&data->mutex);
            require_noerr_string(result, done, "mutex_unlock failed");
        } else {
            // @synchronized(nil) does nothing
        }
    	
    done:
        if ( result == RECURSIVE_MUTEX_NOT_LOCKED )
             result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
    
        return result;
    }
    
    复制代码

    可以看到,其核心逻辑是recursive_mutex_lock和recursive_mutex_unlock。这两个函数在苹果私有库当中,具体实现无从而知。但是从文档中得知是基于递归类型的pthread_mutex的,这个前文中我们已经讨论过。

    需要注意的是,所传入的obj对象主要作用是生成链表节点的哈希索引。该对象的生命周期对代码块及加锁过程无任何影响。也就是说在传入之后,如论何时将对象释放或则置为nil,都是安全的。但是如果传入一个空对象,将不进行任何的加锁解锁操作。

    2.2 自旋锁

    自旋锁 与互斥锁有点类似,只是自旋锁被某线程占用时,其他线程不会进入睡眠(挂起)状态,而是一直运行(自旋/空转)直到锁被释放。由于不涉及用户态与内核态之间的切换,它的效率远远高于互斥锁。

    虽然它的效率比互斥锁高,但是它也有些不足之处:

    • 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。在高并发执行(冲突概率大,竞争激烈)的时候,又或者代码片段比较耗时(比如涉及内核执行文件io、socket、thread等),就容易引发CPU占有率暴涨的风险
    • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。
    • 自旋锁可能会引起优先级反转问题。具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,自旋锁会处于忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。自旋锁OSSpinLock由于上述优先级反转问题,在新版iOS已经不在保证安全,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。在ios10中建议替换为os_unfair_lock

    因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

    #import <libkern/OSAtomic.h>
    
    OSSpinLock lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&lock);
    OSSpinLockUnlock(&lock);
    复制代码

    2.3 信号量

    信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。

    信号量可以分为几类:

    • 二进制信号量(binary semaphore) / 二元信号量 :只允许信号量取0或1值,,只有两种状态:占用与非占用,其同时只能被一个线程获取。

    • 整型信号量(integer semaphore):信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为0。

    • 记录型信号量(record semaphore):每个信号量s除一个整数值value(计数)外,还有一个等待队列List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线程,让其获得信号量,同时信号量再减一。

    信号量通过一个计数器控制对共享资源的访问,信号量的值是一个非负整数,所有通过它的线程都会将该整数减一。如果计数器大于0,则访问被允许,计数器减1;如果为0,则访问被禁止,所有试图通过它的线程都将处于等待状态。

    2.3.1 pthread中的sem_t

    他的具体调用方式如下:

    #include <semaphore.h>
    
    // 初始化信号量:
    // pshared 0进程内所有线程可用 1进程间可见
    // val     信号量初始值
    // 调用成功时返回0,失败返回-1
    int sem_init(sem_t *sem, int pshared, unsigned int val);
            
    // 信号量减1:
    // 该函数申请一个信号量,当前无可用信号量则等待,有可用信号量时占用一个信号量,对信号量的值减1。
    int sem_wait(sem_t *sem);
            
    // 信号量加1:该函数释放一个信号量,信号量的值加1。
    int sem_post(sem_t *sem);
            
    // 销毁信号量:
    int sem_destory(sem_t *sem);
    
    复制代码

    值得注意的是:上述初始化方法,已经被Apple弃用。在调用时基本返回的都是-1,调用失败。其后所有操作也是无效的。搜索了一下原因,iOS不支持创建无名的信号量所至,解决方案是造建有名的信号量。。换成下属方式,创建一个有名信号量,信号量初值为2。使用结束时,调用与之对应的unlick方法。

    sem_t *semt = sem_open("sem name", O_CREAT,0664,2);
    
    
    sem_unlink(semt);
    
    复制代码

    下面我们来看一个简单的例子。结果很明显可以看出,某一时刻,只有两个线程在输出了waite,其他线程都被挂起了,当1s后这两个线程都post之后。另外两个线程才被唤醒,继续运行。

    func testSem_t(name:String){
        let semt = sem_open(name, O_CREAT,0664,2)
        if semt != SEM_FAILED {
            for i in 0...5 {
                DispatchQueue.global().async {
                	   sem_wait(semt)
                    print("waite \(i)")
                    sleep(1)
                    sem_post(semt)
                    print("post \(i)")
                }
            }
            sem_unlink(name)
        }else{
            if errno == EEXIST {
                print("Semaphore with name \(name) already exists.\n")
            }else{
                print( "Unhandled error: \(errno). name=\(name) \n")
            }
            let newName = name + "\(arc4random()%500)"
            print("new name = \(newName)")
            testSem_t(name: newName)
        }
    }
    复制代码

    值得注意的是:当反复创建同一名字的信号量时,会返回错误。及时重新运行,也会几率性得到错误。因此,一方面我们尽量保证每次创建的信号量名字的唯一性,另一方面在重名返回错误时,也应该做相应的处理。本例中处理方式比较简单,只作为参考。(其中errno为全局变量,是内核<errno.h>返回的错误码)

    2.3.2 dispatch_semaphore

    dispatch_semaphore是GCD用于控制多线程并发的信号量,允许通过wait/signal的信号事件控制并发执行的最大线程数,当最大线程数降级为1的时候则可当作同步锁使用,注意该信号量并不支持递归;

    2.3.1中的例子用dispatch_semaphore实现,代码如下:

    let semt = DispatchSemaphore(value: 7)
    for i in 0...20 {
        DispatchQueue.global().async {
            print(" \(i)")
            semt.wait()
            print("waite \(i)")
            sleep(1)
            semt.signal()
            print("post \(i) ")
        }
    }
    复制代码

    2.3.2 信号量的用途

    • 二元信号量相当于互斥锁,也就是说当信号量初值为1时,wait相当于lock,signal相当于unlock。而它允许在一个线程加锁在另任一线程解锁,使用更加灵活,而带来的不确定性则相应增加。

    下述代码中,线程A将等线程B调用之后再逐一运行。如果换成NSLock理论上由其他线程是不允许的,但运行结果一切正常。而换成NSRecursiveLock递归锁,所有加锁操作将失效,线程不会挂起。用pthread_mutex,也是在设置属性为可递归时,加锁才会失效。(普通互斥锁可能是由信号量实现的,具体原因不明,但不建议这样使用。)

    let semt = DispatchSemaphore(value: 1)
    let q1 = DispatchQueue(label:"A")
    let q2 = DispatchQueue(label:"B")
    for i in 0...20 {
       q1.async {
            print(" \(i)")
            semt.wait()
            print("waite \(i)")
        }
        q2.asyncAfter(deadline: .now() + .seconds(i * 1)){
            semt.signal()
            print("post \(i) ")
        }
    }
    复制代码
    • 控制某个代码块的最大并发数。通过设置信号量的初值,很容易实现某一段代码片段的执行的并发数。或者说控制某个资源最大同时访问量。

    • 当信号量的值为0,而waite/signal分属不同线程时,可以适用于经典的生产者-消费者模型。即一种一对一的观测监听方式。当生产者完成生产后,立刻通知消费者购买。而没有产品时,消费者只能等待。

    var a : Int32 = 0
    let semt = DispatchSemaphore(value:0)
    for i in 0..<303 {
        DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
            print("task start \(i)  a= \(   OSAtomicAdd32(1, &a)     )")
            semt.signal()
        }
    }
    for i in 0..<5 {
        DispatchQueue.global().async {
            var count : Int32 = 0
            while(true){
                semt.wait()
                print("obsever \(i) finish a=\( OSAtomicAdd32(-1, &a)  )  一共抢到\( OSAtomicAdd32(1, &count) )")
            }
        }
    }
    复制代码

    上述例子中,信号量的值相当于库存量。初始库存为0。生产者一共生产了303件商品,每生产一件都会及时对外销售。一共有5位消费者(或者经销商),每当有商品生产出来,都会不同的抢购。从结果中可以看出,由于并发比较高,最大库存存在波动,但是最终库存量是0。5位消费者抢购总数等于生产量。而且抢到的总数是一样的。由于余数是3,头三位多抢了一件。

    上述生产者,消费者模型更加适合用条件变量来实现。下面让我们来仔细看看。

    2.4 条件变量

    条件变量 (Condition Variable) 作为一种同步手段类似于栅栏,允许线程以一种无竞争的方式等待某个条件的发生。当该条件没有发生时,线程会一直处于休眠状态。当被其它线程通知条件已经发生时,线程才会被唤醒从而继续向下执行。条件变量是比较底层的同步原语,直接使用的情况不多,往往用于实现高层之间的线程同步。使用条件变量的一个经典的例子就是线程池(Thread Pool)了。

    NSCondition是条件变量在iOS上的一种实现,他是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。比如说,你可以开启一个线程下载图片,一个线程处理图片。这样的话,需要处理图片的线程由于没有图片会阻塞,当下载线程下载完成之后,则满足了需要处理图片的线程的需求,这样可以给定一个信号,让处理图片的线程恢复运行。

    func consumer() {
            DispatchQueue.global().async {
                print("start to track")
                while(true){
                    self.conditionLock.wait()
                    print("in  \(Thread.current)")
                }
            }
        }
        
    func producer(){
        let queue1 = DispatchQueue.global()
        for i in 0...5 {
            queue1.asyncAfter(deadline: .now() + .milliseconds(i*300), execute: {
                print(i)
                self.conditionLock.signal()
            })
        }
    }
    复制代码
    输出结果
    start to track
    0
    in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
    1
    in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
    2
    in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
    3
    in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
    4
    in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
    5
    in  <NSThread: 0x604000272dc0>{number = 3, name = (null)}
    复制代码

    与lock和unlock一一对应相同的是,NSCondition中wait()与signal()也需要一一对应。多个线程waite()时,按顺序解锁。多出的wait()线程,如果一直等不到signal(),会造成死锁。同理同一时刻多个线程signal(),多余的将得不到处理。上述例子中,当时间延迟为0时,每次将只会执行一次,因为同一时间只有一把锁,多余的钥匙将被丢弃。

    NSConditionLock 是另一种条件变量,唯一不同的是,它可以传入一个整型数,从而确定具体的条件。也就是具有处理多种条件的能力。与其他锁一样,**lock(whenCondition:)unlock(withCondition:)**是一一对应的,并且只有condition值相同时,才可以顺利解锁。由于继承NSLock,两者如lock()/unlock()类似,唯一不同是是否指定或修改condition值

    let conditionLock = NSConditionLock()
    let queue1 = DispatchQueue.global()
    for i in 1...5 {
        queue1.asyncAfter(deadline: .now() + .milliseconds(0), execute: {
            conditionLock.lock()
            print("dosomthing thread1 cordition=\(i) ")
            if i == 3 {
                conditionLock.unlock(withCondition:3)
            }
            conditionLock.unlock()
        })
        DispatchQueue.global().async {
            conditionLock.lock(whenCondition:3)
            print("in \(Thread.current)")
            conditionLock.unlock()
        }
    }
    复制代码

    上述代码几率性得到结果如下

    dosomthing thread1 cordition=1 
    dosomthing thread1 cordition=2 
    dosomthing thread1 cordition=3 
    in <NSThread: 0x604000663600>{number = 4, name = (null)}
    in <NSThread: 0x604000663700>{number = 5, name = (null)}
    dosomthing thread1 cordition=5 
    in <NSThread: 0x6040006635c0>{number = 6, name = (null)}
    in <NSThread: 0x60000026f340>{number = 3, name = (null)}
    dosomthing thread1 cordition=4 
    in <NSThread: 0x600000275780>{number = 7, name = (null)} 
    复制代码

    上述代码中,多个线程等到condition=3后才等以执行。

    ###2.4 读写锁 读写锁 从广义的逻辑上讲,也可以认为是一种共享版的互斥锁。如果对一个临界区大部分是读操作而只有少量的写操作,读写锁在一定程度上能够降低线程互斥产生的代价。

    对于同一个锁,读写锁有两种获取锁的方式:共享(share)方式,独占(Exclusive)方式。写操作独占,读操作共享

    读写锁状态 以共享方式获取 以独占方式获取
    自由 成功 成功
    共享 成功 等待
    独占 等待 等待
    NSString *path = [[NSBundle mainBundle] pathForResource:@"t.txt" ofType:nil];
        dispatch_group_t group = dispatch_group_create();
        __block double start = CFAbsoluteTimeGetCurrent();
        for (int k = 0; k <= 3000; k++) {
            dispatch_group_enter(group);
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [self readBookWithPath:path];
                dispatch_group_leave(group);
            });
            dispatch_group_enter(group);
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [self writeBook:path string:[NSString stringWithFormat:@"--i=%d--",k]];
                dispatch_group_leave(group);
            });
        }
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"----result=%@ time=%f",[self readBookWithPath:path],CFAbsoluteTimeGetCurrent()-start);
        });
    复制代码
    - (NSString *)readBookWithPath:(NSString *)path {
        pthread_rwlock_rdlock(&rwLock);
        NSLog(@"start read ---- ");
        NSString *contentString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
        NSLog(@"end   read ---- %@",contentString);
        pthread_rwlock_unlock(&rwLock);
        return contentString;
    }
    - (void)writeBook:(NSString *)path string:(NSString *)string {
        pthread_rwlock_wrlock(&rwLock);
        NSLog(@"start wirte ---- ");
        [string writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
        NSLog(@"end   wirte ---- %@",string);
        pthread_rwlock_unlock(&rwLock);
    }
    复制代码
    输出结果:
    ......
    2017-12-24 17:24:20.506522+0800 lock[8591:299152] start wirte ----
    2017-12-24 17:24:20.507522+0800 lock[8591:299152] end   wirte ---- --i=2998--
    2017-12-24 17:24:20.507685+0800 lock[8591:299162] start read ----
    2017-12-24 17:24:20.507828+0800 lock[8591:299162] end   read ---- --i=2998--
    2017-12-24 17:24:20.507943+0800 lock[8591:299154] start wirte ----
    2017-12-24 17:24:20.508872+0800 lock[8591:299154] end   wirte ---- --i=2999--
    2017-12-24 17:24:20.509065+0800 lock[8591:299161] start read ----
    2017-12-24 17:24:20.509240+0800 lock[8591:299161] end   read ---- --i=2999--
    2017-12-24 17:24:20.509358+0800 lock[8591:299157] start wirte ----
    2017-12-24 17:24:20.510294+0800 lock[8591:299157] end   wirte ---- --i=3000--
    2017-12-24 17:24:20.510443+0800 lock[8591:298979] start read ----
    2017-12-24 17:24:20.510582+0800 lock[8591:298979] end   read ---- --i=3000--
    2017-12-24 17:24:20.510686+0800 lock[8591:298979] ----result=--i=3000-- time=5.968375
    复制代码

    2.4 临界区

    临界区 (Critical Section)是相较于互斥锁更为严格的同步手段。只对本进程可见,其他进程试图获取是非法的(信号量和互斥量可以)。获取锁被称为进入临界区,释放锁叫做离开临界区。除此之外,它具有和互斥锁相同的性质。

    转载于:https://juejin.im/post/5a48c49d518825772a4b53fe

    展开全文
  • 当有个线程同时对某一块资源进行读写的时候就会发生冲突,例如个线程都尝试改变一个变量的值,最终会导致数据计算的出错,也即线程竞争 简单讲: 两个程序同时读写同一个共享数据时会发生意想不到的后果 为了避免...
  • 通过一些方法让进程在竞争资源时相互协调,避免出现数据不完全、不一致等问题,这就叫线程同步。 临界区与临界资源: 被线程同时访问的代码叫临界区,被同时访问的资源叫临界资源。 原子操作:中间不会打断的...
  • 线程通信,锁

    2020-09-04 17:02:45
    通过一些方法让进程在竞争资源时相互协调,避免出现数据不完全、不一致等问题,这就叫线程同步。 临界区与临界资源: 被线程同时访问的代码叫临界区,被同时访问的资源叫临界资源。 原子操作:中间不会打断的...
  • 通过一些方法让进程在竞争资源时相互协调,避免出现数据不完全、不一致等问题,这就叫线程同步。 临界区与临界资源 被线程同时访问的代码叫临界区,被同时访问的资源叫临界资源。 原子操作 中间不会打断的操作叫...
  • 通过一些方法让进程在竞争资源时相互协调,避免出现数据不完全、不一致等问题,这就叫线程同步。 临界区与临界资源: 被线程同时访问的代码叫临界区,被同时访问的资源叫临界资源。 原子操作: 中间不会打断的...
  • 内核同步介绍

    2015-09-09 17:19:08
    个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子的执行--也就是说,操作在执行结束前不能被打断,就如同整个临界区是一个不可分割的指令一样....
  • 环境编程10

    2020-09-14 11:30:55
    通过一些方法让进程在竞争资源时相互协调,避免出现数据不完全、不一致等问题,这就叫线程同步。 临界区与临界资源: 被线程同时访问的代码叫临界区,被同时访问的资源叫临界资源。 原子操作:中间不会打断的...
  • (九)内核同步介绍 共享资源要防止并发访问,因为如果个执行...如果两个执行线程有可能处于同一个临界区中同时执行,那么这就是程序包含的一个bug,我们称它是竞争条件(这里会存在线程竞争)。 避免并发和防止竞
  • C++11 atomic原子操作

    2020-02-12 11:29:03
    所以C++11引入了自己的互斥量的概念来避免在多线程的运行中出现的问题,那么对于每次的加锁解锁以及其他的操作对于资源的消耗都是一定的,那么就又引入了std::atomic的类模板,实现了原子操作,从而避免了在数据的...
  • 所以共享资源一定要防止并发访问,如果个执行线程同时访问和操作数据,就可能发生各线程之间相互覆盖共享数据情况,造成被访问数据处于不一致状态,因此我们要了解Linux内核如何解决同步问题和防止产生竞争条件。...
  • 内核锁浅析

    2018-03-30 20:35:00
    多线程并发访问同一资源时,有可能会导致数据读写异常,多线程之间相互竞争,并且问题不容易复现,调试困难,所以需要引入临界区保证代码段是原子性的,要么执行,要么不执行,不容许执行过程中被打断。...
  • lnux内核同步机制

    2020-01-29 22:45:43
    个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子的执行——也就是说,操作在执行结束前不可被打断,就如同临界区是一个不可分割的指令一样。 竞争 如果两个...
  • 第九章 内核同步介绍

    2018-07-11 23:09:22
    个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子的执行——也就是说,操作在执行结束前不可被打断,就如同临界区是一个不可分割的指令一样。竞争如果两个...
  • LINUX内核~~~临界区

    2020-11-21 16:50:46
    个执行线程并发访问同一个资源是不安全的,为了避免临界区中的并发访问,编程者必须保证这些代码的原子地执行---也就是说,操作想执行结束前不可被打断,就如同临界区是一个不可分割的指令一样。 如果两个执行...
  • 个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码 原子地执行。也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。 2、如果两...
  • Linux内核同步

    2016-12-08 23:53:15
    个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,必须保证这些代码原子执行,操作在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。如果两个执行线程有可能处于同一...
  • VxWorks进程通信0 -- 任务管理

    千次阅读 2012-02-19 10:45:59
    VxWorks进程通信0 -- 任务管理 任务是代码运行的一个映象,从系统的角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、I/O设备及内存空间等系统资源... 任务设计能随时打断正在执行着的任务,对
  • 个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行。也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。 2、如果两...
  • 从系统的角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。 LiteOS是一个支持任务的操作系统。在LiteOS中,一个任务表示一个线程。 LiteOS中的...
  • VxWorks操作系统指南(1.3) 任务管理

    千次阅读 2005-03-06 15:25:00
    1.1.任务管理 任务是代码运行的一个映象,从系统的角度看,任务是竞争系统资源的最小运行单元。...1.1.1.任务结构 任务设计能随时打断正在执行着的任务,对内部和外部发生的事件在确定的时间里作
  • VxWorks学习笔记 -- 任务管理

    千次阅读 2011-04-12 11:42:00
    VxWorks学习笔记 -- 任务管理 任务是代码运行的一个映象,从系统的角度... 任务结构  任务设计能随时打断正在执行着的任务,对内部和外部发生的事件在确定的时间里作出响应。VxWorks实时内核Wind提供了基

空空如也

空空如也

1 2
收藏数 28
精华内容 11
关键字:

多线程竞争资源打断