【Linux】线程:从原理到编写,学习多线程编程

  📚 博主的专栏

   🐧 Linux   |   🖥️ C++   |   📊 数据结构  | 💡C++ 算法 | 🅒 C 语言  | 🌐 计算机网络

 关联文章:【Linux】线程的概念、虚拟地址最终理解(这一篇足够)-优快云博客

pthread_create()-----线程的创建

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

参数说明:

  • thread:存储新线程ID的指针,是一个输出型参数

  • attr:线程属性(NULL 表示默认),线程的调度优先级。

  • start_routine:线程入口函数,返回值类型void*,参数类型也是void*

  • arg:传递给入口函数的参数

返回值

  • 成功:返回0

  • 失败:返回错误码(如EINVAL表示无效参数)。

示例

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

void* thread_function(void* arg) {
    printf("Thread is running\n");
    return NULL;
}

int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thread_function, NULL);
    if (ret != 0) {
        perror("pthread_create failed");
        return -1;
    }
    return 0;
}

pthread_join()-----等待线程结束

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

参数

  • pthread_t thread:要等待的线程ID。

  • void **retval:存储线程返回值的指针(可选,传NULL表示不关心返回值)也就是新线程结束时,线程实现的函数的类型为void*的返回值,然后被主线程获取。

返回值

  • 成功:返回0

  • 失败:返回错误码(如ESRCH表示线程不存在)。

示例:

pthread_join(tid, NULL); // 等待线程结束

组合示例:这段代码通过 pthread_create 创建新线程执行循环打印任务,主线程使用 pthread_join 等待新线程结束以保证进程正常退出,避免新线程未执行完就被强制终止。

问题1:main 和 new线程谁先运行? 不确定(同进程)

问题2:我们期望谁最后退出: main thread,如何保证呢?join来保证,不join,是否会阻塞,不会,当主线程退了,进程退出,因此新线程一并退出不join,会造成类似类似僵尸的问题

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

