【ONE·Linux || 多线程(一)】

总言

  多线程:进程线程基本概念、线程控制、互斥与同步。


  
  
  

1、基本概念

1.1、补充知识

1.1.1、堆区细粒度划分

  问题:堆区里有很多申请到的小空间,那么如何知道哪块区域是一个整体?以及如何找到对应堆区申请的空间?
  
  回答:struct_vm_area_sturct结构体。每次在堆区申请空间,会生成这样一个结构体,vm_startvm_end能记录所申请空间的首尾位置。将这些结构体以双链表的形式链接起来,就可通过vm_nextvm_prev找到每个空间位置。

struct vm_area_struct {

	unsigned long vm_start;		// Our start address within vm_mm. 
	unsigned long vm_end;		// The first byte after our end address within vm_mm.
 
	// linked list of VM areas per task, sorted by address 
	struct vm_area_struct *vm_next, *vm_prev;

	//…………
	//其它内容
}

  
   说明:OS是可以做到让进程进行资源的细粒度划分
  
  
  

1.1.2、虚拟地址到物理空间的转化

  ①我们的可执行程序在编译阶段,就已经以4KB为单位按照虚拟地址的区域被划分。(页帧)
  ②物理内存也是以4KB为单位划分为一个个小块,并以struct page{ }结构体来管理。(页框)

Linux内核将整个物理内存按照页对齐方式划分成千上万个页进行管理。由于一个物理页用一个struct page表示,那么系统会有成千上万个struct page结构体,这些结构体也会占用实际的物理内存,因此,内核选择用union联合体来减少内存的使用。

  ③IO的基本单位是4KB。相当于把页帧装进页框里。
在这里插入图片描述

  
  
  
  

1.2、如何理解线程、进程

1.2.1、如何理解线程?

  在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
在这里插入图片描述

  说明:
  1、通过进程虚拟地址空间可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
  2、因此,线程在进程内部运行的(线程在进程地址空间内运行),是OS调度的基本单位(CPU进行调度时,不关心执行流是进程还是线程,只关心PCB)。
  3、一切进程至少都有一个执行线程。
  
  4、不同操作系统下(Linux、windows等),线程的实现方案不同(只要满足设定的条件规则即可)。上述图示的是Linux的线程方案,实际上Linux没有真正意义上的线程结构,是用进程PCB模拟的。也因此,Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口(Linux下,PCB<=其它OS的PCB,故将Linux进程称之为轻量级进程)。
  5、pthread线程库(Linux系统自带的原生线程库):在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用。(相当于省去一定的学习线程库实现的成本,只需要会调用该线程库即可)。
  
  
  
  

1.2.2、如何理解进程?

  用户视角:进程 = 内核数据结构(可存在多个PCB)+ 该进程对应的代码和数据。
  内核视角:进程是承担分配系统资源的基本实体。(进程向操作系统申请系统资源,此后线程的资源分配就由进程来执行,即OS角度,这些PCB、虚拟地址、页表等是以进程为单位申请的。)
  
  如何理解曾经我们所写的代码?
  以前我们所写的可执行程序,属于内部只有一个执行流的进程。引入线程后,可以有内部有多个执行流的进程。
  
  
  
  
  
  

1.3、实践操作

在这里插入图片描述
  

1.3.1、基本演示(线程创建)

  1)、相关函数介绍和使用说明
  man pthread_create:创建一个新的线程。

NAME
       pthread_create - create a new thread

SYNOPSIS
       #include <pthread.h>

       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

       Compile and link with -pthread.

  参数:

thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数(start_routine)的参数

  返回值:成功返回0;失败返回错误码。

RETURN VALUE
       On success, pthread_create() returns 0; on error, it returns an error number, and the contents
       of *thread are undefined.

  
  其它:由于调用的是操作系统库,因此 使用gcc/g++时需要加上选项表示对应库的名称:-lpthread PS:关于动静态库如何使用的相关细节说明见博文:Linux || 基础IO(二):动静态库

在这里插入图片描述

  
  
  
  2)、演示一:基本使用演示

       int sprintf(char *str, const char *format, ...);
       int snprintf(char *str, size_t size, const char *format, ...);

  相关代码如下:

#include<iostream>
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<string>
using namespace std;

