线程控制 ─── linux第30课

目录

POSIX线程库

创建线程

创建线程pthread_create的传参(arg)

1. 传递单个参数

2. 传递多个参数

创建一个线程

总结:

终止线程

pthread_exit函数

pthread_cancel函数

等待线程

等待线程pthread_join的参数(value_ptr)

示例1:

示例2:(重要)

创建多线程实例:

线程分离

线程分离pthread_detach

线程创建进程

线程库

POSIX线程库

  • 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以“pthread_”打头的
  • 要使⽤这些函数库,要通过引入头文件<pthread.h>
  • 链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项   eg: g++ -o $@ $^ -lpthread
  • 这个pthread库是linux自带的原生线程库 , C++11也有线程<thread>库,在Linux上本质就是封装了<pthread>库,
  • C++11 的 <thread>库 是跨平台的,在 Windows 上可能封装 CreateThread,在 macOS 上封装 pthread,在 Linux 上通常封装 pthread

创建线程

功能:创建⼀个新的线程
原型:
 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
 
参数:
     输出型参数thread:返回线程ID
     attr:设置线程的属性,attr为NULL表⽰使⽤默认属性
     start_routine:是个函数地址,线程启动后要执⾏的函数
     arg:传给线程启动函数的参数
 
返回值:成功返回0;失败返回错误码(错误码一般大于0)

vfork已经基本被废除了

在linux内核看来 ,只有LWP ,所以向外只提供了clone一种系统调用, 通过封装clone, fork可以创建子进程 ,pthread_creat可以创建线程.

但是在Windows内核下,提供了创建进程的syscall 和创建线程的syscall (因为windos中确实单独实现了线程 和进程) linux下只有LWP

错误检查:

  • 传统的⼀些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指⽰错误。
  • pthreads函数出错时不会设置全局变量errno(⽽⼤部分其他POSIX函数会这样做)。⽽是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以⽀持其它使⽤errno的代码。对于pthreads函数的错误,建议通过返回值l来判定,因为读取返回值要⽐读取线程内的errno变量的开销更⼩

创建线程pthread_create的传参(arg)

参数 arg是传入的函数指针(回调函数)的对应的参数(void*) 

32位机器下 void* 是4Byte 64位下是8Byte ,这时void * 与地址变量的大小一样

为什么传void *呢?

        这个参数可以传递任意类型(通过指针+类的封装) ,一般传的是指针

1. 传递单个参数

如果线程函数只需要一个参数,可以直接传递该参数的地址。例如:

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

void* thread_task(void* arg) {
    int num = *(int*)arg;
    printf("Thread received parameter: %d\n", num);
    return NULL;
}

int main() {
    pthread_t tid;
    int value = 45;
    pthread_create(&tid, NULL, thread_task, (void*)&value);
    pthread_join(tid, NULL);
    return 0;
}

2. 传递多个参数

如果需要传递多个参数,通常会将这些参数封装到一个结构体中,然后传递该结构体的地址。例如

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>

class ThreadData
{
public:
    ThreadData(const std::string &name ,int a ,int b)
    :_name(name)
    ,_a(a)
    ,_b(b)
    {}
    ~ThreadData()
    {}
    int excute()
    {
        return _a+_b;
    }
    std::string name()
    {
        return _name;
    }
private:
    std::string _name;
    int _a;
    int _b;
};

std::string ToHex(pthread_t tid)
{
    char buff [64];
    snprintf(buff ,sizeof(buff) ,"0x%lx",tid);
    return buff;
}

void *routine (void* args)
{
    //static_cast 是 C++ 中的 静态类型转换运算符,用于在 编译时 进行类型检查的显式转换。它比 C 风格的强制转换 (type)value 更安全
    //使用方法:static_cast<目标类型>(表达式)
    ThreadData* td =static_cast<ThreadData* >(args);
    while(true)
    {
        std::cout<<"我是子线程 我的名字是 "<<td->name()<<", my id is"<< ToHex(pthread_self()) <<std::endl;
        std::cout<<"task result is"<<td->excute()<<std::endl;
        sleep(1);
    }
    return 0;
}