void *threadRun(void *args)
{
    int cnt = 10;
    while(cnt)
    {
        std::cout << "new thread run ... cnt " << cnt-- << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;  //unsigned long int 
    //问题一:main 和 new线程谁先运行? 不确定(同进程)
    int n = pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    if(n != 0)
    {
        std::cerr << "create thread error" << std::endl;
        return 1;
    }

    std::cout << "main thread begin ..." << std::endl;

    //问题二:我们期望谁最后退出: main thread,如何保证呢
    n = pthread_join(tid, nullptr);//join来保证,不join,是否会阻塞,不会,当主线程退了,进程退出,因此新线程一并退出
    if(n == 0)                     //不join,会造成类似类似僵尸的问题
    {
        std::cerr << "main thread wait success" << std::endl;
    }
    return 0;
}

运行:

while :; do ps -aL ; sleep 1; done

 问题3:tid是什么样子?

直接进行打印时,可看到:tid的值很大,因此我们使用16进制来看

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

    // 问题3: tid 是什么样子的?是什么呢?
    std::string tid_str = PrintToHex(tid); // 我们按照16进行打印出来
    std::cout << "tid : " << tid_str << std::endl;

线程ID,TID是什么?是虚拟地址

问题4:全面看待给pthread_create:void *arg传参,我们可以传递任意参数类型,那么也可以传递类对象的地址

修改代码,查看所传的参数:

 

传变量值给新线程:

 int a = 100;
 int n = pthread_create(&tid, nullptr, threadRun, (void*)&a);
 int a = *(int*)args;
    int cnt = 10;
    while(cnt)
    {
        std::cout << a << " run ... cnt " << cnt-- << std::endl;
        sleep(1);
    }

首先了解一个强转类型:static_cast ---> 安全级别的强转,是为了告诉编译器,从而对目标类型做安全性检查。这里就是(ThreadData*)

封装一个类:

// 可以给线程传递多个参数,甚至方法了
class ThreadData
{
public:
    std::string name;
    int num;
};

在threadRun(void *args):

void *threadRun(void *args)
{
    //std::string name = (const char*)args;
    //int a = *(int*)args; // warning
    ThreadData *td = static_cast<ThreadData*>(args); // (ThreadData*)args
    int cnt = 10;
    while(cnt)
    {
        std::cout << td->name << " run ..." << ", cnt: " << cnt-- << std::endl;
        break;
    }
    return nullptr;
}

 在main函数当中:

  pthread_t tid; // unsigned long int
    // 问题一:main 和 new线程谁先运行? 不确定(同进程)
    ThreadData td;
    td.name = "thread-1";
    td.num = 1;

    int a = 100;
    int n = pthread_create(&tid, nullptr, threadRun, (void *)&td);

运行结果:

由此可以知道主线程可以给线程传递多个参数,甚至方法。

注意:

1.ThreadData对象td,属于main函数栈上开辟的空间,新线程访问的是主线程栈上的变量

上面的方法不推荐,破坏了主线程的完整性和独立性,如果新线程有多个,这些新的线程就会访问了同一个,主函数栈上定义的临时变量,如果有线程修改了这个值,就会影响另一个线程。线程1就会认为自己也是线程2.

2.正确的做法是,建议在堆上申请一块空间,然后在堆上将这个临时变量拷贝给新线程

需要创建多个新线程的时候,就创建多个对象,再拷贝给新线程

然后注意删除掉:

void *threadRun(void *args)
{
    //std::string name = (const char*)args;
    //int a = *(int*)args; // warning
    ThreadData *td = static_cast<ThreadData*>(args); // (ThreadData*)args
    int cnt = 10;
    while(cnt)
    {
        sleep(1); 
        std::cout << td->name << " run ... num is " << td->num << ", cnt: " << cnt-- << std::endl;

    }
    delete td;
    return nullptr;
}
 void **retval:返回的是新线程退出时,函数的返回值void*,返回的是一个指针变量

retval是一个输出型参数,是指针变量,是变量,在Linux中占8个字节,因此可以接受别人的返回值,指针就是一个数字, void *code = nullptr; 一定要注意,虽然是空指针,但是是开辟了空间的,因为Linux系统当中指针占8个字节,因此不能强转为int(int占4个字节),使用u_int64_t。

  void *code = nullptr; //一定要注意,虽然是空指针,但是是开辟了空间的
    n = pthread_join(tid, &code); 
    if (n == 0)                     
    {
        std::cerr << "main thread wait success, new thread exit code: " << (u_int64_t)code << std::endl;

    }

一旦线程退出,函数的返回值就会放到code当中。

如图:主线程就能根据退出信息来判断新线程对任务的处理情况

问题5:全面看待线程函数返回:

运行结果:新线程崩掉,主线程也一起崩掉,程序出错:

这是因为,一旦野指针,出错的时候直接将进程干掉,因此所有线程都没了,

因此线程的返回,只考虑正确的返回,不考虑异常,因为一旦异常,整个进程就崩溃,包括主线程

不用担心因为主线程也崩溃而不能知道新线程的执行情况,因为,线程崩溃就相当于进程崩溃,退出信息将有进程的父进程收到。

也可以返回执行任务的的结构体:指派一个任务给线程,让线程执行任务

这段代码通过传递自定义类 ThreadData 到新线程中进行计算,主线程使用 pthread_join 等待子线程返回 ThreadResult 结果并打印

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

// 可以给线程传递多个参数,甚至方法了
class ThreadData
{
public:
    int Excute()
    {
        return x + y;
    }

public:
    std::string name;
    int x;
    int y;
    // other
};

class ThreadResult
{
public:
    std::string print()
    {
        return std::to_string(x) + "+" + std::to_string(y) + "=" + std::to_string(result);
    }

public:
    int x;
    int y;
    int result;
};

void *threadRun(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args); // (ThreadData*)args
    ThreadResult *result = new ThreadResult;
    int cnt = 10;
    while (cnt)
    {
        sleep(1);
        result->result = td->Excute();
        result->x = td->x;
        result->y = td->y;
        break;
        std::cout << td->name << " run ... " << ", cnt: " << cnt-- << std::endl;

        // std::cout << td->name << " run ... num is " << td->num << ", cnt: " << cnt-- << std::endl;
    }
    delete td;
    return (void *)result;
}

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