void* threadRun(void * args)
{
    string name = (char*)args;//字符串首元素地址、赋值运算符
    while(true)
    {
        cout << name << " , pid:" << getpid() << "\n" << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid[5];//创建5个线程
    char threadname[64];//用于arg参数传递线程名称,以便区分
    for(int i = 0; i < 5; ++ i)
    {
        //snprintf每次都会向threadname数组中写入字符串。(thread-1、thread-2、thread-3、……)
        snprintf(threadname, sizeof(threadname) ,"%s-%d","thread",i);
        pthread_create(tid+i, nullptr, threadRun, (void*)threadname);
        sleep(1);//此处是缓解传参BUG
    }

    while(true)
    {
        cout << " main thread , pid:" << getpid() << endl;
        sleep(3);
    }
    return 0;
}

  以下为演示结果:

在这里插入图片描述
  
  相关说明:
  1、ps -aL:可查看线程。
  2、主线程和新线程运行顺序是不确定的,取决于调度器(和父子进程顺序不定一样)
  3、由上图可知,CPU调度时看的是LWP,而非PID。因为当有多个线程时,LWP唯一,但PID可以对应多个线程。(PS:对于单线程的进程,其LWP和PID一样,故CPU看的仍旧是LWP。)
  4、kill -9 PID :用于杀掉一个进程,需要注意,内部所有线程都被杀掉

 main thread , pid:28571
thread-1 , pid:28571

thread-0 , pid:28571

thread-3 , pid:28571

thread-2 , pid:28571

thread-4 , pid:28571

Killed   #kill -9 28571 杀掉进程,对于进程内所有线程都被杀掉
[wj@VM-4-3-centos T0927]$ 

  
  
  
  
  
  
  
  
  

1.3.2、线程如何看待进程内部的资源?

  进程是资源分配的基本单位,线程是调度的基本单位。

  1、线程共享进程数据,但也拥有自己的一部分数据,如:
    ①线程ID;
    ②一组寄存器
    ③(一般认为独自占用);
    ④errno错误码;
    ⑤信号屏蔽字;
    ⑥调度优先级。
    

  2、除了上述全局变量在各线程中都可以访问到,各线程还共享以下进程资源和环境:
    ①文件描述符表;
    ②每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数);
    ③当前工作目录;
    ④用户id和组id;
    ⑤代码区、全局数据区(已初始化/未初始化)、堆区、共享区

  
  
  
  
  

1.3.3、进程VS线程(调度层面上)

  1)、为什么说线程切换的成本更低?

  1、地址空间、页表不需要被切换。(假如调度的是另外的进程PCB,则上下文、页表、地址空间等都需要切换,故而比线程切换成本更高)。

  2、对于线程,CPU内部有L1~L3 cache(缓存),对内存的代码和数据根据局部性原理预读到CPU内部。(若是进程切换,cache会失效,新进程只能重新缓存)。
  
  
  
  
  
  
  
  
  

2、线程控制

  1)、总览
在这里插入图片描述
  
  2)、POSIX线程库使用说明
  1、与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头。
  2、要使用这些函数库,要通过引入头文<pthread.h>
  3、链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
  
  

2.1、线程创建

2.1.1、函数介绍

  pthread_create函数在上述小节中已经演示过,此处只做补充说明。

NAME
       pthread_create - create a new thread

SYNOPSIS
       # include <pthread.h>

       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

       Compile and link with -pthread.

  参数:

thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数(start_routine)的参数

  返回值:成功返回0;失败返回错误码

RETURN VALUE
       On success, pthread_create() returns 0; on error, it returns an error number, and the contents
       of *thread are undefined.

  
  
  

2.1.2、演示:线程异常

  演示代码如下:当创建线程成功,新线程执行对应的threadRoutine(参数start_routine)内部内容,主线程继续执行ptread_create后面的代码。

void* threadRoutine(void * args)
{
    while(true)
    {
        sleep(2);
        cout << "新线程: " << (char*)args << " , is runing. " << endl;
        
        int a = 10;
        a /= 0 ;//error
    }
}

int main()
{
    fflush(stdout);
    pthread_t tid; // 创建一个线程

    pthread_create(&tid, nullptr, threadRoutine, (void *)"new thread");

    while (true)
    {
        cout << " main thread , pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

  
  演示结果如下:
在这里插入图片描述

  线程异常说明:
  1、单个线程如果出现除零、野指针等问题导致线程崩溃,进程也会随着崩溃
  2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
  
  
  
  
  

2.2、线程等待

2.2.1、函数介绍

  1)、 为什么需要线程等待?
  ①资源回收:当一个线程执行完毕并退出时,它所占用的系统资源(如栈空间、线程ID等)并不会立即被自动回收。如果主线程或其他线程不等待该线程结束,那么这些资源可能无法得到及时的释放,导致资源浪费。 线程等待(join)操作可以确保一个线程等待另一个线程结束后再继续执行,从而允许系统回收已结束线程的资源。
  
  ②避免僵尸线程:在操作系统层面,如果一个进程中的线程没有正确退出或被清理,它们可能会变成类似于僵尸进程的状态,占用系统资源但不再执行任何有用的工作。线程等待有助于避免这种情况的发生,确保所有线程都得到正确的清理和退出。
  
  
  2)、函数介绍
  man pthread_join:等待线程结束。调用该函数的线程将挂起等待,直到id为thread的线程终止

NAME
       pthread_join - join with a terminated thread

SYNOPSIS
       #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);

       Compile and link with -pthread.

DESCRIPTION
       The  pthread_join()  function  waits for the thread specified by thread to terminate.  If that
       thread has already terminated, then pthread_join() returns immediately.  The thread  specified
       by thread must be joinable.

  参数:

thread:线程ID
retval:它指向一个指针,后者通常是线程thread运行结束后的返回值

  返回值:成功返回0;失败返回错误码

