[linux]linux多线程详解(代码片段)

哦哦呵呵 哦哦呵呵     2022-12-13     320

关键词:

1. 线程概念

1.1 什么是线程

  在操作系统中,如果我们执行了某一应用程序,那么操作系统就会对这个应用程序创建一系列的资源以用来让这个程序在操作系统中运行起来。而整个创建过程以及创建成功后所产生的资源,我们将其称为一个进程。
  所以说,进程是操作系统分配资源的基本单位。而线程通俗来讲就是一个进程中一个执行流。这里以串行与并行下载文件举例,如果我们使用串行的方式去下载多个文件,那么得到的结果是,将这些文件逐个按个的下载,即上一个下载完成之后才会下载接下来的文件。如果使用并行的方式下载,那么这些文件就会一次同时下载多个文件,而不是等待上一个下载完后才继续下载接下来的,大大的提高了下载效率。
  通过上述例子,可以看出一个进程中可以同时执行多段程序代码片段,而这种同时有多个程序片段在执行就称为多线程。而其中的每一个执行流就被称为一个线程

1.2 从操作系统看线程

我们先来看一看进程是如何组织的。

线程又是如何组织的?

我们又称线程位轻量级进程(LWP),因为在linux内核中并没有描述线程的结构体,线程与进程都使用struct task_struct...,所以在线程中pid被称为线程号,tgid被称为线程组id,对标进程id。
线程是操作系统调度的基本单位,进程是操作系统分配资源的基本单位

轻量级线程在哪里体现轻量级?
  在创建进程时,需要对该进程分配一系列的资源,资源如上图所示,但是在创建线程时,就不需要开辟那些资源,与进程共用虚拟地址空间,大大减少了开销,但线程也有自身独立的空间线程号,栈,errno,信号屏蔽字,寄存器,调度优先级。共享空间有:文件描述符、信号处理方式、当前的工作目录、用户id与组id

1.3 线程的分类

线程分为主线程与工作线程。
主线程

  • pid = tgid
  • 一个进程中绝对有一个主线程

工作线程

  • 同一个进程中的线程的线程组是相同的,标识是同一个进程
  • 但pid不同,标识不同的进程

注意:多线程在工作执行时,也是抢占式执行的,所有当有很多进程同时在执行时,cpu采用时间片轮转的方式,轮流执行所有进程。

1.4 线程的优缺点

优点

  • 创建线程的代价要小于创建进程
  • 与进程切换相比,线程之间的切换需要操作系统做的工作很少,线程切换只需要交换上下文信息即可
  • 线程占用的资源比进程少很多
  • 可以充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,系统可以执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

缺点

  • 性能损失
    如果一个进程中的多个线程在频繁的进行切换,则程序的运行效率会降低,因为性能损失在了线程切换当中。进程的执行效率,随着线程数量的增多,性能呈现出正态分布的状况。
  • 代码健壮性降低
    一个线程的崩溃,会导致整个进程的崩溃
  • 缺乏访问控制
    多个线程在访问同一个变量时可能会导致程序结果的二义性

2. 线程控制

2.1 线程创建

函数接口

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
// 参数
//   tread: pthread_t线程标识符类型,返回线程ID,出参
//   attr: 线程属性,一般情况传递NULL,采用默认属性
//   start_routine: 线程的入口函数,线程创建完毕、启动后执行的函数
//   art: 传递给线程启动函数的参数

// 返回值: 成功返回0,失败返回1 

代码测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

struct ThreadNum

    int thread_num_;
;


void* MyThreadStrat(void* arg)

    struct ThreadNum* tn = (struct ThreadNum*)arg;
    while(1)
    
        printf("MyThreadStrat: %d\\n", tn->thread_num_);
        sleep(1);
    
    delete tn;
    return NULL;


