zoukankan      html  css  js  c++  java
  • 线程

    1.线程概念

    一个进程在某一时刻只能做一件事情,当有了线程的时候,从宏观上面来说,线程是并行的,并且在某一个线程发生阻塞的时候,某一些线程还是可以运行的,这在单处理器上面来说,同样具有不小的优势,并且处理器的多核可以与器多线程进行连接,可以实现高并发,并且改善相应时间和提高吞吐率

    在Linux中,新建的线程并不是在原先的进程中,而是系统通过一个系统调用clone()。该系统copy了一个和原先进程完全一样的进程, 并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。这样,原先进程中的变量变动在copy后的进程中便能体现出来

    2.线程ID

    由于线程ID使用pthread_t表示的一类数据结构,所以不能用一种可移植的方式打印数据类型的值

    2.1 关于线程ID的相关函数

    int pthread_equal(pthread_t tid1,pthread_t tid2)

    比较如果pthread_t数据结构类型的变量相等,则返回0,若不相等则返回非0值

    pthread_t pthread_self(void);

    返回调用线程的ID

    3.关于创建线程

    int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void *),void *restrict arg);

    tidp是一个指针,指向了一个pthread_t空间,可以用来存放线程ID

    attr是线程属性

    start_rtn是函数地址

    arg是函数的参数值,当参数较多的时候,可以打包成一个结构体,然后传递进去

    4.关于线程终止

    4.1线程退出方式

    4.1.1正常方式退出

    4.1.2以pthread_exit(void *rval_ptr)退出

    4.1.3线程被同一进程中的其他线程取消

    int pthread_cancel(pthread_t tid);

    如果线程被取消,则返回的值是PTHREAD_CANCELED,他的行为就像是pthread_exit()函数设置了PTHREAD_CANCEL,但是此函数只是在另一个线程中,对他想要取消的线程提出申请,线程可以选择忽略或者是控制如何被取消

    4.2获得退出的状态

    int pthread_join(pthread_t thread,void **rval_ptr)

    4.2.1在Linux中,默认情况下是在一个线程被创建后,必须使用此函数对创建的线程进行资源回收,但是可以设置Threads attributes来设置当一个线程结束时,直接回收此线程所占用的系统资源。

    pthread_join使一个线程等待另一个线程结束。

    代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行

    在多线程编程的时候我们往往都是以for循环的形式调用pthread_join函数,既然运行prhtead_join之后主线程就阻塞了,也没法调用后面的pthread_join,那么以for循环有什么用呢?

    主线程是在第一个线程处挂起。

    比如有:

    pthread_join(1,NULL);

    pthread_join(2,NULL);

    pthread_join(3,NULL);

    pthread_join(4,NULL);

    pthread_join(5,NULL);

    实际上主线程在pthread_join(1,NULL);这里就挂起了,在等待1号线程结束后再等待2号线程。

    当然会出现3,4,5比1,2先结束的情况。主线程还是在等待1,2结束后,发现3,4,5其实早已经结束了,就会回收3,4,5的资源,然后主线程再退出。

    4.3程序分析

    要注意pthread_join(pthread_t thread,void **rval_ptr)ptr是一个双重指针

    而pthread_exit(void *rval_ptr)是一个指针,rval_ptr可以是一个结构体,但是结构体必须保证,在线程结束的时候是可用的。上面的tret实际上是定义了一个指针,用一个指针变量去接受一个exit所退出的状态(void* 2,强制转化为指针了已经)

    4.4线程退出时的清理函数

    4.4.1线程清理函数在此条件下进行调用

    (1)调用pthread_exit()

    (2)相应取消请求

    (3)用非0execute参数调用pthread_cleanup_pop

    4.4.2void pthread_cleanup_push(void (*rtn)(void *),void *arg);

    相当于在线程中注册一个清理一个函数,进行线程退出时候的清理工作,不是必须的,即进程也可以不使用此函数

    4.4.3void pthread_cleanup_pop(int execute);

    每次调用此函数的时候,总是要在调用完void pthread_cleanup_push时,进行以前注册的函数弹栈操作

    5.线程同步

    5.1线程需要同步的情况

    (1)A线程读变量,写变量需要两个周期,但是B线程在两个写周期中间读变量的时候,就会造成数据不准确,此时应该进行同步


    (2)两个线程对同一变量进行同一时间的修改,通常对变量进行修改需要进行如下几步

    ->从内存单元读入寄存器

    ->把寄存器中对变量做增量操作

    ->把新的值写到内存单元

    但是要注意,当A线程读取一个初始值为1的变量,此时没有进行到步骤三,B线程此时开始执行,只要此时指向发生在A进程步骤三之前,此时B进程也会读出变量1来,当A,B线程都执行完毕的时候,此时变量并没有发生变化,所以次步骤需要进行同步

    (3)除了计算机体系结构造成的不同步的原因外,程序使用变量的方式也会引起竞争,也会导致不一致的情况发生,比如我们对某个变量加1,然后基于这个值做出某种决定,因为这个决定和这个增量操作并非院子操作,所以也会给不一致的情况提供给机会

    5.2互斥量

    互斥量本质上来说就是一把锁,在访问共享资源的时候需要加锁,在访问完之后需要释放锁,在对互斥量进行加锁之后,任何想要进行加锁的线程都会被阻塞,当释放锁的时候,只有一个线程可以运行

    注意!!!只有所有线程都设计成遵守相同的数据访问规则,互斥机制才能正常运行

    5.2.1互斥变量的创建以及销毁

    int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr)

    mutex代表的是互斥量,互斥量可以静态分配也可以动态分配

    attr 代表的是互斥量的属性

    当互斥量是静态分配的可以直接复制PTHREAD_MUTEX_INITIALIZER,或者调用pthread_mutex_init进行初始化

    当互斥量是动态分配的,必须利用init函数进行初始化。

    int pthread_mutex_destory(pthread_mutex_t *mutex);

    对于动态分配的互斥变量,必须进行销毁

    5.2.2互斥量的加锁与解锁

    int pthread_mutex_lock(pthread_mutex_t *mutex)

    对互斥量进行加锁,有并发的线程则访问一个加锁的互斥量,则进行阻塞

    int pthread_mutex_trylock(pthread_mutex_t *mutex)

    如果可以锁住互斥量,则返回0,如果不能锁住互斥量,则返回EBUSY,互斥量已经被锁住

    int pthread_mutex_unlock(pthread_mutex_t *mutex)

    对互斥量进行解锁,此时可以通知各个线程,本互斥量处于可以加锁的状态


    5.2.3加锁条件

    当对一个共享变量符合5.1的时候,需要加锁

    并且在一些不想被打断的操作中,也需要加锁

    5.3避免死锁

    5.3.1死锁发生的调价

    ->对同一资源访问两次

    ->当有两个资源的时候,A线程访问一资源,但是二资源被B线程占有,此时A线程阻塞,B线程拥有二资源,但是想要需要一资源,此时B线程也阻塞,此时两个线程一直阻塞,发生死锁

    5.3.2死锁的避免方法

    (1)控制互斥量加锁的顺序来避免死锁,比如需要对两个互斥量A和B同时加锁,如果所有线程总是对互斥量B加锁之前锁住互斥量A,那么使用这两个互斥量就不会产生死锁,如果所有线程首先锁住B,然后锁住互斥量A,那么也不会发生死锁

    (2)如果涉及了太多的锁和数据结构,那么就需要别的方法,比如说,可以释放占有的锁,然后过一段时间再试,这种情况可以使用pthread_mutex_trylock()接口避免死锁,如果已经占有某些锁,而且pthread_mutex_trylock接口返回成功,那么就可以前进,不能获取锁,可以先释放已经占有的锁,做好清理工作,然后过一段时间在重新试

    列如

    5.3.3避免死锁的一个例子

    5.3.3.1

    (1)为什么在初始话的时候在23行和24行没有进行锁住fp->f_lock因为在这个动作之前,此变量唯一被使用的就是在这个函数里面,因为对其他的函数foo还不具备可见想(这些函数由其他线程调用),即是其他线程调用的也是这个函数,malloc重新分配的也不管他的事情,fp不一样

    (2)在31行的时候,因为fh数组对其他的函数是可见的,所以应该加锁访问,所以此foo结构队其他的线程具备可见性,因为将这个变量已经加入了HASH桶中,所以再次初始化的时候应该加锁,初始代码在36,37,38行

    5.3.3.2

    (1)避免死锁的一个策略就是以相同的顺序访问两个资源,也就是紧接着上锁两个互斥量,并不是说,上锁这两个互斥量的时候中间必须没有间隔(即必须一个资源的上锁后面必须紧跟着另一个资源)而是要访问哪个资源就先锁住哪个资源,在访问的这个资源的时候访问在访问另一个资源的时候,不解锁第一个锁,直接锁上第二个锁,大体的顺序就是先上锁互斥量A后上锁互斥量B

    5.3.3.3

    在75行的时候,如果单单访问一个共享变量,则只对一个互斥量进行加锁即可

    5.4读写锁

    读写锁与互斥量类似,不过读写锁可以允许更高的并行性,互斥锁只有两种状态,加锁状态和解锁状态,而且一次只有一个线程可以对其进行加锁,读写锁可以有三中状态,读模式下加锁,写模式下加锁,解锁状态,写模式只有一个线程可以对其加锁(所有想要对其加锁的线程都会被阻塞),而读模式状态下可以有多个线程对其进行加锁(当有一个写模式准备对其进行加锁的时候,读写锁通常会阻塞随后的读模式请求,这样可以避免读模式锁长期占用)

    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock.const pthread_rwlockattr_t *restrict attr);

    成功返回0,失败就返回错误编号

    读写锁必须使用此函数进行初始化

    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

    成功返回0,失败就返回错误编号

    读写锁在使用完毕的时候,必须进行销毁

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

    成功返回0,失败就返回错误编号

    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

    成功返回0,失败就返回错误编号

    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

    成功返回0,失败就返回错误编号

    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

    int pthread_rwlock_trywrlock(pthread_rwlock_t *wrlock);


    如例子,此处用一个读写锁保护了两个共享结构,在需要此些资源的时候,要进行写操作,则线程对其进行加锁,pthread_rwlock_wrlock()进行写加锁模式,再用完互斥变量多时候进行了解锁

    注意!!当进程的读操作很多,但是写操作不是很多,允许所有的读线程的进程并行的进行,可以增加进程的吞吐量

    5.5条件变量

    5.5.1定义

    当多个线程之间因为存在某种依赖关系,导致只有当某个条件存在时,才可以执行某个线程,此时条件变量(pthread_cond_t)可以派上用场。比如:

    (1)当系统不忙(这是一个条件)时,执行扫描文件状态的线程。

    (2)多个线程组成线程池,只有当任务队列中存在任务时,才用其中一个线程去执行这个任务。为避免惊群(thrundering herd),可以采用条件变量同步线程池中的线程。

    5.5.2初始化条件变量

    int pthread_cond_init(pthread_cont_t *restrict cond,const pthread_condattr_t *restrict attr);

    使用条件变量的时候有两种方法,第一种是使用静态分配的方法,需要赋PTHREAD_COND_INIT赋给静态分配的条件变量,如果条件变量是动态分配的,则需要pthread_cond_init函数进行初始化


    int pthread_cond_destroy(pthread_cond_t *cond)

    对动态分配的函数进行反初始化

    5.5.3条件变量的阻塞

    int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

    函数pthread_cond_wait()有几步操作:1。判断条件2.如果条件满足,继续执行;如果条件不满足,就将线程挂到条件变量的等待线程队列中。如果不加锁的话(在第一步之前加锁,一直锁到退出函数为止),这两步之间就可能存在时间窗口(1)当线程1判断条件不满足,(2)然后准备把线程挂起的时候,线程2改变了条

    (3)接着线程1挂在了条件变量的等待队列上,这样就可能死锁!!!


    如果加上锁,这种时间窗口就会消除,使pthread_cond_wait的操作变成原子操作。pthread_cond_wait的第二个参数是一个加了锁的互斥量,这样可以避免线程在判断条件变量以及挂起的时候被别的线程改变条件。如果线程被挂起,pthread_cond_wait里面会解锁,是为了让别的线程来改变条件变量(别的线程在改变条件变量的时候,必须加锁,也是为了防止竞争)。但是从phread_cond_wait返回的时候,互斥量要再次被锁住。


    总的来说,互斥锁就是用来保护条件变量的,因为有一些操作不是原子操作,存在竞争。


    关于参数的一些限制

    (1)mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),

    (2)且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前

    (3)mutex保持锁定状态,并在线程挂起进入等待前解锁。

    (4)在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。 

    (5)激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个; 

    (6)而pthread_cond_broadcast()则激活所有等待线程(惊群)。

    条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被 用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或 多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步

    int pthread_cond_timewait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespace *restrict tsptr);

    可以加一个等待时间

    int pthread_cond_signal(pthread_cond_t *cond);

    至少能够唤醒一个等待该条件的线程


    int pthread_cond_broadcast(pthread_cond_t *cond);

    唤醒等待该条件的所有线程

    利用qlock在29行进行对共享变量的加锁,并且在18行进行了对条件变量的加锁,phread_cond_wait进行了等待条件变量的改变,查询是否可以进行改变

    5.6屏障

    5.6.1定义

    屏障是用户协调多个线程并行工作的同步机制,允许多个进程共同等待,当等到一个点的时候,这个点也即是屏障,当所有的线程都到达屏障的,所有线程可以继续运行;

    5.6.2创建屏障和销毁
    int pthread_barrier_init(pthread_barrier_t *barrier,const pthread_barrierattr_t *restrict attr,unsigned count);
    count参数必须大于0, 指定要同步的线程的数量:只有当所有的线程都执行pthread_barrier_wait后,它们才能从pthread_barrier_wait返回。

    int pthread_barrier_destroy(pthread_barrier_t *barrier);

    如果使用init(上面)函数进行初始化屏障,需要用destroy函数进行反初始化屏障量

    int pthread_barrier_wait(pthread_barrier_t *barrier);

    pthread_barrier_wait:同步当前线程,使其在barrier对象处同步。当在该barrier处执行pthread_barrier_wait的线程数量达到预先设定值后,该线程会得到PTHREAD_BARRIER_SERIAL_THREAD返回值,其他线程得到0返回值。barrier对象会被reset到最近一次init的状态。






  • 相关阅读:
    吃推荐3个最近去了的好地方
    30岁生日
    今天开始试用Briglow Hair Cream
    WPF中如何在文本外面加虚线外框
    对账算法改进
    如何退出正在Sleep的线程
    关于framework4.5的相关介绍
    恐怖的报警邮件
    对帐引擎2个月后的监控数据
    wcf rest服务启用gzip压缩
  • 原文地址:https://www.cnblogs.com/SmileLion/p/5863590.html
Copyright © 2011-2022 走看看