玩命加载中 . . .

3.1-线程


线程

  • 与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程,同一个进程中的线程均会独立执行相同的程序,且共享同一份全局内存区域,包括初始化数据段,未初始化数据段,以及堆内存段

  • 进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位

  • 线程是轻量级进程(LWP: Light Weight Process),在Linux环境下线程的本质仍是进程

  • 查看指定进程的LWP号:ps -Lf pid

线程共享资源

  • 进程ID和父进程ID
  • 进程组ID和会话ID
  • 用户ID和用户组ID
  • 文件描述符表
  • 信号处置
  • 文件系统的相关信息:文件权限掩码(umask),当前工作目录
  • 虚拟地址空间(除栈、.text)

线程非共享资源

  • 线程ID
  • 信号掩码
  • 线程特有数据
  • error变量
  • 实时调度策略和优先级
  • 栈、本地变量和函数的调用链接信息

查看当前系统使用的线程库

getconf GNU_LIBPTHREAD_VERSION
NPTL 2.27   // Native POSIX Thread Library

线程相关API

创建线程

main函数所在的线程称为主线程,其余线程称为子线程

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// 创建一个线程
- thread: 传出参数,子线程id
- attr: 设置线程的属性,默认NULL
- start_routine: 子线程需要处理的逻辑代码
- arg: 给第三个参数使用
// 成功返回0,失败返回错误号,与errno不一样
// 获取错误号信息:char* strerror(int errnum)
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* callback(void* arg) {
    printf("child thread...\n");
    printf("arg: %d\n", *(int *)arg);
    return NULL;
}

int main() {
    pthread_t tid;
    int num = 10;
    int ret = pthread_create(&tid, NULL, callback, (void *)&num);
    if (ret != 0) {
        char *errstr = strerror(ret);
        printf("%s\n", errstr);
    }
    for (int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }
    sleep(1);
    return 0;
}
0
1
child thread...
arg: 10 // 主线程和子线程交替执行
2
3
4

终止线程

#include <pthread.h>
void pthread_exit(void *retval);
// 线程退出
- retval: 作为返回值,向线程的回收者传递其退出信息,可以在pthread_join()中获取到
=============================================

pthread_t pthread_self(void);
// 返回当前线程id
=============================================

int pthread_equal(pthread_t t1, pthread_t t2);
// 比较两个线程id是否相等
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* callback(void* arg) {
    printf("child thread: %ld\n", pthread_self());  // 获取当前线程id
    return NULL;
}

int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);
    if (ret != 0) {
        char *errstr = strerror(ret);
        printf("%s\n", errstr);
    }
    // 主线程
    for (int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }
    printf("tid: %ld, main id: %ld\n", tid, pthread_self());

    pthread_exit(NULL);// 主线程退出,对子线程没有影响

    return 0;
}
0
1
2
3
4
tid: 140341352228608, main id: 140341360527168
child thread: 140341352228608

连接已终止的线程

和一个已经终止的线程进行连接,来回收子线程的资源

int pthread_join(pthread_t thread, void **retval);
// 阻塞函数,调用一次回收一个子线程
// 主线程调用,等待子线程退出并回收其资源,类似于进程中wait/waitpid
- thread: 需要回收的子线程id
- retval: 接受子线程退出的返回值
// 成功返回0,失败返回错误码
    + EDEADLK: 可能引起死锁
    + EINVAL: 目标线程是不可回收的
    + ESRCH: 目标线程不存在
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int val = 10;

void* callback(void* arg) {
    printf("child thread: %ld\n", pthread_self());  // 获取当前线程id
    pthread_exit((void *)&val); // return (void *)&val
}

int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);
    if (ret != 0) {
        char *errstr = strerror(ret);
        printf("%s\n", errstr);
    }
    // 主线程
    for (int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }
    printf("tid: %ld, main id: %ld\n", tid, pthread_self());

    // 主线程回收子线程的资源
    int *thread_retval;
    ret = pthread_join(tid, (void **)&thread_retval);  // 阻塞直至子线程结束
    if (ret != 0) {
        char *errstr = strerror(ret);
        printf("%s\n", errstr);
    }

    printf("return value: %d\n", *thread_retval);
    printf("recovery success\n");
    return 0;
}
0
1
2
3
4
tid: 140608466753280, main id: 140608475051840
child thread: 140608466753280
return value: 10
recovery success