int main()

    pthread_t tid;
    for(int i = 0; i < 2; i++)
    
        struct ThreadNum* tn = new ThreadNum;
        if(tn == NULL)
        
            exit(1);
        

        tn->thread_num_ = i;
        int ret = pthread_create(&tid, NULL, MyThreadStrat, (void*)tn);
        if(ret != 0)
        
            perror("pthread_create");
            return 0;
        
    

    while(1)
    
        printf("i am main thread\\n");
        sleep(1);
    
    return 0;


注意:

  • 线程入口函数的参数最好不传递临时变量,因为临时变量在创建完毕后会销毁,而线程中的arg指针就变成了野指针。尽量传递堆上开辟空间。
  • 线程入口函数的参数如果传递的为堆上开辟的空间,则释放时是在线程不去使用这块空间的条件下释放。
  • 线程入口函数的参数不仅可以传递内置类型还可以传递自定义类型。

2.2 线程终止

1. 线程入口函数的return返回,当前线程也进行了退出

2. 函数退出

函数一:
void pthread_exit(void* retval);
// 作用:谁调用谁退出
// 参数:线程在退出的时候返回的内容
函数二:
int pthread_canael(pthread_t thread);
// 作用: 退出thread线程,thread为线程描述符

pthread_t pthread_self(void);
// 作用: 谁调用返回谁的线程标识符

代码测试

#include <stdio.h>      
#include <stdlib.h>      
#include <unistd.h>      
#include <pthread.h>                                                                                                                     
                                                                                                                                         
void* MyThreadStrat(void* arg)      
      
	// 两种退出方法,退出则不会打印下方内容
    pthread_exit(NULL);                                                                                                                                                  
    pthread_cancel(pthread_self());  
                                                                                                                                                               
    printf("MyThreadStrat :%s\\n", (char*)arg);                                                                                                                 
                                                                                                                                                               
    return NULL;                                                                                                                                               
                                                                                                                                                              
                                                                                                                                                               
int main()                                                                                                                                                     
                                                                                                                                                              
    pthread_t tid;                                                                                                                                             
    for(int i = 0; i < 2; i++)                                                                                                                                 
                                                                                                                                                              
        int ret = pthread_create(&tid, NULL, MyThreadStrat, NULL);                                                                                             
        if(ret != 0)                                                                                                                                           
                                                                                                                                                              
            perror("pthread_create");                                                                                                                          
            return 0;                                                                                                                                          
                                                                                                                                                              
                                                                                                                                                              
                                                                                                                                                               
    while(1)                                                                                                                                                   
                                                                                                                                                              
        printf("i am main thread\\n");                                                                                                                          
        sleep(1);                                                                                                                                              
                                                                                                                                                              
    return 0;

注意:

默认创建线程时,线程的属性时joinable属性,joinable会导致线程在退出时,需要别人来回收自己的退出资源(即线程退出了,但是线程在共享区当中的空间还没有释放)。所以就需要线程等待或者线程分离,来解决当前问题。

2.3 线程等待

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。

int pthread_join(pthread_t thread, void **retval);
// 作用: 等待进程退出
// 参数:
//   thread: 线程标识符,想要等待哪一个线程退出
//   retval: 
//		1.如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
//		2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
//		3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
//		4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
// 返回值:成功返回0;失败返回错误码

代码测试

#include <stdio.h>    
#include <stdlib.h>    
#include <string.h>    
#include <unistd.h>    
#include <pthread.h>    
    
void *thread1(void *arg)    
    
    printf("thread 1 returning ... \\n");    
    int *p = (int*)malloc(sizeof(int));    
    *p = 1;    
    
    return (void*)p;    
    
    
void *thread2(void *arg)    
    
    printf("thread 2 exiting ...\\n");    
    int *p = (int*)malloc(sizeof(int));    
    *p = 2;    
    pthread_exit((void*)p);    
    
    
    
void *thread3(void *arg)    
    
    while ( 1)    
        
        printf("thread 3 is running ...\\n");    
        sleep(1);    
        
    
    return NULL;    
    
    