RETURN VALUE
       On success, pthread_join() returns 0; on error, it returns an error number.

  
  说明:thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。(宏:-13. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是pthread_exit传递的参数。
4. 如果对thread线程的终止状态不感兴趣,可以将retval设置为NULL

  
  
  

2.2.2、演示一:验证退出有序

  演示代码如下:

void* pthreadRoutine(void * args)
{
    cout << (char*)args << ": runing." << endl;// 提示:新线程运行

    int count = 5;// 执行相关操作
    do{
        cout << "new thread: " << count << endl;
        sleep(1);
    }while(count--);

    cout << "new thread: quit." << endl;// 提示:新线程退出
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
    cout << "main thread: create succeed." << endl;
    pthread_join(tid, nullptr);
    cout << "main thread: wait succeed, main quit." << endl;
    return 0;
}

  演示结果如下:
在这里插入图片描述

  
  可用脚本来观察:

while :; do ps -aL | head -1 && ps -aL | grep thread; sleep 1; done

在这里插入图片描述
  
  
  
  

2.2.3、演示二:线程返回值

  问题: 在上述演示代码中,线程执行函数void* pthreadRoutine(void * args)会返回一个(void*),该返回值是给谁?

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

  
  回答: 谁来等待,就给谁。 一般是给主线程,主线程可通过线程等待函数pthread_join的第二参数 void **retval来知道结果。

int pthread_join(pthread_t thread, void **retval);

  PS:这里retval属于输出型参数,其类型是void**,这是因为pthread_create的返回值类型是void*,要改变实参retval,那么需要传递其指针(这里是二级指针变量)。
  
  演示代码如下:

void* pthreadRoutine(void * args)
{
    cout << (char*)args << ": runing." << endl;

    int count = 5;
    do{
        cout << "new thread: " << count << endl;
        sleep(1);
    }while(count--);

    cout << "new thread: quit." << endl;
    return (void*)22;//注意点1:从整形转变为void*类型,相当于将地址数据为22处返回。
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
    cout << "main thread: create succeed." << endl;

    void* ret = nullptr;//注意点2:用于接收pthreadRoutine,新线程返回值

    pthread_join(tid, &ret);//注意点2:要想实参被修改,则需要传址,void*的地址类型为void**
    cout << "ret: " << ret << " , ret:" << (long long)ret <<endl;//注意点3:Linux下指针为8字节,故此处强转int类型(4字节)不适用(会出现截断问题)
    cout << "main thread: wait succeed, main quit." << endl;
    return 0;
}

  
  演示结果如下:
在这里插入图片描述

  
  
  
  

2.2.4、演示三:线程返回值2.0

  除了上述返回一个值外,线程的返回值具有可玩性,运用恰当可以做一些有意义的操作。 以下为一个代码举例,我们可以让新线程做一些运算,并将结果存储在堆中,或以其它方式返回给主线程。

  演示代码如下:虽然是新线程申请的动态空间,但堆区在线程间能够共享,所以主线程也能看到。

void* pthreadRoutine(void * args)
{
    cout << (char*)args << ": runing." << endl;

    int* data = new int[5];
    for(int count = 0 ; count < 5; ++count)
    {
        cout << "new thread: " << count << endl;
        data[count] = count;
        sleep(1);
    }

    cout << "new thread: quit." << endl;
    return (void*)data;//返回了堆上申请的空间(新线程)
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
    cout << "main thread: create succeed." << endl;

    int* ret = nullptr;//用于接收pthreadRoutine,新线程返回值

    pthread_join(tid, (void**)&ret);
    cout << "main thread: wait succeed." << endl;

    for(int i = 0; i < 5; ++i)
    {
        cout << ret[i] << ' ';
    }
    cout << endl;
    return 0;
}

  演示结果如下:
在这里插入图片描述

  
  
  
  

2.3、线程终止

2.3.1、exit( )终止进程

  说明:exit( )是用于终止进程的,调用它不仅仅当前线程会被终止,整个进程都会终止
  
  演示代码如下:

void* pthreadRoutine(void * args)
{
    cout << (char*)args << ": runing." << endl;

    int* data = new int[5];
    for(int count = 0 ; count < 5; ++count)
    {
        cout << "new thread: " << count << endl;
        data[count] = count;
        sleep(1);
    }
    cout<< "now, exit the new thread." << endl;
    exit(22);//使用exit终止新线程

    cout << "new thread: quit." << endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
    cout << "main thread: create succeed." << endl;

    pthread_join(tid, nullptr);//阻塞式等待
    cout << "main thread: wait succeed." << endl;
    while(true)
    {
        cout << " main still runing." << endl;
        sleep(1);
    }

    return 0;
}

  演示结果如下:
在这里插入图片描述
  
  
  
  

2.3.2、pthread_exit( )

  1)、函数介绍
  man pthread_exit:线程可以调用该函数终止自己。

NAME
       pthread_exit - terminate calling thread

SYNOPSIS
       #include <pthread.h>

       void pthread_exit(void *retval);

       Compile and link with -pthread.

DESCRIPTION
       The  pthread_exit() function terminates the calling thread and returns a value via
       retval that (if the thread is joinable) is available to another thread in the same
       process that calls pthread_join(3).

  参数:

retval:返回指针,用于存储退出线程的返回数据,注意不要指向一个局部变量。

  返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

RETURN VALUE
       This function does not return to the caller.

  
  
  
  2)、使用演示
  演示代码如下:根据之前所学,如果thread线程是调用pthread_exit终止的,retval参数所指向的单元将会传递给pthread_join

