美文网首页
线程控制原语/线程同步

线程控制原语/线程同步

作者: D_Major | 来源:发表于2020-03-15 05:35 被阅读0次

    线程是最小执行单位, 进程是最小分配资源单位. 线程由进程退化而来(进程调用pthread_create), 多个线程同享同一份虚拟地址空间, 每一个线程有一个独立的PCB. 对CPU而言, 进程和线程的地位是等同的(都有PCB), 所以一个进程退化成多个线程后能够分到更多的时间片, 从而提高效率. 各线程共享文件描述符表和除栈以外的用户地址空间; 不共享内核栈/用户栈,信号屏蔽字,errno变量.
    使用多线程模型时应避免使用fork()和信号, 否则容易出错.

    三级页表

    PCB内页目录的指针指向中间页目录(4k), 中间页目录的每一个指针指向一个页表, 页表的每一个指针又指向一个物理页面, 物理页面内部就是存储数据的内存单元. 页目录 -> 中间页目录 -> 页表 -> 物理页面, 该结构称为三级页表. 物理页面和虚拟地址空间通过MMU映射起来, MMU记录页码和页框码以及页内偏移量, 经常访问的页面会存放到快表寄存器中.

    栈帧和寄存器

    线程虽然独享PCB, 但是它的页目录页表还是跟原进程相同的, 所以多个线程共享虚拟地址空间, 指向相同的内存. 线程负责执行函数, 所以可以看做寄存器和栈的集合, 每一个函数在栈空间内都有一个栈帧(通过寄存器指针esp - ebp得到), 由于对应的寄存器只有一个, 所以栈帧内会存储之前ebp/esp的值. 线程也要在内核栈内保存寄存器的值, 用来切换线程.

    线程控制原语

    pthread库是第三方库, 编译链接时要加上-lpthread参数.

    pthread_self()查看进程ID, 相当于getpid(). 注意线程号LWP不是线程ID, 线程号是CPU分配时间片的依据, 线程ID用来在进程中区分线程. 使用ps -Lf pid查看线程号, 其中pid是进程号, 可以看到该进程下各线程的LWP.

    pthread_create(&tid, NULL, 线程主函数, 线程主函数参数)创建线程, 相当于fork(). 成功返回0, 失败返回错误号, 不是-1, 可以使用strerror(errno)打印错误信息. 线程主函数为void* (*func)(void*)类型的函数指针. 参数必须传值进去如(void*)i, 传地址可能是在争抢中其他线程的参数.
    调用pthread_create()的进程退化为主控线程, 与其他线程的pid相同, 线程ID各不同.

    pthread_exit((void*)retval)单个线程自己退出, 等价于exit(), 参数retval为线程退出时传出的值. 线程中最好不要使用return, 绝不能使用exit(), exit()会将整个进程退出.

    pthread_join(tid, (void**)&retval)等价于waitpid(), 阻塞等待线程关闭, 回收线程, 可以是兄弟线程调用. 传出参数&retval同上, 类似于&status, 也是线程退出时传出的值. 根据如何退出的retval会存放不同的值, 如return存放返回值, pthread
    _cancel()存放PTHREAD_CANCELED, pthread_exit()存放exit传出的retval.

    pthread_detach(tid)使线程与主控线程断开关系, 从而不需要(也不能被)其他进程pthread_join()就可以自己结束并回收所有资源(清理PCB), 不会导致僵尸进程.

    pthread_cancel(tid)等价于kill(), 杀死线程. 线程不会立刻被杀死, 而是在系统调用处如open()/read()后才被杀死. 可以自己添加取消点pthread_testcancel().

    修改线程属性
    1. 修改线程为分离态: 除了detach函数也可以在create的时候就指定为分离态

    pthread_attr_t是一个结构体, 需要有初始化init和对应的destroy函数, 在setdetachstate后传入到create中.

    1. 修改线程栈的大小

    线程同步

    互斥量

    pthread_mutex_t mutex定义锁, 是结构体类型, 可看作只有0/1的整数. 初始化成功为1. 应定义成全局变量.
    pthread_mutex_init(&mutex, NULL)对互斥锁进行初始化为1, 要在create线程之前, 声明和初始化如果定义成全局/静态变量可以用静态初始化替代pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER, 局部变量必须用动态初始化.
    pthread_mutex_destroy(&mutex). 销毁互斥锁

    pthread_mutex_lock(&mutex)加锁, 将mutex-1变为0. 加锁失败会阻塞.
    pthread_mutex_unlock(&mutex)解锁, 将mutex+1变为1. 同时唤醒所有阻塞在该锁上的线程.
    pthread_mutex_trylock(&mutex)尝试非阻塞加锁, 如果锁已被占用则直接返回, 不会像lock一样阻塞.
    在访问共享资源前加锁, 访问结束后立即解锁, 锁的粒度应越小越好.

    死锁
    1. 连续对同一个互斥量加锁两次. 第二次请求加锁时会造成线程阻塞等待, 导致第一次加的锁无法释放.

      解决方法: 加锁完立刻解锁, 第二次加锁前先检查是否已经有锁.

    2. 线程1拥有着A锁, 请求获得B锁; 线程2拥有着B锁, 请求获得A锁. 对于已经拥有互斥锁的线程, 再去请求别的锁, 会导致线程阻塞等待. 双方都不释放就会导致死锁.

      预防方法:

      1. 一次申请两把锁, 之后一起释放.
      2. 当不能拿到所有的锁时, 主动释放已有的锁, 等别的线程把两个锁都释放后就可以继续加锁了.
      3. 对线程编号.
        注意, trylock()不能解决问题, 因为即使不阻塞等待, 但两个锁仍未被两个线程释放, 所以仍然无法得到对方的锁.

    死锁解决方法:
    除了预防, 还可以1.抢占资源分配给死锁线程. 2.杀死线程, 打破循环.

    哲学家吃饭问题:

    策略1: 规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家则先拿起他右边的筷子,然后再去拿他左边的筷子。按此规定,将是0、1号哲学家竞争1号筷子,2、3号哲学家竞争3号筷子。即五个哲学家都竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一个哲学家能获得两支筷子而进餐。

    策略2: 至多只允许四个哲学家同时进餐,以保证至少有一个哲学家能够进餐,最终总会释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。定义信号量count,只允许4个哲学家同时进餐,这样就能保证至少有一个哲学家可以就餐。一个主动放弃自己(可能)拥有的锁, 使别的线程获得. 使用信号量实现.

    读写锁

    读写锁同时具备两种状态: 读锁和写锁. 其特性为: 写独占, 读共享; 写锁优先级高.
    举例: 当已经有线程对共享资源加了读锁时, 若其他线程也都是读者, 以读模式对其加锁的线程可以共享该线程的读锁, 一起读; 若其他线程都是写者, 则写者都阻塞; 若其他线程有读锁有写锁, 则写锁优先.
    若共享资源已被加了写锁, 在写的时候别的线程都无法访问共享资源, 等到写锁释放后, 其他线程再争夺锁, 这时仍是写锁优先.
    一个读写锁同时只能有一个写者或多个读者(与CPU数相关), 当有多个写者竞争时, 只有一个能获得写锁, 其他的写者下一次获得锁.

    pthread_rwlock_init()初始化
    pthread_rwlock_destroy()销毁, 主控线程内最后执行
    pthread_rwlock_rdlock()读锁
    pthread_rwlock_tryrdlock()尝试读
    pthread_rwlock_wrlock()写锁
    pthread_rwlock_trywrlock()尝试写
    pthread_rwlock_unlock()解锁, 不区分读写
    rdlock若申请不到锁,则自旋(隔一会再看看),tryrdlock若申请不到锁,则返回(不会再尝试),由用户自旋.

    条件变量

    条件变量不是锁, 但会造成线程阻塞. 通常与互斥锁配合使用, 实现生产者消费者模型. 条件变量的优点是减少生产者和消费者,消费者和消费者之间对互斥量无意义的竞争, 使线程挂起释放CPU, 提高效率.
    pthread_cond_t cond条件变量, 结构体类型
    pthread_cond_init(cond, NULL)初始化, 声明和初始化如果定义成全局/静态变量也可以用静态初始化替代pthread_cond_t cond = PTHREAD_COND_INITIALIZER
    pthread_cond_destroy(cond)摧毁
    pthread_cond_wait(cond, mutex)阻塞等待条件变量并释放锁, 当被唤醒时才重新加锁. 在调该函数之前需要线程已加互斥锁, 再判断是否符合条件变量(如共享资源为空)来阻塞并释放锁.

    pthread_cond_timedwait(cond, mutex, abstime)阻塞一定时间就不等了, abstime是一个timespec结构体, 有tv_sec(秒)和tv_nsec(纳秒)两个成员. 绝对时间是相对于1970年1月1日00:00:00来说, 需要先使用time_t cur = time(NULL)获取当前时间, 再把cur+n的偏移量赋值给结构体成员tv_sec.
    pthread_cond_signal(cond)唤醒至少一个阻塞在条件变量的线程.
    pthread_cond_broadcast(cond)唤醒所有阻塞在条件变量的线程.

    生产者生产后条件变量就满足了, 之后使用signal()/broadcast()唤醒阻塞在条件变量的消费者线程, 不过消费者线程在访问共享数据时仍要先获得锁.

    信号量

    int sem_init(sem_t *sem, int pshared, unsigned int value)sem_t虽然是结构体, 但可以近似看作是整数. 由于进程和线程都可以使用信号量, pshared用于指定信号量是否可以在进程间共享(一般传0). value是信号量的初值, 当取1时和互斥量相同. 不同于线程系列函数, 失败返回-1并设置errno.
    sem_destroy(sem)销毁信号量.

    在用信号量实现生产者消费者模型时, 生产者先sem_wait(&sem_blank)使共享资源的空格信号量减少, 然后生产, 之后再sem_post(&sem_product)使产品信号量增加. 对应消费者那边先sem_wait(&product)使产品信号量减少, 然后消费走, 再sem_post(&sem_blank)使空格信号量增加.
    共享资源的环形队列可以用数组实现, i = (i+1)%NUM, 通过取余实现逻辑上的环.

    相关文章

      网友评论

          本文标题:线程控制原语/线程同步

          本文链接:https://www.haomeiwen.com/subject/gmtkjhtx.html