int main()    
    
    pthread_t tid;    
    void *ret;    
    
    // thread 1 return    
    pthread_create(&tid, NULL, thread1, NULL);    
    pthread_join(tid, &ret);    
 	printf("thread return, thread id %X, return code:%d\\n", tid, *(int*)ret);    
    free(ret);    
    
    // thread 2 exit    
    pthread_create(&tid, NULL, thread2, NULL);    
    pthread_join(tid, &ret);    
  	printf("thread return, thread id %X, return code:%d\\n", tid, *(int*)ret);    
    free(ret);    
    
    // thread 3 cancel by other    
    pthread_create(&tid, NULL, thread3, NULL);    
    sleep(3);    
    pthread_cancel(tid);    
    pthread_join(tid, &ret);    
    
    if ( ret == PTHREAD_CANCELED  )    
      printf("thread return, thread id %X, return code:PTHREAD_CANCELED\\n", tid);    
    else    
      printf("thread return, thread id %X, return code:NULL\\n", tid);    

	return 0;
     

现象

2.4 线程分离

一个线程被设置为分离属性,则该线程在退出之后,不需要其他执行流回收该进程的资源,而是由操作系统统一回收。

函数接口

int pthread_detach(pthread_t thread)
// 给thread线程设置分离属性,thread线程也可以是自己

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>


void* MyThreadStrat(void* arg)

    (void)arg;
    pthread_detach(pthread_self());
    
    while(1)
    
        printf("i am MyThreadStrat\\n");
        pthread_cancel(pthread_self());
        sleep(1);
    

    sleep(20);
    return NULL;


int main()

    pthread_t tid;

    int ret = pthread_create(&tid, NULL, MyThreadStrat, NULL);
    if(ret != 0)
    
        perror("pthread_create");
        return 0;
    

    while(1)
    
        printf("i am main thread\\n");
        sleep(1);
    
    return 0;

执行一遍 函数内部数据就进行退出,退出时不需要手动释放资源,而是由操作系统进行资源的释放。

3. 线程安全

3.1 线程不安全的现象

以下大致模拟了一个黄牛抢票的系统,使用4个线程同时去抢1000张票,我们的预期结果是,4个线程没人拿到的票都不相同,不会拿到同一张票。但是:

代码

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define THREADNUM 4

int g_val = 100;

void* buyTicket(void* arg)

    while (1)
    
        if (g_val > 0)
        
            printf("%p:have ticket %d\\n", pthread_self(), g_val);
            g_val--;
        
        else
        
            break;
        
    

    return NULL;


int main()

    pthread_t tid[THREADNUM];
    for (int i = 0; i < THREADNUM; i++)
    
        int ret = pthread_create(&tid[i], NULL, buyTicket, NULL);
        if (ret < 0)
        
            perror("pthread_create");
            return 0;
        
    

    for (int i = 0; i < THREADNUM; i++)
    
        pthread_join(tid[i], NULL);
    

    return 0;

如上就是我们说的线程不安全的现象

当多个线程访问同一个资源时,这个资源不能够保持原子性,就有可能发生结果错误。

总结

假设有一个cpu,两个线程A和B,线程AB都想对全局变量g_i++操作。假设线程A先运行,但是线程A将g_i读取到寄存器后,A的时间片使用完了被线程切换了。但是A没有对g_i的值修改完成,而是被线程B读取修改完成了,等到线程切换回来之后,线程A还是对原本的g_i修改,不是对线程B修改完后的值进行修改,两个线程都进行了++操作,但是结果不符合预期,所以造成了结果的错误。

3.1 如何解决–互斥锁

  上述黄牛抢票的问题,如果我们给上述的4个黄牛,只给一个特殊的令牌。只有抢到这个令牌之后,才有资格买票。但是买完票之后必须把令牌交出来,4个人再次公平竞争,抢到令牌的才可以去买票。这样就保证了每个人买的票都是唯一的,不会出现多人买一张票。我们将上述的令牌就叫做互斥锁

3.1.1 互斥锁原理

  互斥锁保证多个执行流在访问同一个临界资源时,其操作时原子性的。