void* pthreadRoutine(void * args)
{
    cout << (char*)args << ": runing." << endl;

    int* data = new int[5];
    for(int count = 0 ; count < 5; ++count)
    {
        cout << "new thread: " << count << endl;
        data[count] = count;
        sleep(1);
    }
    cout<< "now, exit the new thread." << endl;
    pthread_exit((void*)11);//使用exit终止新线程

    cout << "new thread: quit." << endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
    cout << "main thread: create succeed." << endl;

    int* ret = nullptr;
    pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_exit的参数值
    cout << "main thread: wait succeed, the return value:" << (long long)ret << endl;
    while(true)
    {
        cout << " main still runing." << endl;
        sleep(1);
    }

    return 0;
}

  演示结果如下:
在这里插入图片描述

  
  
  
  
  

2.3.3、pthread_cancel( )

  1)、函数介绍
  man pthread_cancel:取消一个执行中的线程。

NAME
       pthread_cancel - send a cancellation request to a thread

SYNOPSIS
       #include <pthread.h>

       int pthread_cancel(pthread_t thread);

       Compile and link with -pthread.

DESCRIPTION
       The  pthread_cancel()  function sends a cancellation request to the thread thread.
       Whether and when the target thread reacts to the cancellation request  depends  on
       two  attributes that are under the control of that thread: its cancelability state
       and type.

  参数

thread:线程ID

  返回值:成功返回0,失败返回错误码

RETURN VALUE
       On  success, pthread_cancel() returns 0; on error, it returns a nonzero error number.

  
  
  
  2)、使用演示
  演示代码如下:

void* pthreadRoutine(void * args)
{
    cout << (char*)args << ": runing." << endl;
    size_t count = 0;
    while(true)//让新线程一直循环运行
    {
        cout << "new thread: " << count++ << endl;
        sleep(1);
    }

    cout << "new thread: quit." << endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");
    cout << "main thread: create succeed." << endl;

    sleep(6);//让新线程运行6s后,取消新线程。
    pthread_cancel(tid);

    int* ret = nullptr;
    pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_cancel的返回值
    cout << "main thread: wait succeed, the return value:" << (long long)ret << endl;

    int count = 5;//让主线程在新线程退出后再运行一段时间
    while(count--)
    {
        cout << " main thread: runing." << endl;
        sleep(1);
    }
    cout << "main quit." << endl;
    return 0;
}

  演示结果如下:
在这里插入图片描述

  PS:不要在随意位置使用终止函数,按照场景需求正常使用即可。一般是主线程中使用,取消新线程;虽然没说不可以在新线程中取消主线程,但有可能会引起一些奇怪问题。
  
  
  
  
  
  
  

2.4、线程ID探索:pthread_self( )

  1)、问题引入
  此处使用2.3.3中演示代码,对main函数稍加修改,打印tid值:

    pthread_cancel(tid);

    int* ret = nullptr;
    pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_cancel的返回值
    cout << "main thread: wait succeed, the return value:" << (long long)ret <<" ,tid:" << tid << endl;

  演示结果如下,tid:140604466824960,是一个很大的数字。这似乎与我们之前学习到的进程ID,文件描述符fd等都不同,且也并非我们用ps -aL 指令查看到的LWP值。

[wj@VM-4-3-centos T0927]$ ./thread.out 
main thread: create succeed.
new thread: runing.
new thread: 0
new thread: 1
new thread: 2
new thread: 3
new thread: 4
new thread: 5
main thread: wait succeed, the return value:-1 ,tid:140604466824960
 main thread: runing.
 main thread: runing.
 main thread: runing.
 main thread: runing.
 main thread: runing.
main quit.
[wj@VM-4-3-centos T0927]$ 

  那么,这里的 线程ID究竟什么
  
  
  
  2)、解释说明
  结论:对于Linux目前实现的实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
在这里插入图片描述

  主线程用的是虚拟地址的栈结构,新线程用的是库里提供的私有栈结构。
  
  pthread_self()可以获得线程自身的ID,在哪个线程中使用,获取的就是哪个线程的ID。
  演示代码如下:

void* pthreadRoutine(void * args)
{
    size_t count = 3;
    while(count--)    
    {
       cout << (char*)args << ": runing." << "  tid:" << pthread_self() << endl;
       sleep(1);
    }
    cout << "new thread: quit." << endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");//创建新线程

    pthread_join(tid, nullptr);//阻塞式等待新线程

    int count = 3;//让主线程在新线程退出后再运行一段时间
    while(count--)
    {
        cout << "main thread: runing."  "  tid:" << pthread_self() << endl;
        sleep(1);
    }
    cout << "main quit." << endl;
    return 0;
}

  演示结果如下:

[wj@VM-4-3-centos T0927]$ make
g++ -o thread.out mythread.cc -std=c++11 -lpthread
[wj@VM-4-3-centos T0927]$ ls
makefile  mythread.cc  thread.out
[wj@VM-4-3-centos T0927]$ ./thread.out 
new thread: runing.  tid:140510573528832
new thread: runing.  tid:140510573528832
new thread: runing.  tid:140510573528832
new thread: quit.
main thread: runing.  tid:140510591952704
main thread: runing.  tid:140510591952704
main thread: runing.  tid:140510591952704
main quit.
[wj@VM-4-3-centos T0927]$ 

  
  
  
  
  
  
  

