Linux —— 线程

目录

一,线程概念

二,Linux进程与线程

三,Linux线程控制

创建线程

线程终止

线程等待

线程分离

linux线程互斥


一,线程概念

        在一程序内,一个执行路线称为线程thread,即线程是一个进程内部的控制序列;

  • 一切进程至少都有一个执行线程;
  • 线程在进程内部运行,本质是在进程地址空间内运行;
  • 在Linux系统中,CPU看到的PCB都要比传统的进程更加轻量化;
  • 透过进程虚拟地址空间,可看到进程的大部分资源;将进程资源合理分配给每个执行流,就形成线程执行流;

Linux —— 线程_第1张图片

线程优点

  • 创建新线程的代价要比创建新进程小的多;
  • 与进程间的切换相比,线程间的切换需要操作系统所做的工作要少很多;
  • 线程占用资源比进程少很多;
  • 能充分利用多处理器的可并行数量;
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务;
  • 计算密集型应用,为了提高性能,将I/O操作重叠;线程可同时等待不同的I/O操作;

线程缺点

  • 性能损失,一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享一个处理器;如计算密集型线程的数量比可用的处理器多,可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变;
  • 健壮性降低,编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话即线程间缺乏保护;
  • 缺乏访问控制,进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响;
  • 编程难度提高,编写与调试一个多线程程序比单线程困难的多;

线程异常

  • 单个线程如出现除零,野指针等问题导致线程崩溃,进程也会随之崩溃;
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程;进程终止,该进程内的所有线程也会退出;

线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率;
  • 合理的使用多线程,能提高I/O密集型程序的用户体验;

二,Linux进程与线程

  • 进程是资源分配的基本单位;
  • 线程是调度的基本单位;
  • 线程共享进程数据,但也拥有自己的一部分数据;
    • 线程ID;
    • 一组寄存器(上下文数据);
    • 栈;
    • errno;
    • 信号屏蔽字;
    • 调度优先级;

        进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的;如定义一个函数,在各个线程中都可调用,如定义一个全局变量,在各线程都可访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符;
  • 每种信号的处理方式(SIG_IGN、SIG_DFL或自定义信号处理函数);
  • 当前工作目录;
  • 用户id和组id;

Linux —— 线程_第2张图片

三,Linux线程控制

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以”pthread_“开头的;
  • 要使用这些函数库,引用头文件
  • 链接这些线程函数库,使用编译器命令”-lpthread“选项;

线程创建

//创建新线程
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,失败返回错误码;
#include 
#include 
#include 
#include 
#include 

void* rout(void* arg){
    for( ; ; ){
        printf("I am thread1\n");
        sleep(1);
    }
}

int main(){
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, rout, NULL);
    if(ret != 0){
        fprintf(stderr, "pthread_create: %s\n", strerror(ret));
        exit(EXIT_FAILURE);
    }
    for( ; ; ){
        printf("I am main thread\n");
        sleep(1);
    }
}
[wz@192 Desktop]$ gcc -o test test.c -lpthread
[wz@192 Desktop]$ ldd test
	linux-vdso.so.1 =>  (0x00007ffce0bb2000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f9ab6dd8000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f9ab6a0a000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f9ab6ff4000)
[wz@192 Desktop]$ ./test
I am main thread
I am thread1
I am main thread
I am thread1
I am main thread
I am thread1
//一个进程,两个线程(轻量级进程)
[wz@192 ~]$ ps axj | head -1 && ps axj | grep test
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  2976  53194  53194   2976 pts/0     53194 Sl+   1000   0:00 ./test
  3351  53324  53323   3351 pts/1     53323 S+    1000   0:00 grep --color=auto test
[wz@192 ~]$ ps -aL | head -1 &&  ps -aL | grep test
   PID    LWP TTY          TIME CMD
 53194  53194 pts/0    00:00:00 test
 53194  53195 pts/0    00:00:00 test

线程ID及进程地址空间布局

  • pthread_create函数会产生一个线程ID,存放在第一参数所指向的地址;此线程ID与前面所说的线程ID不是一回事;此前所说的线程ID属于进程调度范畴,因线程是轻量级进程,是OS调度的最小单位,所以需一个数值来唯一标识该线程;
  • pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元地址即为新创建线程的线程ID,属于NPTL线程库范畴;线程库的后续操作,就是根据该线程ID来操作线程的;
  • 线程库NPTL提供了pthread_self函数,可获得线程自身ID;

        pthread_t类型是什么,取决于实现;对于Linux目前实现的NPTL,pthread_t类型的线程ID,本质上是一个进程地址空间的一个地址;

  • Linux没有真正意义上的线程,是用进程模拟的(轻量级进程);
  • Linux本身不会直接提供类似线程创建、终止、等待、分离等相关system call接口,但会提供创建轻量级进程的接口vfork;
  • 但用户需要所谓的线程创建、终止、等待、分离等相关接口,所以系统基于轻量级进程接口模拟封装了用户原生线程库pthread;
  • 进程由PCB管理的,用户层也需进行用户级线程管理(由用户空间维护);
  • 用户层线程ID,本质是一个地址(共享区,pthread库中某个起始位置);