int main()
{
    ThreadData *td =new ThreadData("thread-1" , 10,20);
    pthread_t tid;
    int n =pthread_create(&tid ,nullptr ,routine,(void*)td);
    if(n!=0 )
    {
        std::cout<<"creat thread error"<<std::endl;
        return 1;
    }
    while(true)
    {
        std::cout<<"我是main线程 "<<", my id is"<< ToHex(pthread_self()) <<std::endl;
        sleep(2);
    }
    return 0;
}

创建一个线程

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>

void* routine (void* args)
{
    //static_cast 是 C++ 中的 静态类型转换运算符,用于在 编译时 进行类型检查的显式转换。它比 C 风格的强制转换 (type)value 更安全
    //使用方法:static_cast<目标类型>(表达式)
    std::string name = static_cast<const char *>(args);// 将 args(某种类型)显式转换为 const char *,并用它构造一个 std::string 对象 name
    while(true)
    {
        std::cout<<"我是子线程 我的名字是 "<<name<<std::endl;
        sleep(1);
    }
    return 0;
}

int main()
{
    pthread_t tid;
    int n =pthread_create(&tid ,nullptr ,routine,(void*)"thread-1");
    if(n!=0 )
    {
        std::cout<<"creat thread error"<<std::endl;
        return 1;
    }

    while(true)
    {
        std::cout<<"我是main线程 "<<std::endl;
        sleep(1);
    }
    return 0;
}

每个线程都有自己的ID ,在创建时就以第一个参数形式输出了,也可以使用函数pthread_self( ) 获取自己线程的ID

两个线程同时访问同一个公共资源有可能引发错误(像显示器打印,在不加保护的情况下,显示器也是公共资源)下面可以看到打印错乱,就是这个原因,就是我们以前讲的重入.

我们可以看到main线程和子线程的ID不同

总结:

  • 新线程和main线程谁先运行 ,不确定
  • 线程要瓜分进程的资源,包括时间片
  • 不加保护的情况下 ,显示器文件就是公共资源
  • 对于多线程代码 ,进程内的函数 ,被所有线程共享,只要这个函数不使用全局变量(显示器文件就是全局变量),就是安全的
  • 全局变量能被此进程中的全部线程看到
  • 一旦一个线程出现异常,进程就崩了(被信号杀死了),其它线程也就全崩了  例如:一个线程中使用了野指针 ,查页表时失败了,MMU报错 ,导致CPU触发软中断 ,OS发送信号杀死进程.
  • 线程创建后也是要被等待和回收的(主线程像父进程一样一定是最后退出的)
  • 线程的栈 ,是独立的栈 ,不是私有的栈 ,如果将线程1中栈上的数据的地址给同一个进程的其它线程2 ,线程2也可以使用或修改其中数据(不会这么用 )

终止线程

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从新线程return。     这种⽅法对主线程不适⽤, 从main函数return相当于调用exit ,整个进程就退出来。
  2. 线程可以调用 pthread_exit终⽌⾃⼰
  3. ⼀个线程可以调用 pthread_cancel 终⽌同⼀进程中的另⼀个线程。(一般都是主线程调用pthread_cannel( ) 终止其他进程 )  不推荐使用
  4. 重要重要重要: 进程中不管任何地方调用exit , 整个进程就会终止.

pthread_exit函数

功能:线程终⽌
原型:
     void pthread_exit(void *value_ptr);
 
参数:
     value_ptr:value_ptr不要指向⼀个局部变量。

返回值:
     ⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)

要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是⽤malloc分配的, 不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数

功能:取消⼀个执⾏中的线程  
不推荐使用 ,因为不清楚要取消的线程的运行状态
使用前提: 要终止的线程已经跑起来了 
原型:
     int pthread_cancel(pthread_t thread);
 
参数:
     thread:线程ID
 
返回值:成功返回0;失败返回错误码