线程分离

分离一个线程,不能多次分离,分离之后不能再连接
该线程结束退出后,系统会自动回收它的资源,不再需要pthread_join

int pthread_detach(pthread_t thread);
- thread: 要分离的线程id
// 成功返回0,失败返回错误号

线程取消

int pthread_cancel(pthread_t thread);
// 取消线程,让线程终止
// 不是立刻终止,而是当子线程执行到一个取消点才会终止
// 取消点:规定好的一些系统调用,例如
    + pthread_join/pthread_testcancel
    + pthread_cond_wait/pthread_cond_timewait
    + sem_wait/sig_wait
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

void* callback(void* arg) {
    printf("child thread id: %ld\n", pthread_self());
    for (int i = 0; i < 10; i++) {
        printf("child: %d\n", i);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);
    if (ret != 0) {
        char *errstr = strerror(ret);
        printf("error: %s\n", errstr);
    }

    // 取消线程
    pthread_cancel(tid);

    for (int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }

    // 输出主线程和子线程id
    printf("tid: %ld, main id: %ld\n", tid, pthread_self());

    pthread_exit(NULL);
    return 0;
}
child thread id: 140210217600768
child: 0
child: 1
child: 2
child: 3
child: 3
0
1
2
3
4
tid: 140210217600768, main id: 140210225899328

线程属性

int pthread_attr_init(pthread_attr_t *attr);
// 初始化线程属性变量
---
int pthread_attr_destroy(pthread_attr_t *attr);
// 释放线程属性的资源
---
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 设置线程分离的状态属性
---
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
// 获取线程分离的状态属性
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

void* callback(void* arg) {
    printf("child thread id: %ld\n", pthread_self());
    return NULL;
}

int main() {

    // 创建一个线程属性变量
    pthread_attr_t attr;
    // 初始化属性变量
    pthread_attr_init(&attr);
    // 设置属性
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    pthread_t tid;
    int ret = pthread_create(&tid, &attr, callback, NULL);
    if (ret != 0) {
        char *errstr = strerror(ret);
        printf("error: %s\n", errstr);
    }

    // 获取线程栈
    size_t size;
    pthread_attr_getstacksize(&attr, &size);
    printf("thread stack size: %ld\n", size);

    // 输出主线程和子线程id
    printf("tid: %ld, main id: %ld\n", tid, pthread_self());

    // 释放线程属性资源
    pthread_attr_destroy(&attr);

    pthread_exit(NULL);
    return 0;
}

线程同步与互斥

  • 线程的优势在于,能够通过全局变量共享信息,但是,必须确保多个线程不会同时修改同一变量,或者某个线程不会去读取正在由其他线程修改的变量

  • 临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作

  • 线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能操作

线程同步机制:信号量、互斥量和条件变量

互斥量

为避免线程更新共享变量时出现问题,可以使用互斥量(mutex, mutual exclusion)来确保同一时间只有一个线程可以访问共享资源

互斥量有两种状态:已锁定(locked)和未锁定(unlocked),任何时候,至多只有一个线程可以锁定该互斥量

一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁

  1. 针对共享资源锁定互斥量
  2. 访问共享资源
  3. 对互斥量解锁

操作系统负责线程调度,为了实现『锁的状态改变时再唤醒』就需要把锁也交给操作系统管理,所以互斥量的加锁操作通常都需要涉及到上下文切换,开销比自旋锁大

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 初始化互斥量
- mutex: 需要初始化的互斥量
- attr: 互斥量的属性,NULL
- restrict: 修饰符,被修饰的指针不能由另外的指针进行操作

int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 释放互斥量资源

int pthread_mutex_lock(pthread_mutex_t *mutex);
// 加锁,阻塞的,一个线程加锁,其他线程阻塞