int main()
{
    pthread_t tid; // unsigned long int
    // 问题一:main 和 new线程谁先运行? 不确定(同进程)
    ThreadData *td = new ThreadData();
    td->name = "thread-1";
    td->x = 10;
    td->y = 20;

    int a = 100;
    int n = pthread_create(&tid, nullptr, threadRun, td);

    // int n = pthread_create(&tid, nullptr, threadRun, (void*)&td);
    if (n != 0)
    {
        std::cerr << "create thread error" << std::endl;
        return 1;
    }

    std::string tid_str = PrintToHex(tid); // 我们按照16进行打印出来
    std::cout << "tid : " << tid_str << std::endl;

    std::cout << "main thread begin ..." << std::endl;

    void *code = nullptr; 
    n = pthread_join(tid, &code);
    if (n == 0)
    {
        std::cerr << "main thread wait success, new thread exit code: " << (u_int64_t)code << std::endl;
    }


    return 0;
}

运行结果:

问题6:如何创建多线程-线程的批量化创建和等待

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

const int num = 10;

void *threadRun(void *args)
{
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        std::cout << name << " is running" << std::endl;
        sleep(1);
     }
}


int main()
{
    std::vector<pthread_t> tids;
    //问题6:如何创建多线程
    for(int i = 0; i < num; i++)
    {   
        // 1.有线程的id
        pthread_t tid;
        // 2.有线程的名字
        char name[128];
        snprintf(name, sizeof(name), "thread-%d", i+1);
        pthread_create(&tid, nullptr, threadRun, name);
    }
    sleep(100);
}

虽然是有11个线程,但是出来的线程名很乱。我们想要看到的是有序地。虽然是按顺序创建,因为线程被创建,谁被调度不确定,公共区域被传给了所有的线程,连线程名都在不断的被覆盖。因此创建的时候要使用堆,然后再拷贝给新线程。

修改:

运行正确:

对所有的线程进行等待:

const int num = 10;

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

void *threadRun(void *args)
{
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        std::cout << name << " is running" << std::endl;
        sleep(1);
        break;
     }
     return args;
}

int main()
{
    std::vector<pthread_t> tids;
    //问题6:如何创建多线程
    for(int i = 0; i < num; i++)
    {   
        // 1.有线程的id
        pthread_t tid;
        // 2.有线程的名字
        char *name = new char[128];
        snprintf(name, 128, "thread-%d", i+1);
        pthread_create(&tid, nullptr, threadRun, name);
        // 3.保存所有新线程的 id
        tids.emplace_back(tid);
    }

    // join todo
    for(auto tid : tids)
    {   
        void *name = nullptr;
        pthread_join(tid, &name);
        // std::cout << PrintToHex(tid) << " quit... "<< std::endl;
        std::cout << (const char*)name << " quit... "<< std::endl;
        delete (const char*)name;
    }
}

运行结果:

问题7 - 线程的终止

1.新线程如何终止:函数return

2.主线程如何终止:main函数结束,主线程结束

可以使用exit()来结束新线程吗?不可以,exit()是专门用来结束进程的,如果在子线程运行完直接使用exit(),进程就直接退出了,那么主线程肯定也跟着一起退出了。

pthread_exit --显式地终止调用线程
#include <pthread.h>
void pthread_exit(void *retval);

参数

  • void *retval:指向线程返回值的指针。这个返回值可以通过 pthread_join 函数获取。如果 retvalNULL,则表示线程没有返回值。

返回值

  • pthread_exit 函数不返回任何值。调用 pthread_exit 后,线程会立即终止,后续操作将不再执行。控制权返回给线程库。