等待线程

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复⽤刚才退出线程的地址空间。
功能:等待线程结束

原型
 int pthread_join(pthread_t thread, void **value_ptr);
 
参数:
     thread:线程ID
     输出型参数value_ptr:它指向⼀个指针,后者指向线程的返回值
 
返回值:成功返回0;失败返回错误码(一般大于0)

调用该函数的线程挂起等待(阻塞),直到 id 为thread的线程终止thread线程以不同的方法终止(三种方式), 通过 pthread_join得到的终⽌状态是不同的,总结如下

  1.  如果thread线程通过return返回 ,value_ptr所指向的单元(将void*作为了容器)⾥存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调⽤pthread_cancel异常终掉,value_ptr所指向的单元⾥存放的是常数PTHREAD_CANCELED,也就是 -1。
  3. 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元(将void*作为了容器)存放的是传给 pthread_exit的参数。
  4. 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ptr参数。

等待线程pthread_join的参数(value_ptr)

返回时产生了void* 的8Byte(64位)的空间,将100写入这个空间, 等待的线程将退出的线程的返回值空间的内容拷贝到他自己value_ptr所指的空间中

也就是*value_ptr = (void*) k

一般等待线程pthread_join的value_ptr所指空间(*value_ptr)接受到的是指针. (例如k是堆指针)

理论上:线程共享地址空间(包含堆)

一个线程在内部申请了堆空间,就认为这个空间被它独占(谁拿到堆空间的地址,谁就能访问).

如果想让其他线程访问它的堆空间,只需要将堆空间地址传给他即可.(应用于示例2)

示例1:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>

class ThreadData
{
public:
    ThreadData(const std::string &name ,int a ,int b)
    :_name(name)
    ,_a(a)
    ,_b(b)
    {}
    ~ThreadData()
    {}
    int excute()
    {
        return _a+_b;
    }
    std::string name()
    {
        return _name;
    }
private:
    std::string _name;
    int _a;
    int _b;
};

std::string ToHex(pthread_t tid)
{
    char buff [64];
    snprintf(buff ,sizeof(buff) ,"0x%lx",tid);
    return buff;
}

void *routine (void* args)
{
    //static_cast 是 C++ 中的 静态类型转换运算符,用于在 编译时 进行类型检查的显式转换。它比 C 风格的强制转换 (type)value 更安全
    //使用方法:static_cast<目标类型>(表达式)
    ThreadData* td =static_cast<ThreadData* >(args);
    while(true)
    {
        std::cout<<"我是子线程 我的名字是 "<<td->name()<<", my id is"<< ToHex(pthread_self()) <<std::endl;
        std::cout<<"task result is"<<td->excute()<<std::endl;
        sleep(1);
        break;
    }
    return (void *)100;
}

int main()
{
    ThreadData *td =new ThreadData("thread-1" , 10,20);
    pthread_t tid;
    int n =pthread_create(&tid ,nullptr ,routine,(void*)td);
    if(n!=0 )
    {
        std::cout<<"creat thread error"<<std::endl;
        return 1;
    }

    void* ret =nullptr;
    int n2 =pthread_join(tid ,&ret);
    if(n2!=0)
    {
        std::cout<<"wait thread error"<<std::endl;
        return 1;
    }
    //此时ret为8Byte (64位机器)
    std::cout<<"wait thread succcess, ret "<<(long)ret<<std::endl;

    return 0;
}

示例2:(重要)

这个join 可以保证只要join成功,新线程就执行完毕了,结果一定可信

如果创建全局变量,让新线程去处理 ,主线程要不停检查是否执行完了,用主线程的堆空间 ,传递给新线程,新线程只要返回堆指针 ,就证明处理完了.主进程拿结果即可.




#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>

class ThreadData
{
public:
    ThreadData(const std::string &name ,int a ,int b)
    :_name(name)
    ,_a(a)
    ,_b(b)
    {}
    ~ThreadData()
    {}
    void excute()
    {
       _result = _a+_b;
    }
    int result()
    {
        return _result;
    }
    std::string name()
    {
        return _name;
    }
private:
    std::string _name;
    int _a;
    int _b;
    int _result;
};