int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 尝试加锁,如果失败,不会阻塞,直接返回,当互斥锁已经被加锁时,返回错误码EBUSY

int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 解锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 3个子线程卖100张票
int tickets = 1000;

// 创建互斥量
pthread_mutex_t mutex;

void* sellticket(void *arg) {

    while (1) {
        // 加锁
        pthread_mutex_lock(&mutex);
        if (tickets > 0) {
            usleep(5000);
            printf("%ld is selling %d ticket\n", pthread_self(), tickets);
            tickets--;
        }
        else {
            // 这里也要解锁,不然break之后就没解锁了
            pthread_mutex_unlock(&mutex);
            break;
        }
        // 解锁
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

int main() {
    // 初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);
    
    pthread_join(tid1, NULL);   // 阻塞,回收子线程的资源
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
    
    pthread_mutex_destroy(&mutex);  // 释放互斥量资源

    pthread_exit(NULL); // 退出主线程

    return 0;
}

读写锁

读写锁的特点:

  1. 如果有其他线程读数据,则允许,但不允许写操作
  2. 如果有线程写数据,则其他线程不能读数据、写数据
  3. 写是独占的,写的优先级高
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 初始化读写锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 加读锁

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 加写锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 解锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 释放读写锁
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int num = 1;
pthread_rwlock_t rwlock;    // 创建读写锁

void* writeNum(void *arg) {
    while (1) {
        pthread_rwlock_wrlock(&rwlock);     // 加写锁
        num++;
        printf("++write, tid: %ld, num: %d\n", pthread_self(), num);
        pthread_rwlock_unlock(&rwlock);     // 解写锁
        usleep(100);
    }
    return NULL;
}

void* readNum(void *arg) {
    while (1) {
        pthread_rwlock_rdlock(&rwlock);     // 加读锁
        printf("===read, tid: %ld, num: %d\n", pthread_self(), num);
        pthread_rwlock_unlock(&rwlock);     // 解读锁
        usleep(100);
    }
    return NULL;
}

int main() {
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    // 创建3个写线程,5个读线程
    pthread_t wtids[3], rtids[5];
    for (int i = 0; i < 3; i++) {
        pthread_create(&wtids[i], NULL, writeNum, NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_create(&rtids[i], NULL, readNum, NULL);
    }
    
    // 设置线程分离
    for (int i = 0; i < 3; i++) {
        pthread_detach(wtids[i]);
    }
    for (int i = 0; i < 5; i++) {
        pthread_detach(rtids[i]);
    }

    pthread_rwlock_destroy(&rwlock);    // 释放读写锁

    pthread_exit(NULL);
    
    return 0;
}
++write, tid: 140571381163776, num: 21902
++write, tid: 140571364378368, num: 21903
===read, tid: 140571339200256, num: 21903
++write, tid: 140571372771072, num: 21904
===read, tid: 140571347592960, num: 21904
===read, tid: 140571251304192, num: 21904
===read, tid: 140571259696896, num: 21904
===read, tid: 140571355985664, num: 21904

自旋锁(spin)

自旋锁属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,如果自旋锁已经被线程B所持有,那么线程A就会一直在core 0上进行忙等待并不停的进行锁请求,检查该自旋锁是否已经被线程B释放,直到得到这个锁为止。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁

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

  • 自旋锁一直占用CPU,在未获得锁的情况下,一直进行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,无疑会使CPU效率降低
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁

自旋锁适用于锁使用者保持锁时间比较短的情况


死锁

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去,此时系统处于死锁状态

死锁只有同时满足四个条件才会发生:

  1. 互斥条件
  2. 不可剥夺条件
  3. 持有并保持条件
  4. 环路等待条件

死锁的几个场景

  1. 忘记释放锁
  2. 重复加锁
  3. 多线程多锁,抢占锁资源

避免死锁问题只需要破坏其中一个条件就可以,最常见且可行的是使用资源有序分配法,来破坏环路等待条件,即线程总是以相同的顺序申请自己想要的资源


文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录