名词解释

  • 执行流: 线程
  • 临界资源: 多个线程都能访问到的资源
  • 临界区: 访问临界资源的代码区被称为临界区
  • 原子操作: 要么执行流还没有开始执行临界区代码,要么已经执行完毕临界区代码

原理
  互斥锁的底层是一个互斥量,互斥量的本质时一个计数器,该计数器的取值只能为0或者1。0代表不能获取互斥锁,1标识可以获取互斥锁

加锁时原理

如何保证我们拿锁的这个过程是原子性操作?

  为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
  在加锁之前,申请一个寄存器,寄存器中放入0,直接入内存中的值进行交换,交换后寄存器值有两种结果: 寄存器中为1: 加锁成功;寄存器中为0: 加锁失败

解锁时原理
直接将寄存器中的值置为1,不关心内存的值,直接交换完成就是解锁的过程。

3.1.2 互斥锁接口

1. 初始化接口

1.动态初始化: 必须手动销毁否则内存泄露
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
// 参数:
//    pthread_mutex_t mutex: 互斥锁类型,传递一个互斥锁变量给该地址
// 	  attr: 一般传递NULL,采用默认属性

2.静态初始化: 系统自动回收
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 使用宏进行初始化

2. 销毁接口

int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 销毁指定互斥量

3. 加锁接口

阻塞加锁: 如果没加上锁就一直加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

非阻塞加锁: 需要搭配循环判断返回值
int pthread_mutex_trylock(pthread_mutex_t *mutex);

带有超时时间的加锁接口
int pthread_mutex_timelock(pthread_mutex_t* restrict mutex,
			const struct timspec* restrict abs_timeout);
// 参数:struct timspec  
//			time_t tv_sec;  // 秒
//			long tv_nesc:   // 纳秒
//      

注意: 加锁位置一定要放在访问临界资源之前

4. 解锁接口

以上三种加锁的方式,都可以使用该接口解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

注意: 在所有可能导致线程退出的地方进行解锁,否则可能造成死锁得情况

黄牛抢票改良

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define THREADNUM 4

int g_val = 100;
pthread_mutex_t g_lock;

void* buyTicket(void* arg)

    while (1)
    
        // 即将访问临界资源,加锁
        pthread_mutex_lock(&g_lock);

        if (g_val > 0)
        
            printf("%p:have ticket %d\\n", pthread_self(), g_val);
            g_val--;
        
        else
        
            // 可能会导致退出 解锁
            pthread_mutex_unlock(&g_lock);
            break;
        

        // 可能会导致退出 还锁
        // 如果不在此处进行解锁的操作,则本次循环结束后,还是会进行拿锁,但是锁在上方并没有还掉
        pthread_mutex_unlock(&g_lock);
    

    return NULL;


int linux内核线程kernelthread详解--linux进程的管理与调度(代码片段)

内核线程为什么需要内核线程Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。内核线程就是内核的分身,... 查看详情

万字详解linux系列多线程(上)(代码片段)

文章目录前言一、线程1.概念2.优点3.缺点4.线程异常二、进程与线程1.进程和线程2.进程的多个线程共享三、线程控制1.线程创建2.线程查看命令行查看用函数查看3.线程等待参数thread参数retval4.进程退出returnpthread_exitpthread_cancel四... 查看详情

万字详解linux系列多线程(下)(代码片段)

文章目录前言一、线程同步1.概念2.条件变量3.代码实现(1)相关函数(2)代码使用(3)关于pthread_cond_wait二、生产者消费者模型1.什么是生产者消费者模型2.相关概念(1)一个交易场所(2)... 查看详情

[linux]linux多线程详解(代码片段)

目录1.线程概念1.1什么是线程1.2从操作系统看线程1.3线程的分类1.4线程的优缺点2.线程控制2.1线程创建2.2线程终止2.3线程等待2.4线程分离3.线程安全3.1线程不安全的现象3.1如何解决--互斥锁3.1.1互斥锁原理3.1.2互斥锁接口3.2死锁3.2.1... 查看详情

linux多线程的创建等待终止(代码片段)