2.5、其它验证

2.5.1、验证全局区的数据能被多线程共享(__thread介绍)

  1)、相关演示
  演示代码:

int val = 0;

void* Routine(void* args)
{
    while(true)
    {
        //cout << "new  thread," << ", val:" << val << ", &val:" << &val << endl;
        printf("new  thread, val:%d, &val:%p\n",val,&val);
        val++;
        sleep(1);
    }
}

int main()
{
    pthread_t tid = 0;
    pthread_create(&tid, nullptr, Routine, (void*)"new thread");

    while(true)
    {
        //cout << "main thread" << ", val:" << val << ", &val:" << &val << endl;
        printf("main thread, val:%d, &val:%p\n",val,&val);
        sleep(1);
    }
    pthread_join(tid ,nullptr);

    return 0;
}

  演示结果:
在这里插入图片描述
  
  
  2)、若想让全局变量不被共享,如何操作?

  __thread修饰全局变量,可以让该全局变量被每一个线程独自占有(线程的局部存储)

  注意这里的的__是两个_。使用如下:__thread int val = 0;
在这里插入图片描述

  
  
  
  

2.5.2、如果在线程中使用了execl系列进程替换函数,会发生什么?

  演示代码:

void* Routine(void* args)
{
    while(true)
    {
        sleep(3);
        execl("/bin/ls","ls",nullptr);//进程替换


        printf("new thread,tid:%u\n",pthread_self());
        sleep(1);
    }
}

int main()
{
    pthread_t tid = 0;
    pthread_create(&tid, nullptr, Routine, (void*)"new thread");

    while(true)
    {
        printf("main thread,tid:%u\n",pthread_self());
        sleep(1);
    }
    pthread_join(tid ,nullptr);

    return 0;
}

  
  演示结果如下:在多线程环境中,execl系列函数尽管是在线程上下文中调用的,但由于进程替换的性质,整个进程(包括其所有线程)都将被替换为新的程序映像。因此,原有的线程和线程相关的资源(如线程栈)都将被释放。

在这里插入图片描述
  
  需要注意,语言级别的线程(如C++中也提供了线程),无论再怎么支持,其底层还是使用的是原生系统的线程库
  
  
  
  
  
  
  
  
  
  

2.6、线程分离

2.6.1、基本介绍

  1)、函数介绍
  man pthread_detach:默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时自动释放线程资源。  

NAME
       pthread_detach - detach a thread

SYNOPSIS
       #include <pthread.h>

       int pthread_detach(pthread_t thread);

       Compile and link with -pthread.

DESCRIPTION
       The  pthread_detach()  function  marks  the thread identified by thread as detached.  When a detached
       thread terminates, its resources are automatically released back to the system without the  need  for
       another thread to join with the terminated thread.

       Attempting to detach an already detached thread results in unspecified behavior.

RETURN VALUE
       On success, pthread_detach() returns 0; on error, it returns an error number.

  1、可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
  2、在POSIX线程(pthreads)编程中,线程分离和线程等待是冲突的。 一个线程要么是可连接的(joinable),要么是分离的(detached)。一个线程不能同时处于这两种状态。
  
  
  
  

2.6.2、相关演示

  1)、演示一
  演示代码如下:

void* Routine(void* args)
{
    //可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
    pthread_detach(pthread_self());

    int count = 5;
    while(count--)
    {
        printf("new thread,tid:%u, count:%d\n",pthread_self(),count);
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid = 0;
    pthread_create(&tid, nullptr, Routine, (void*)"new thread");

    int count = 5;
    while(count--)
    {
        printf("main thread, tid:%u, count:%d\n",pthread_self(),count);
        sleep(1);
    }
    cout <<"thr result:" << strerror(pthread_join(tid ,nullptr)) << endl;

    return 0;
}

  演示结果如下:
在这里插入图片描述

  
  
  
  2)、演示二:线程分离,若该线程异常,是否会影响主线程?
  回答:会,同一个进程中,资源还是共享的。
在这里插入图片描述
  
  
  
  
  
  
  
  
  
  
  
  

3、线程互斥与同步

3.1、线程互斥

3.1.1、问题引入与概念介绍

  1)、问题引入:不加保护的情况下,多线程抢票逻辑
  问题说明:如果多线程访问同一个全局变量,并对它进行数据计算,多线程会相互影响吗?
  
  演示代码:

int tickets = 1000;

void* getTickets(void* args)
{
    (void)args;
    while(true)
    {
        if(tickets > 0 )
        {
            usleep(1000);//休眠
            printf("%p: %d\n", pthread_self(), tickets--);
        }
        else break;
    }
    return nullptr;
}

int main()
{
    pthread_t tid1, tid2, tid3;//一次创建多个线程
    pthread_create(&tid1, nullptr, getTickets, nullptr);
    pthread_create(&tid2, nullptr, getTickets, nullptr);
    pthread_create(&tid3, nullptr, getTickets, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);

    return 0;
}

  演示结果:根据上述代码,printf只会打印出tickets > 0的数,可运行程序我们发现最后总会有ticket== -1。为什么会出现此现象?