std::string ToHex(pthread_t tid)
{
    char buff [64];
    snprintf(buff ,sizeof(buff) ,"0x%lx",tid);
    return buff;
}

void *routine (void* args)
{
    ThreadData* td =static_cast<ThreadData* >(args);
    while(true)
    {
        std::cout<<"我是子线程 我的名字是 "<<td->name()<<", my id is"<< ToHex(pthread_self()) <<std::endl;
        td->excute();
        sleep(1);
        break;
    }
    return td;
}

int main()
{
    ThreadData *td =new ThreadData("thread-1" , 10,20);
    pthread_t tid;
    int n =pthread_create(&tid ,nullptr ,routine,(void*)td);
    if(n!=0 )
    {
        std::cout<<"creat thread error"<<std::endl;
        return 1;
    }

    ThreadData* rtd =nullptr;
    int n2 =pthread_join(tid ,(void**)&rtd);
    if(n2!=0)
    {
        std::cout<<"wait thread error"<<std::endl;
        return 1;
    }
    //此时ret为8Byte (64位机器)
    std::cout<<"wait thread succcess, ret "<<rtd->result()<<std::endl;
    delete(td);
    return 0;
}

创建多线程实例:

创建多线程过程(多线程如何操作):
1. 处理数据

2. 创建多线程

3. 等待多线程

4. 汇总数据

// class ThreadData
// {
// public:
//     ThreadData()
//     {}
//     void Init(const std::string &name, int a, int b)
//     {
//         _name = name;
//         _a = a;
//         _b = b;
//     }
//     void Excute()
//     {
//         _result = _a + _b;
//     }
//     int Result(){ return _result; }
//     std::string Name(){ return _name; }
//     void SetId(pthread_t tid) { _tid = tid;}
//     pthread_t Id() { return _tid; }
//     int A(){return _a;}
//     int B(){return _b;}
//     ~ThreadData()
//     {}
// private:
//     std::string _name;
//     int _a;
//     int _b;
//     int _result;
//     pthread_t _tid;
// };


// #define NUM 10

// int main()
// {

//     // ThreadData td[NUM];
//     // // 准备我们要加工处理的数据
//     // for(int i = 0; i < NUM; i++)
//     // {
//     //     char id[64];
//     //     snprintf(id, sizeof(id), "thread-%d", i);
//     //     td[i].Init(id, i*10, i*20);
//     // }

//     // // 创建多线程
//     // for(int i = 0; i < NUM; i++)
//     // {
//     //     pthread_t id;
//     //     pthread_create(&id, nullptr, routine, &td[i]);
//     //     td[i].SetId(id);
//     // }

//     // // 等待多个线程
//     // for(int i =0; i< NUM;i++)
//     // {
//     //     pthread_join(td[i].Id(), nullptr);
//     // }

//     // // 汇总处理结果
//     // for(int i =0;i < NUM; i++)
//     // {
//     //     printf("td[%d]: %d+%d=%d[%ld]\n", i, td[i].A(), td[i].B(), td[i].Result(), td[i].Id());
//     // }

// }

线程分离

想让主线程不等待新线程(主线程去做自己的事)

注意:我们要保证在多执行流下 ,主执行流是最后退出的

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则无法释放资源,从而造成系统泄漏。 
  • 线程两种被等待的状态: 1. joinable :线程需要被主线程等待  2. detach:线程被分离(主线程不需要等待新线程)
  • 如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

线程分离pthread_detach

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程⾃⼰分离:

pthread_detach(pthread_self());
或者
pthread_detach(tid);
#include <stdio.h>
#include <stdlib.h>
#include <string.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( void )
{
     pthread_t tid;
     if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 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;
}

线程创建进程

任何一个线程不能调用exec函数进行程序的替换