用法

  • 显式终止线程pthread_exit 用于显式地终止调用线程。与 exit 函数不同,pthread_exit 仅影响调用它的线程,而不是整个进程。

  • 返回值:通过 pthread_exitretval 参数,线程可以返回一个指向返回值的指针。调用线程的其他线程可以通过 pthread_join 函数获取这个返回值。

  • 资源释放:调用 pthread_exit 后,线程相关的资源(如线程栈和线程控制块)会被释放。如果线程在创建时分配了特定的资源(如动态分配的内存),需要在 pthread_exit 之前手动释放这些资源。

新线程退出成功,主线程等待成功。pthread_cancel类似作用是取消一个线程,会发送一个取消请求给新线程。

pthread_cancel---取消的目标线程的线程ID
#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数

  • pthread_t thread:指定要取消的目标线程的线程ID。

返回值

  • 成功:返回0

  • 失败:返回非零错误码。例如:

    • ESRCH:未找到目标线程。

    • 其他错误码可能因不同实现而异。

取消一个等待一个。如果一个线程是取消的,他的返回类型是一个有符号的整数。

最后只剩主线程,线程被取消,他的退出结果是: -1,这是在pthread库中被定义的宏。

问题8:在多线程的时候,是否能不join线程,让他执行完自己退出?--可以

pthread_detach()-----将线程设置为分离状态,使其在后台独立运行
int pthread_detach(pthread_t thread);

参数

  • pthread_t thread:要分离的线程ID。

返回值

  • 成功:返回0

  • 失败:返回错误码。

示例

pthread_detach(tid); // 分离线程

1.一个线程被创建,默认是joinable的:必须要被join的。

2.如果一个线程被分离,线程的工作状态分离状态,不需要/不能被join,依旧属于进程内部,但是不需要被等待join了。

pthread_self 函数
#include <pthread.h>
pthread_t pthread_self(void);

参数

  • 无参数。

返回值

  • 返回当前线程的线程ID,类型为pthread_t。在Linux系统中,pthread_t是一个不透明的数据类型,通常是一个结构体,包含线程的内部信息。

用法

  • 获取当前线程IDpthread_self 用于获取当前正在执行的线程的ID。

  • 线程标识:线程ID在同一个进程中是唯一的,可以用于标识不同的线程。

  • 线程管理:可以用于线程间的通信、同步和管理。

注意事项

  • 线程ID的唯一性:线程ID在同一个进程中是唯一的,但在不同进程中可能相同。

  • 线程ID的用途:线程ID可以用于线程间的通信,例如通过 pthread_joinpthread_cancel 等函数。

  • 线程ID的类型pthread_t 是一个不透明的数据类型,具体实现可能因系统而异。

原来的等待后得到的返回值:

现在分离:

退出的返回值等于22,出错 

修改代码:

运行结果:

这表示了,禁止等待,一等待就函数调用直接出错,就直接到主线程。

注意:就算新线程分离了,也仍然属于这个进程,如果发生了异常,还是会导致进程退出,主线程当然也退出。

主线程分离新线程:前提条件,新线程一定存在。主线程可以主动分离新线程

C++11已经实现多线程了:include<thread>

对比 pthread:无需手动管理 pthread_t,避免 void* 类型转换风险

示例:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include<vector>
#include<stdlib.h>
#include <thread>

//C++11已经支持多线程

void threadrun(std::string name, int num)
{
    while(num)
    {
        std::cout << name << " num : " << num<< std::endl;
        num--;
        sleep(1);
    }
}

int main()
{
    std::string name = "thread-1";

    std::thread mythread(threadrun, std::move(name), 10);
    while(true)
    {
        std::cout << "main thread..." << std::endl;
        sleep(1);
    }
    mythread.join();
    return 0;
}

删除掉Makefile中的导入第三方库的选项:有可能无法编译通过,因此要支持多线程也要添加pthread库。

 修改之后:需要引入 -lthread

C++11的多线程体系本质,就是对原生线程库的接口的封装

语言具有跨平台性

在语言层面上,就提供了同一种线程的创建方式,但是不同平台每一种库的实现就是不一样的

在学习文件操作的时候也是如此。

结语:

       随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

  

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。 

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值