在这里插入图片描述

  
  
  2)、原因解释1.0
  分析上述代码,tickets>0是逻辑运算,会在CPU中进行,同理,tickets--也是在CPU中进行的。这就需要将物理地址中存储的tickets变量载入进程(当前执行流)的上下文,在线程被调度时CPU执行计算操作。
  但需要注意的是,tickets--实则为三步操作:①读取数据到CPU寄存器;②CPU内部进行数据计算;③将结果写回内存。 若在此期间,线程因为CPU调度被切换,那么对于tickets的相关操作,无论执行到哪一步骤,都会随PCB上下文被切换走,直到下次再被调度。
   由此,在不加保护的情况下,多个线程对同一数据不具有实时同步性,会导致并发访问时数据不一致
在这里插入图片描述

  
  
  3)、一些概念
  临界资源: 多线程执行流共享的资源就叫做临界资源。
  临界区: 每个线程内部,访问临界资源的代码,就叫做临界区(实际还有很大一部分代码段属于普通代码)。
  互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
  
  
  
  
  
  
  
  

3.1.2、互斥锁

  针对上述问题,一个避免方法是加锁保护。
在这里插入图片描述

  
  

3.1.2.1、相关涉及函数

  1、对锁初始化(初始化互斥量):

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

       mutex:要初始化的互斥量
       attr:NULL
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  
  2、加锁、解锁

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

  
  3、销毁

       int pthread_mutex_destroy(pthread_mutex_t *mutex);

  ①使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  ②不要销毁一个已经加锁的互斥量
  ③已经销毁的互斥量,要确保后面不会有线程再尝试加锁
  
  
  
  

3.1.2.2、使用一:静态、全局方式

  1)、代码演示1.0
  如下:

int tickets = 1000;

//1、定义一个锁并对其初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* getTickets(void* args)
{
    (void)args;
    while(true)
    {
        //2、加锁:
        pthread_mutex_lock(&mutex);
        if(tickets > 0 )
        {
            usleep(1000);//休眠
            printf("%p: %d\n", pthread_self(), tickets--);
            pthread_mutex_unlock(&mutex);//3、解锁
        }
        else 
        {
            pthread_mutex_unlock(&mutex);//3、解锁
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t tid1, tid2, tid3;//一次创建多个线程
    pthread_create(&tid1, nullptr, getTickets, nullptr);
    pthread_create(&tid2, nullptr, getTickets, nullptr);
    pthread_create(&tid3, nullptr, getTickets, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);

    return 0;
}

  演示结果:
在这里插入图片描述

  
  解释说明:
  1、if…else语句中,break退出前需要解锁:
在这里插入图片描述

  
  2、能明显看出与不加锁时相比,运行速度变慢。(可以获取一个时间来验证,time、gettimeofday)。
在这里插入图片描述
  
  
  3、可加入随机数,让持锁进程更加随机。(实则我们演示时多个线程都有参与)

//int main()
    srand((unsigned int)time(nullptr)^getpid());//种子:用于让持锁线程更加随机

//void* getTickets(void* args)
	usleep(rand() % 1500);//休眠:让休眠随机一点

  
  
  4、加锁时容易影响效率,为了保证加锁粒度,加锁区域越小越好
  
  
  
  

3.1.2.3、使用二:动态、局部方式
int tickets = 10000;
#define THREAD_NUM 5 //待创建线程数目


class ThreadData // 用于创建线程时,args传参:线程名、锁
{
public:
    ThreadData(const string &name, pthread_mutex_t *pmutex)
        : _name(name), _pmutex(pmutex)
    {}

    string _name;
    pthread_mutex_t* _pmutex;
};


void *getTickets(void *args)
{
    ThreadData* td = (ThreadData*)args;
    while(true)
    {
        //3、加锁:
        pthread_mutex_lock(td->_pmutex);
        if(tickets > 0 )
        {
            usleep(rand() % 1500);//休眠:让休眠随机一点
            printf("%s: %d\n", td->_name.c_str(), tickets--);
            pthread_mutex_unlock(td->_pmutex);//3、解锁
        }
        else 
        {
            pthread_mutex_unlock(td->_pmutex);//3、解锁
            break;
        }
    }
    delete td;//销毁new出来的空间
    return nullptr;
}



int main()
{
    srand((unsigned int)time(nullptr) ^ getpid()); // 种子:用于让持锁线程更加随机
    clock_t t1 = clock();                          // 测试时间

    pthread_mutex_t mutex;                         // 1、定义一个锁
    pthread_mutex_init(&mutex, nullptr);           // 2、对锁初始化

    // 创建线程
    pthread_t tid[THREAD_NUM]; // 线程ID
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        string name = "thread";        // 线程名
        name += std::to_string(i + 1); // 线程名
        ThreadData *td = new ThreadData(name, &mutex);
        pthread_create(tid + i, nullptr, getTickets, (void *)td);
    }

    // 等待线程
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        pthread_join(tid[i], nullptr);
    }

    //4、销毁锁
    pthread_mutex_destroy(&mutex);

    clock_t t2 = clock();
    cout << "time: " << (t2 - t1) << endl;

    return 0;
}

  
  
  
  
  

