Linux —— 线程

2023-09-17 10:48:22

目录

一,线程概念

二,Linux进程与线程

三,Linux线程控制

创建线程

线程终止

线程等待

线程分离

linux线程互斥


一,线程概念

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

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

线程优点

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

线程缺点

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

线程异常

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

线程用途

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

二,Linux进程与线程

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

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

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

三,Linux线程控制

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以”pthread_“开头的;
  • 要使用这些函数库,引用头文件<pthread.h>;
  • 链接这些线程函数库,使用编译器命令”-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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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库中某个起始位置);

线程终止

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

  • 从线程函数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 <stdio.h>    
#include <stdlib.h>  
#include <unistd.h> 
#include <pthread.h> 

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 <stdio.h>    
#include <stdlib.h>  
#include <unistd.h> 
#include <pthread.h> 

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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
  • 代码必须要有互斥行为,当代码进入临界区执行时,不允许其他线程进入该临界区;
  • 如多个线程同时要求执行临界区的代码,并且临界区没有线程执行,那么只能允许一个线程进入临界区;
  • 如线程不再临界区中执行,那么该线程不能阻止其他线程进入临界区;

互斥量接口

初始互斥量

  • 方法一,静态分配
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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

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指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期;

可重入和线程安全

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

常见线程不安全情况

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

常见线程安全的情况

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

常见不可重入情况

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

常见可重入情况

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

可重入与线程安全联系

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

可重入与线程安全区别

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

更多推荐

【2023集创赛】加速科技杯作品:高光响应的二硫化铼光电探测器

本文为2023年第七届全国大学生集成电路创新创业大赛(“集创赛”)加速科技杯西北赛区二等奖作品分享,参加极术社区的【有奖征集】分享你的2023集创赛作品,秀出作品风采,分享2023集创赛作品扩大影响力,更有丰富电子礼品等你来领!团队介绍参赛单位:西北工业大学队伍名称:噜啦噜啦咧指导老师:李伟参赛队员:程琳,韩笑,尹天乐

苹果手机怎么录屏?1分钟轻松搞定

虽然一直使用苹果手机,但是对它的录屏功能还不是很会使用。苹果手机怎么录屏?录屏可以录制声音吗?麻烦大家教教我!苹果手机为用户提供了十分便捷的内置录屏功能,可以让您随时随地录制手机上的内容。但是很多小伙伴在第一次使用苹果手机时,找不到苹果手机的录屏工具在哪,所以不知道该如何进行录屏。那么,苹果手机怎么录屏呢?下面将给大家

python+nodejs+php+springboot+vue 学生选课程作业提交教学辅助管理系统

二、项目设计目标与原则1、关于课程作业管理系统的基本要求(1)功能要求:可以管理首页、个人中心、公告信息管理、班级管理、学生管理、教师管理、课程类型管理、课程信息管理、学生选课管理、作业布置管理、作业提交管理、作业评分管理、课程评价管理、课程资源管理等功能模块。(2)性能:在不同操作系统上均能无差错实现在不同类型的用户

[答疑]角色和状态的区别

DDD领域驱动设计批评文集“软件方法建模师”不再考查基础题《软件方法》各章合集jeri2023-9-1013:09设备关联角色,设备也有子类(车辆/设备),按书中的解释,设备是一个抽象类,角色类名像是带了状态名的类,如在使用的设备/在维护的设备,设备和这几个角色是关联关系,而且是0.1的关系,潘老师的观点是泛化关系还是

Linux下的网络编程——网络基础、socket编程(一)

前言:前面我们学习了Linux的系统编程,从今天我们就要开始Linux网络编程的学习了,Linux网络编程中的知识点可能没有前面的Linux系统多一点,但是基础的网络知识我们还是需要了解的,并且网络编程中的socket编程的知识也是相当重要的,那么现在我们就开始Linux网络编程的学习吧。目录一、协议1.7层模型和4层

使用Oracle实现完美的不重复随机数(oracle不重复随机数

使用Oracle实现完美的不重复随机数Oracle是一个功能强大的关系型数据库管理系统,它可以实现各种数据库操作和管理。在许多应用程序中,生成随机数是一个非常常见的任务。然而,许多时候,我们需要生成不重复的随机数,以确保数据的完整性和准确性。下面我们将介绍如何使用Oracle实现完美的不重复随机数。生成不重复随机数的方

Linux系统编程——进程间通信的学习

学习参考博文:进程间的五种通信方式介绍Linux信号介绍Linux系统编程学习相关博文Linux系统编程——文件编程的学习Linux系统编程——进程的学习Linux系统编程——线程的学习Linux系统编程——网络编程的学习Linux系统编程——进程间通信的学习一、概述1.无名管道和有名管道的区别2.当打开一个FIFO时

智能热水器语音控制丨打造智能家居新体验

随着科学技术的不断发展,智能电器越来越被大众所采纳,如智能扫地机,智能洗衣机,智能微波炉等等,越来越智能的电器为人们的生活带来了许多便利。以往的热水器一般都是只有按键/机械的控制方式,没有其他无线控制的控制方式。但现在新增了语音功能控制。用户通过语音控制智能热水器进行加热或保温等操作,无需用户手动控制;为人们带来了全新

电脑怎么录音,亲身测评,让你事半功倍!

“电脑怎么录音呀?最近学校的十大歌手比赛快开始了,需要自己录制一段音频线上参赛,通过的人才能参与线下的复赛,可是我的伴奏一直有杂音,就想问问大家,有没有比较好用的电脑录音方法呀?”在当今这个信息爆炸的时代,录音已经成为我们日常生活中不可或缺的一部分。无论是录制会议、讲座、课程还是简单的个人笔记,录音工具都能为我们带来极

从丢失msvcp140_codecvt_ids.dll到修复,解决方法详解

在日常计算机使用过程中,我们有时会遇到一些烦人的错误提示。其中之一就是丢失了msvcp140_codecvt_ids.dll文件。当我们打开某些应用程序或游戏时,可能会收到类似于“找不到msvcp140_codecvt_ids.dll”或“该应用程序无法正常启动”的错误信息。这可能会影响我们的工作效率或是影响我们游戏的

字符串函数----篇章(1)

目录补上章缺失的两道题七.笔试题(7)八.笔试题(8)一.字符串函数(1)----strlen函数二.字符串函数(2)----strcpy函数2-1模拟实现strcpy三.字符串函数(3)----strcmp函数​编辑3-1模拟实现strcmp四.字符串函数(4)----strcat函数​编辑4-1模拟实现strcat

热文推荐