但是线程可以先创建(fork)一个子进程,再调用exec函数

创建出来的子进程 ,会将线程所属当前进程的页表 ,地址空间全都拷贝一份(不会拷贝PCB),形成的子进程只有他自己的PCB

 void *start(void *args)
 {
      //可以的!
      pid_t id = fork();
      if(id == 0)
      {
          // ....
      }
      return 0; 
}

线程库

  • pthread_create函数第⼀个参数指向⼀个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 同一进程的线程共享地址空间,线程库加载在地址空间的共享区 ,被加载到地址空间中的线程库,对应的进程中有几个线程 ,其线程库中就会有几个线程属性结构,线程退出 ,地址空间中的线程库中线程属性结构体也会随之释放。
  • 线程ID(tid)是此线程在线程库中属性结构体在地址空间的地址 ,具有唯一性
  • linux内核中只认识LWP ,线程库pthread库封装了Linux的LWP的创建接口以及LWP属性接口 ,在用户层面产生了线程的概念
  • 线程库NPTL提供了pthread_self函数,可以获得线程⾃⾝的ID:
  • 一个线程退出 ,将退出码写在他的tcb中,主线程通过jion可以拿到

动态库中存在代码中数据的属性集合是如何做到的呢?

FILE 由 C 标准库 <stdio.h> 维护。

这个其实原来接触过 ,例如文件操作中的FILE就是存储在C 标准库中的 ,是在C 标准库中malloc的一块空间 ,打开多少个文件就malloc了多少个FILE

pthread_t 到底是什么类型呢取决于实现。对于Linux⽬前实现的NPTL实现⽽⾔,pthread_t类 型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。

线程栈

虽然Linux将线程和进程不加区分的统⼀到了task_struct ,但是对待其地址空间的 stack 还是有些区别的。

地址空间中的栈是主线程的栈,其他线程的栈在共享区的动态库中(动态申请)

  • 对于Linux进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复制了⽗亲的 stack 空间地址,然后写时拷⻉(cow)以及动态增⻓。如果扩充超出该上限则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯⼀可以访问未映射⻚⽽不⼀定会发⽣段错误⸺⸺超出扩充上限才报。
  • 然⽽对于主线程⽣成的⼦线程⽽⾔,其 stack 将不再是向下⽣⻓的,⽽是事先固定下来的。线程栈⼀般是调⽤glibc/uclibc等的 pthread 库接⼝ pthread_create 创建的线程,在⽂件映射区(或称之为共享区)。其中使⽤ mmap 系统调⽤,这个可以从 glibc 的 nptl/allocatestack.c 中的 allocate_stack 函数中看到:
mem = mmap (NULL, size, prot, 
             MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
  • 此调⽤中的 size 参数的获取很是复杂,你可以⼿⼯传⼊stack的⼤⼩,也可以使⽤默认的,⼀般⽽ ⾔就是默认的 8M 。这些都不重要,重要的是,这种stack不能动态增⻓,⼀旦⽤尽就没了,这是和⽣成进程的fork不同的地⽅。在glibc中通过mmap得到了stack之后,底层将调⽤ sys_clone 系 统调⽤
int sys_clone(struct pt_regs *regs) 
{ 
     unsigned long clone_flags; 
     unsigned long newsp; 
     int __user *parent_tidptr, *child_tidptr; 
 
     clone_flags = regs->bx; 
     //获取了mmap得到的线程的stack指针  
     newsp = regs->cx; 
     parent_tidptr = (int __user *)regs->dx; 
         child_tidptr = (int __user *)regs->di; 
     if (!newsp) 
     newsp = regs->sp; 
     return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr); 
 
} 

因此,对于⼦线程的 stack ,它其实是在进程的地址空间中map出来的⼀块内存区域,原则上是 线程私有的,但是同⼀个进程的所有线程⽣成的时候,是会浅拷⻉⽣成者的 task_struct 的很多 字段,如果愿意,其它线程也还是可以访问到的,于是⼀定要注意

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一码归—码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值