3.1.2.4、问题说明(由实践到理论理解)

  1)、加锁之后,线程在临界区中是否切换,是否会有问题?
  回答:会切换,但不会有问题。
  第一次理解:当前线程虽然被切换了,但其是持有锁被切换的。而其他抢票线程要执行临界区代码,也必须先申请锁,此时锁无法申请成功的,所以,也不会让其他线程进入临界区,由此保证了临界区中数据一致性!
  
  
  2)、原子性体现?
  回答:设线程1持有锁,在未持有锁的线程看来,对其最有意义的情况只有两种:1. 线程1没有持有锁(什么都没做) 2. 线程1释放锁(做完),此时我可以申请锁。
  
  
  
  3)、加锁就是串行执行了吗?
  回答:是的,执行临界区代码一定是串行的。
  
  
  
  

3.1.3、锁的原理

  1)、问题引入
  要访问临界资源,每一个线程都必须先申请锁,每一个线程都必须先看到同把一锁并能够访问它。这就味着锁本身就是一种共享资源。所以,为了保证锁的安全,申请和释放锁,必须是原子的。
  
  那么,谁来保证?如何保证?锁是如何实现的?
  
  
  
  2)、原理解释
  为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换。在汇编角度,若只有一条汇编语句,则认为该汇编指令是原子性。

  即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。
在这里插入图片描述
  PS:实际底层还有过多概念原理,这里只是一层理解。
  
  
  1、谁来保证锁的安全?
  回答:锁自身。加锁、解锁这步动作都只涉及一行汇编。
  
  
  
  

3.1.4、死锁

  1)、概念
  死锁是指在一组进程中的各个进程均占有不会释放的资源,因互相申请被其他进程所站用的不会释放的资源,使得彼此处于一种永久等待的状态。
在这里插入图片描述
  
  以下为一种线程自己把自己弄成死锁的场景举例:

void *getTickets(void *args)
{
    ThreadData* td = (ThreadData*)args;
    while(true)
    {
        //此处已经加锁
        pthread_mutex_lock(td->_pmutex);
        if(tickets > 0 )
        {
            usleep(rand() % 1500);
            printf("%s: %d\n", td->_name.c_str(), tickets--);
            pthread_mutex_lock(td->_pmutex);//在加锁后,尚未解锁前,再次加锁,那么即使是线程本身,也是申请失败的。
            pthread_mutex_unlock(td->_pmutex);//解锁
        }
        else 
        {
            pthread_mutex_unlock(td->_pmutex);//解锁
            break;
        }
    }
    delete td;//销毁new出来的空间
    return nullptr;
}

在这里插入图片描述
  
  
  
  2)、死锁的必要条件
  互斥条件:一个资源每次只能被一个执行流使用。(产生死锁,正是因为牵扯到互斥)
  请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(类似于吃着碗里的还看着锅里的)
  不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
  PS:只要产生死锁,必然是这四个条件都被满足。反之,若破坏其中某个条件,就无法达成死锁。
  
  
  
  3)、如何避免死锁
  破坏死锁的四个必要条件
  加锁顺序一致
  避免锁未释放的场景
  资源一次性分配
  
  
  
  

3.1.5、可重入VS线程安全

  1)、概念
  线程安全:多个线程并发同一段代码时,不会出现不同的结果,则说明线程是安全的。常见对全局变量或者静态变量进行操作时,在没有锁保护的情况下会出现并发问题。

  重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,称为不可重入函数。
  
  
  
  2)、常见的线程不安全和线程安全的情况
  
  线程不安全:

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

  
  
  线程安全:

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

  
  
  
  3)、常见的可重入和不可重入的情况
  
  不可重入:

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

  
  
  可重入:

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

  
  
  
  4)、联系与区别
  联系:

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

  
  
  区别:

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

  
  
  
  
  
  

3.2、线程同步

3.2.1、问题引入与概念介绍

  1)、引入:上述互斥锁是否存在什么问题?
  回答:存在以下两种不合理的行为(虽然没有错误,但不合适)
  1、在拥有资源时,某个线程频繁的申请到资源,导致其它线程处于"饥饿"状态(长时间得不到资源)。
  2、在资源短缺时,某个线程频繁申请失败,浪费彼此时间。
  
  为了解决上述访问临界资源合理性的问题,我们引入同步的概念。
  
  
  
  12)、什么叫做同步?
  说明:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步(线程同步)。
  
  那么,如何实现同步?
  
  

3.2.2、方案一:条件变量

3.2.2.1、方案说明与函数介绍

  1)、在使用条件变量前的一些理解说明
  申请临界资源前,要先对临界资源是否存在做出检测,而检测本身也是在访问临界资源,也需要对其进行加锁解锁。常规方式下, 线程检测条件是否就绪,就需要频繁申请和释放锁。(此时若临界资源不就绪,线程申请锁失败,相当于其在频繁地加锁解锁做无意义的耗时行为)

