[linux] linux多线程详解

Posted 哦哦呵呵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[linux] linux多线程详解相关的知识,希望对你有一定的参考价值。

目录

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 main()

    // 初始化互斥锁以上是关于[linux] linux多线程详解的主要内容,如果未能解决你的问题,请参考以下文章

windows 和 linux 多线程

[linux] linux多线程详解

[linux] linux多线程详解

万字详解Linux系列多线程

Linux多线程编程详解 [By: HarryAlex]

Linux从青铜到王者第十三篇:Linux多线程四万字详解