Linux —— 线程_第3张图片

线程终止

如需终止某个线程而不是整个进程,有三种方法:

  • 从线程函数return,此方法对主线程不适用,从main函数return相当于调用exit;
  • 线程可调用pthread_exit终止自己;
  • 线程可调用pthread_cancel终止同一进程中的另一个线程;
void pthread_exit(void* value_ptr);
  • vaule_ptr,不要指向一个局部变量;
  • pthread_exit或return返回的指针指向的内存单元必须是全局或是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出;
int pthread_cancel(pthread_t thread);
  • 成功返回0,失败返回错误码;

线程等待

为何需要线程等待:

  • 已退出的线程,其空间没有被释放,仍然在进程的地址空间内;
  • 创建新的线程不会复用刚才退出线程的地址空间;
int pthread_join(pthread_t thread, void** value_ptr);
  • value_ptr,指向一个指针,然后在指向线程的返回值;
  • 成功返回0,失败返回错误码;

        调用该函数的线程将挂起等待,直到id为thread的线程终止;thread的线程以不同的方法终止,通过pthread_join得到的终止状态也是不同的:

  • 如thread线程通过return返回,value_ptr所指向的单元存放的时thread线程函数的返回值;
  • 如thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数;
  • 如thread线程被别的线程调用pthread_cancel异常终止的,value_ptr所指向的单元存放的是常数PTHREAD_CANCELED((void*)-1);
  • 如对thread线程的终止状态不感兴趣,可对value_ptr传NULL;

#include     
#include   
#include  
#include  

void* thread1(void* arg){
    printf("thread1 returning ...\n");
    int *p = (int*)malloc(sizeof(int));
    *p = 1;
    return (void*)p;
}

void* thread2(void* arg){
    printf("thread1 exiting ...\n");
    int *p = (int*)malloc(sizeof(int));
    *p = 2;
    pthread_exit((void*)p);
}

void* thread3(void* arg){
    while(1){
        printf("thread3 running ...\n");
        sleep(1);
    } 
    return NULL;
}