void *getTickets(void *args)
{
    ThreadData* td = (ThreadData*)args;
    while(true)
    {
        pthread_mutex_lock(td->_pmutex);//加锁
        if(tickets > 0 )//如此处,在申请临界资源前,先对临界资源是否存在做了检测。而该检测是在加锁之后进行的。
        {
            usleep(rand() % 1500);
            printf("%s: %d\n", td->_name.c_str(), tickets--);
            pthread_mutex_unlock(td->_pmutex);//解锁
        }
        else 
        {
            pthread_mutex_unlock(td->_pmutex);//解锁
            break;
        }
    }
    delete td;
    return nullptr;
}

  
  考虑到此,我们设置出方案,让线程在(首次)检测到资源不就绪时,①不再频繁地重复进行资源检测,而是处于等待状态;②当资源就绪时,能接收到相应通知,随后再进行资源申请和访问。
  
  
  

  2)、条件变量涉及函数 在这里插入图片描述
  
  
  
  

3.2.2.2、方案演示1.0

  
   演示代码:

#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
using namespace std;


#define TNUM 4
volatile bool quit = false;//用于让线程结束循环,退出

typedef void(*func_t)(const string& name, pthread_mutex_t* pmx, pthread_cond_t* pcd);//函数指针

class ThreadData//用于create线程是,args传参
{
public:
    ThreadData(const string& name, func_t func, pthread_mutex_t* pmx, pthread_cond_t* pcd)
    :_name(name),_func(func),_pmx(pmx),_pcd(pcd)
    {}

    string _name;//线程名
    func_t _func;//函数指针
    pthread_mutex_t* _pmx;//锁
    pthread_cond_t* _pcd;//条件变量
};

void func1(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{
    while (!quit)
    {
        pthread_mutex_lock(pmx); // 加锁
        // if(满足条件)(访问临界资源)
        // else(不满足条件,等待临界资源就绪)
        pthread_cond_wait(pcd, pmx);
        cout << name.c_str() << " is running, action : F1--帮助键" << endl;
        cout << endl;
        pthread_mutex_unlock(pmx); // 解锁
    }
}

void func2(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{

    while (!quit)
    {
        pthread_mutex_lock(pmx); // 加锁
        // if(满足条件)(访问临界资源)
        // else(不满足条件,等待临界资源就绪)
        pthread_cond_wait(pcd, pmx);
        cout << name.c_str() << " is running, action : F2--重命名" << endl;
        cout << endl;
        pthread_mutex_unlock(pmx); // 解锁
    }
}

void func3(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{

    while (!quit)
    {
        pthread_mutex_lock(pmx); // 加锁
        // if(满足条件)(访问临界资源)
        // else(不满足条件,等待临界资源就绪)
        pthread_cond_wait(pcd, pmx);
        cout << name.c_str() << " is running, action : F3--搜索按钮" << endl;
        cout << endl;
        pthread_mutex_unlock(pmx); // 解锁
    }
}

void func4(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{
    while (!quit)
    {
        pthread_mutex_lock(pmx); // 加锁
        // if(满足条件)(访问临界资源)
        // else(不满足条件,等待临界资源就绪)
        pthread_cond_wait(pcd, pmx);
        cout << name.c_str() << " is running, action : F4--浏览器网址列表" << endl;
        cout << endl;
        pthread_mutex_unlock(pmx); // 解锁
    }
}

void* Entry(void* args)
{
    //所有新线程都会执行Entry函数,在Entry函数中每个线程又会执行其对应的func
    ThreadData* td = (ThreadData*) args;
    td->_func(td->_name, td->_pmx, td->_pcd);
    sleep(1);
    cout << td->_name.c_str() <<": " <<pthread_self() << endl;
    delete td;//新线程有自己独立的栈结构,每一个td变量都在各自私有栈中保存,最后新线程运行结束时,记得释放掉申请出来的堆
    return nullptr;
}

int main()
{
    //创建并初始化锁、条件变量
    pthread_cond_t cond;
    pthread_mutex_t mutex;
    pthread_cond_init(&cond, nullptr);
    pthread_mutex_init(&mutex, nullptr);

    //创建新线程
    pthread_t tid[TNUM];
    func_t funcs[TNUM] = {func1, func2, func3, func4};
    for(int i = 0; i < TNUM; ++i)
    {
        string name = "thread";
        name += to_string(i);
        ThreadData* td = new ThreadData(name, funcs[i], &mutex, &cond);
        pthread_create(tid+i, nullptr, Entry, (void*)td);
    }

    //主线程:唤醒新线程
    int count = 10;//执行10s退出
    while(count--)
    {   
        cout << "awake thread: " << endl;
        pthread_cond_signal(&cond);//任意唤醒一个线程:并不关心具体是哪一个
        sleep(1);
    }
    
    quit = true;//此时func函数不满足条件,线程回到Entry
    cout << endl << "quit -> true." << endl;

    pthread_cond_broadcast(&cond);//虽然quit退出函数,但有线程处于等待条件状态,此处再统一唤醒。
    //只是为了演示两种唤醒函数。
    
    sleep(3);
    cout << endl;

    //等待新线程
    for(int i = 0; i < TNUM; ++i)
    {
        pthread_join(tid[i],nullptr);
        cout << "join thread: " << tid[i] << endl;
    }

    //销毁锁、条件变量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

  
  演示结果:
在这里插入图片描述

  
  
  
  
  
  
  
  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值