目录一、线程的概念1、页表详解1.1页表的举例1.2页表的真实表示形式2、进程、线程的区别2.1进程2.2线程(Linux中的线程被称为轻量级进程)3、使用POSIX标准的pthread原生线程库创建“线程”4、线程中共享和私有数据及读... 查看详情

linux多线程(代码片段)

线程一、Linux线程概念1、什么是线程2、原生线程库pthread3、线程的优点4、线程的缺点5、线程异常6、线程的用途二、Linux进程VS线程1、进程和线程2、进程的多个线程共享3、进程和线程的区别三、POSIX线程库1、线程创建2、线程等... 查看详情

python多线程linux命令(代码片段)

查看详情

python多线程linux命令(代码片段)

查看详情

linux多线程(代码片段)

linux多线程一.线程概念1.线程的优点2.线程的缺点3.线程之间的独有与共享:4.多进程与多线程二.线程控制1.线程的创建2.线程的终止3.线程的等待4.线程的分离三.线程安全概念实现互斥锁死锁同步的实现:条件变量生产者... 查看详情

linux多线程(代码片段)

文章目录前言一,线程1.1什么是线程1.2线程的优点1.3线程的缺点1.4线程异常1.5线程用途二,进程与线程对比三,Linux线程控制3.1POSIX线程库3.2进程ID和线程ID3.3线程终止3.4线程等待3.5分离线程四,linux线程互斥4.1互斥... 查看详情

linux多线程(代码片段)

文章目录前言一,线程1.1什么是线程1.2线程的优点1.3线程的缺点1.4线程异常1.5线程用途二,进程与线程对比三,Linux线程控制3.1POSIX线程库3.2进程ID和线程ID3.3线程终止3.4线程等待3.5分离线程四,linux线程互斥4.1互斥... 查看详情

linux多线程(代码片段)

文章目录前言一,线程1.1什么是线程1.2线程的优点1.3线程的缺点1.4线程异常1.5线程用途二,进程与线程对比三,Linux线程控制3.1POSIX线程库3.2进程ID和线程ID3.3线程终止3.4线程等待3.5分离线程四,linux线程互斥4.1互斥... 查看详情

linux内核——多任务内核程序head.s源码详解(代码片段)

....0)的第四章,最后一节的实验,多任务内核程序head.s源码详解#多任务内核程序[32]位的启动代码#包含32位模式下的初始化设置代码,时钟中断代码,系统调用中断代码和两个任务代码LATCH=11930SCRN_SEL=0x18#屏幕显示内存段选择符。#问:... 查看详情

多线程下的fork问题(模拟与解决)(代码片段)

目录前言1.浅谈在多线程下的fork的问题2.死锁问题的模拟实现3.解决办法前言有关进程、多线程、fork的概念,请看我之前写的这两篇文章。Linux:进程控制(进程创建、进程终止、进程等待、进程程序替换)Linux:详解多线程&#... 查看详情

linux提高:多线程压力测试(代码片段)

文章目录题目代码知识回顾线程线程特点题目创建一个多进程的程序,由用户输入进程个数和每个进程的运行圈数代码/*************************************************************************>FileName:main.c>Author:杨永利>Mail:1795018360@qq... 查看详情

linux提高:多线程压力测试(代码片段)

文章目录题目代码知识回顾线程线程特点题目创建一个多进程的程序,由用户输入进程个数和每个进程的运行圈数代码/*************************************************************************>FileName:main.c>Author:杨永利>Mail:1795018360@qq... 查看详情

linux---多线程线程池(代码片段)

多线程线程概念:线程就是进程中的一条执行流,负责代码的执行调度,在linux下线程执行流是通过pcb实现的,一个进程中可以有多个pcb,这些pcb共享进程中的大部分资源,所以线程被称为一个轻量级进程。Notes... 查看详情

linux----多线程(下)(代码片段)

多线程(下)2)线程控制①...②线程池Routine的static属性实现一个线程池,多线程处理任务(接收两数和一个操作符打印结果)③单例模式(线程安全)更改上面实现的线程池为懒汉模式④STL智能指... 查看详情