int main(){
    pthread_t tid;
    void* ret;
    //线程1,return
    pthread_create(&tid, NULL, thread1, NULL);
    pthread_join(tid, &ret);
    printf("thread1 return, thread id %x, return code: %d\n", tid, *(int*)ret);
    free(ret);
    //线程2,exit
    pthread_create(&tid, NULL, thread2, NULL);
    pthread_join(tid, &ret);
    printf("thread2 return, thread id %x, return code: %d\n", tid, *(int*)ret);
    free(ret);
    //线程3,cancel by other
    pthread_create(&tid, NULL, thread3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if(ret == PTHREAD_CANCELED)
        printf("thread3 return, thread id %x, return code: PTHREAD_CANCELED\n", tid);
    else
        printf("thread3 return, thread id %x, return code: NULL\n", tid);
}
[wz@192 Desktop]$ gcc -o test test.c -lpthread
[wz@192 Desktop]$ ./test 
thread1 returning ...
thread1 return, thread id 2d3f6700, return code: 1
thread1 exiting ...
thread2 return, thread id 2d3f6700, return code: 2
thread3 running ...
thread3 running ...
thread3 running ...
thread3 return, thread id 2d3f6700, return code: PTHREAD_CANCELED

线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄露;
  • 如不关心线程的返回值,join是一种负担,此时可告诉系统,当线程退出时,自动释放线程资源;
  • joinable与detach是冲突的,线程不可既是joinable又是detach;
int pthread_detach(pthread_t thread);
//可是线程组内其他线程对目标线程进行分离,也可是线程自己分离
pthread_detach(pthread_self());
#include     
#include   
#include  
#include  

void* thread_run(void* arg){
    pthread_detach(pthread_self());
    printf("%s\n", (char*)arg);
    return NULL;
}

int main(){
    pthread_t tid;
    if(pthread_create(&tid, NULL, thread_run, "thread run ...\n") != 0){
        printf("create thread error\n");
        return 1;
    }
    
    int ret = 0;
    sleep(1); //很重要,要让线程先分离,在等待
    
    if(pthread_join(tid, NULL) == 0){
        printf("pthread wait success\n");
        ret = 0;
    } else{
        printf("pthread wait failed\n");
        ret = 1;    
    }
    return ret;
}
[wz@192 Desktop]$ gcc -o test test.c -lpthread
[wz@192 Desktop]$ ./test
thread run ...

pthread wait failed

linux线程互斥

  • 临界资源,多线程执行流共享的资源叫做临界资源;
  • 临界区,每个线程内部,访问临界资源的代码叫做临界区;
  • 互斥,任何时候,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用;
  • 原子性,不会被任何调度机制打断的操作,该操作只有两态,要么完成要么未完成;

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间,这种情况,变量归属单个线程,其他线程无法获得这种变量;
  • 但有时候,很多变量都需要在线程间共享,称为共享变量,可通过数据的共享,完成线程间的交互;
  • 多个线程并发的操作共享变量,会带来一些问题;
#include 
#include 
#include 
#include 
#include 

int ticket = 100;

void* route(void* arg){
    char* id = (char*)arg;
    while(1){
        if(ticket > 0){
            usleep(1000);
            printf("%s sells ticket: %d\n", id, ticket);
            ticket--;
        }
        else break;   
    }
}

int main(){
    pthread_t t1, t2, t3, t4;
    
    pthread_create(&t1, NULL, route, "thread1");
    pthread_create(&t2, NULL, route, "thread2");
    pthread_create(&t3, NULL, route, "thread3");
    pthread_create(&t4, NULL, route, "thread4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}
[wz@192 Desktop]$ gcc -o test test.c -lpthread
[wz@192 Desktop]$ ./test
thread1 sells ticket: 100
thread3 sells ticket: 100
thread4 sells ticket: 98
thread2 sells ticket: 100
thread1 sells ticket: 96
thread4 sells ticket: 95
thread2 sells ticket: 94
...
thread3 sells ticket: 4
thread4 sells ticket: 2
thread1 sells ticket: 2
thread3 sells ticket: 0
thread2 sells ticket: 0
thread4 sells ticket: -2
  • 代码必须要有互斥行为,当代码进入临界区执行时,不允许其他线程进入该临界区;
  • 如多个线程同时要求执行临界区的代码,并且临界区没有线程执行,那么只能允许一个线程进入临界区;
  • 如线程不再临界区中执行,那么该线程不能阻止其他线程进入临界区;

Linux —— 线程_第4张图片

互斥量接口

初始互斥量

  • 方法一,静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 方法二,动态分配
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);

销毁互斥量

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁;
  • 不要销毁一个已经加锁的互斥量;
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁;
int pthread_mutex_destroy(pthread_mutex_t* mutex);

互斥量加锁/解锁

int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

调用pthread_lock时,可能会遇到以下问题:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;
  • 发起函数调用时,其他线程已锁定互斥量,或存在线程同时申请互斥量,但没有竞争到互斥量,那么调用会阻塞(执行流被挂起),等待互斥量解锁;
#include 
#include 
#include 
#include 
#include 

int ticket = 100;
pthread_mutex_t mutex;

void* route(void* arg){
    char* id = (char*)arg;
    while(1){
        pthread_mutex_lock(&mutex);
        if(ticket > 0){
            usleep(1000);
            printf("%s sells ticket: %d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }
        else{
            pthread_mutex_unlock(&mutex);   
            break;
        }   
    }
}

int main(){
    pthread_t t1, t2, t3, t4;
    
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, route, "thread1");
    pthread_create(&t2, NULL, route, "thread2");
    pthread_create(&t3, NULL, route, "thread3");
    pthread_create(&t4, NULL, route, "thread4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
}
[wz@192 Desktop]$ gcc -o test test.c -lpthread
[wz@192 Desktop]$ ./test
thread1 sells ticket: 100
thread1 sells ticket: 99
thread1 sells ticket: 98
thread1 sells ticket: 97
thread1 sells ticket: 96
...
thread1 sells ticket: 5
thread1 sells ticket: 4
thread1 sells ticket: 3
thread1 sells ticket: 2
thread1 sells ticket: 1

互斥量实现原理

  • 单纯的i++或++i都不是原子的,有可能会有数据一致性问题;
  • 为实现互斥锁操作,大多数体系结构都提供了swap、exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期;

Linux —— 线程_第5张图片

可重入和线程安全

  • 线程安全,多个线程并发同一段代码,不会出现不同的结果;常见对全局变量或静态变量进行操作,并且没有锁保护的情况下,会出现该问题;
  • 重入,同一函数被不同的执行流调用,当前一个流程还没执行完,就有其他的执行流再次进入,称为重入;一个函数在重入情况下,运行结果不会出现任何不同或任何问题,则该函数称为可重入函数,否则为不可重入函数;

常见线程不安全情况

  • 不保护共享变量的函数;
  • 函数状态随着被调用,状态发生变化的函数;
  • 返回指向静态变量指针的函数;
  • 调用线程不安全函数的函数;

常见线程安全的情况

  • 每个线程对全局变量或静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的;
  • 类或接口对于线程来说都是原子操作;
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性;

常见不可重入情况

  • 调用了malloc/free函数,因malloc函数是全局链表来管理堆的;
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构;
  • 可重入函数体内使用了静态的数据结构;

常见可重入情况

  • 不使用全局变量或静态变量;
  • 不使用malloc/new开辟的空间;
  • 不调用不可重入函数;
  • 不返回静态或全局数据,所有数据都由函数的调用者提供;
  • 使用本地数据,或通过制作全局数据的本地拷贝来保护全局数据;

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的;
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题;
  • 如一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入;

可重入与线程安全区别

  • 可重入函数是线程安全函数的一种;
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的;
  • 如将临界资源访问加锁,则这个函数是线程安全的,如这个重入函数锁还未释放则会产生死锁,因此是不可重入的;

你可能感兴趣的:(操作系统,Linux)