前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux】线程控制的秘密:如何写出高效、稳定的多线程程序

【Linux】线程控制的秘密:如何写出高效、稳定的多线程程序

作者头像
Yui_
发布2025-01-23 09:14:57
发布2025-01-23 09:14:57
9000
代码可运行
举报
文章被收录于专栏:Yui编程知识Yui编程知识
运行总次数:0
代码可运行

在上篇关于线程的文章中,我们已经比较详细的了解了关于线程得概念,以及简单得见识过了线程,本篇文章将对线程概念进行些补充,同时帮助大家实现对线程的控制,如:创建线程,等待线程,取消线程,终止线程。

1. 线程概念补充

1.1 线程的私有资源

我们知道线程是可以共享资源的,但是真的是所有资源都共享吗?实则不然,在操作系统中,线程是相对独立的。尽管共享的大部分数据,但是都有其独立的资源。 这些资源在同一进程的不同线程之间是隔离的。每个线程可以独立访问和修改自己的私有资源,而不会影响其他线程的私有资源。 在多线程编程中,线程私有资源的主要作用是解决资源竞争数据共享的问题。通过将某些资源声明为线程私有,可以避免多个线程同时访问共享资源时引发的竞态条件(Race Condition)和数据不一致问题。 以下是线程私有的资源:

  • 线程ID:内核观点中的LWP.
  • 一组寄存器:线程切换时,当前线程的上下文数据需要被保存。
  • 线程独立栈:线程在执行函数时,需要创建临时变量。
  • 错误码:errno:线程因为错误而终结,需要告知父进程。
  • 信号屏蔽字:不同线程对于信号的屏蔽需求不同。
  • 调度优先级:线程也是需要被调度的,需要根据优先级进行合理调度。 现在我会验证线程的独立栈,下面是相关的代码:
代码语言:javascript
代码运行次数:0
复制
/**
 * 验证线程的私有属性
 */

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

void* threadFunc(void* arg) {
    int localVar = 0;
    std::cout << "Thread ID: " << pthread_self() 
              << ", Address of localVar: " << &localVar << std::endl;
    return nullptr;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, nullptr, threadFunc, nullptr);
    pthread_create(&t2, nullptr, threadFunc, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    return 0;
}
//打印结果
/*
Thread ID: 139879076959808, Address of localVar: 0x7f3822b18e34
Thread ID: 139879085352512, Address of localVar: 0x7f3823319e34
*/

可以看到他们的地址是不一样的。

1.2 线程的共享资源

共享资源就比较好理解了,因为线程看到的都是同一块地址空间。 ![[Pasted image 20250117153905.png]] 如图所示。那么线程具体共享哪些资源的呢?

  • 共享区、全局数据区、字符常量区、代码区:常规资源共享区。
  • 文件描述符表:进行IO操作时,无需再次打开文件。
  • 每种信号的处理方法:多线程共同构成一个整体,信号的处理地址必须一样。
  • 当前工作目录:即便是多线程,也是在同一个工作目录下的。
  • 用户ID和组ID:进程属于某一个组中的某个用户,多线程也是如此。
代码语言:javascript
代码运行次数:0
复制
/**
 * 验证全局变量的共享性
 */


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

int sharedVar = 0;

void* threadFunc(void* arg) {
    for (int i = 0; i < 5; ++i) {
        ++sharedVar;
        std::cout << "Thread ID: " << pthread_self() 
                  << ", sharedVar: " << sharedVar << std::endl;
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, nullptr, threadFunc, nullptr);
    pthread_create(&t2, nullptr, threadFunc, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    return 0;
}
/*
打印结果
Thread ID: Thread ID: 140233541813824, sharedVar: 140233550206528, sharedVar: 22
Thread ID: 140233550206528, sharedVar: 3
Thread ID: 140233550206528, sharedVar: 4
Thread ID: 140233550206528, sharedVar: 5
Thread ID: 140233550206528, sharedVar: 6

Thread ID: 140233541813824, sharedVar: 7
Thread ID: 140233541813824, sharedVar: 8
Thread ID: 140233541813824, sharedVar: 9
Thread ID: 140233541813824, sharedVar: 10
*/

2. 线程控制

2.1 创建线程

创建线程,不管是在上一篇文章还是在前面的代码中都有所提及。 创建线程的函数就是pthread_create() pthread_create函数是POSIX标准中用于创建新线程的函数,它运行在同一进程中并发执行多个任务。

代码语言:javascript
代码运行次数:0
复制
#include <pthread.h>

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

参数说明:

  1. pthread_t* thread:
    • 用于存储创建的线程ID(线程句柄)。
    • 线程ID在后续操作(如pthread_join)中用于标识线程。
  2. const pthread_attr_t* attr:
    • 线程的属性。可以设置为NULL使用默认属性。
    • 自定义属性可以用于指定线程栈大小、调度策略等。
  3. void* (*start_routine)(void*):
    • 线程执行的函数指针。
    • 函数的返回值可以通过pthread_join获取。
  4. void* arg:
    • 传递给函数的参数,如果线程函数需要多个参数,可以将参数打包为一个结构体后传递。 返回值:
  • 0:表示线程创建成功。
  • 非0:表示线程创建失败,返回错误代码。

虽然已经看了很多遍了,但是我还是要用这个函数来给大家演示一个现象。

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include <threads.h>

using namespace std;

void* run(void* arg){
    while(true){
        cout<<"我是子线程:"<<(char*)arg<<endl;
        sleep(1);
    }
    return nullptr;
}

const int num = 5;
int main(){
    pthread_t pth[num];
    for(int i = 1;i<=num;++i){
        char name[64];
        snprintf(name,sizeof name,"thread%d",i);
        pthread_create(&pth[i-1],nullptr,run,name);
    }
    while(true){
        cout<<"我是主线程: main"<<endl;
        sleep(2);
    }
    
    return 0;
}

/*
打印结果
我是子线程:thread5
我是子线程:thread5
我是子线程:我是主线程: main
thread5
我是子线程:thread5
我是子线程:thread5
我是子线程:我是子线程:thread5thread5
*/

在这段代码中我创建了5个线程,然后给他们分别命名位thread[1-5]。但是当我们打印结果后出来的却只有thread5. 这是为什么呢? 其实这也是线程共享资源造成的错误,name是在主线程栈区开辟的空间,多个线程实际上指向的是同一块空间,最后一次覆盖后,所有线程就都打印thread5了。 ![[Pasted image 20250122203537.png]] 为了避免这个问题,我们应该区堆上开辟空间。

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include <threads.h>

using namespace std;

void* run(void* arg){
    while(true){
        cout<<"我是子线程:"<<(char*)arg<<endl;
        sleep(1);
    }
    delete[] (char*)arg;
    return nullptr;
}

const int num = 5;
int main(){
    pthread_t pth[num];
    for(int i = 1;i<=num;++i){
        char* name = new char[64];
        snprintf(name,sizeof name,"thread%d",i);
        pthread_create(&pth[i-1],nullptr,run,name);
    }
    while(true){
        cout<<"我是主线程: main"<<endl;
        sleep(2);
    }
    
    return 0;
}
/*
打印结果:
我是主线程: main
我是子线程:thread2
我是子线程:thread1
我是子线程:thread4
我是子线程:thread3
我是子线程:thread5
我是子线程:我是子线程:thread4thread2
我是子线程:thread1
我是子线程:thread3
*/

这时肯定有人问了,堆区也是共享资源啊,为什么不会被覆盖?

这个就牵扯到计算机对不同空间的处理了。 在第一段代码中,name是局部变量,name内存地址是相同的因为栈帧被重复利用了。 而第二段代码中,name是动态分配的内存,存储在堆上,每次给name分配的地址是不同的。

2.2 等待线程

我们知道,进程是存在等待机制的,其实线程也是存在等待机制的。 线程等待的存在是为了在多线程程序中协调线程间的执行顺序,确保资源的正确访问和结果的有序生成。它通过协调线程间的执行顺序、确保资源的安全访问、回收线程资源以及实现线程间的通信,保证了多线程程序的稳定性和正确性。 Linux 中的 POSIX 线程库提供了 pthread_join 函数来实现这种等待机制。

2.2.1 pthread_join函数

代码语言:javascript
代码运行次数:0
复制
int pthread_join(pthread_t thread, void **retval);

参数说明

  1. thread:
    • 类型:pthread_t
    • 作用:指定要等待的目标线程 ID。
    • 该值通常是由 pthread_create 创建线程时返回的。
  2. retval:
    • 类型:void **
    • 作用:接收目标线程的返回值,即 pthread_exit 的参数。
    • 如果不需要获取线程返回值,可以传入 NULL返回值
  • 0: 表示成功等待目标线程结束。
  • 错误码:
    • ESRCH: 指定的线程不存在。
    • EINVAL: 线程不可连接,例如它已经分离。
    • EDEADLK: 调用线程与被等待线程之间存在死锁。
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>


using namespace std;

void* run(void* arg){
    int cnt = 3;
    while(cnt--){
        cout<<"我是子线程:"<<(char*)arg<<endl;
        sleep(1);
    }
    delete[] (char*)arg;
    return nullptr;
}

int main(){
    pthread_t pth[5];
    for(int i = 1;i<=5;++i){
        char* name = new char[64];
        snprintf(name,64,"pthread%d",i);
        pthread_create(&pth[i-1],nullptr,run,name);
    }
    int cnt = 1;
    for(int i = 1;i<=5;++i){
        
        if(pthread_join(pth[i-1],nullptr)!=0){
            perror("等待失败");
            exit(1);
        }else{
            printf("成功等待%d个线程\n",cnt++);
        }
        sleep(1);
    }
}

/*
打印结果:
我是子线程:pthread1
我是子线程:pthread2
我是子线程:pthread4
我是子线程:pthread3
我是子线程:pthread5
我是子线程:pthread2
我是子线程:pthread3
我是子线程:pthread1
我是子线程:pthread4
我是子线程:pthread5
我是子线程:pthread2
我是子线程:pthread3
我是子线程:pthread1
我是子线程:pthread4
我是子线程:pthread5
成功等待1个线程
成功等待2个线程
成功等待3个线程
成功等待4个线程
成功等待5个线程
*/

2.3 终止线程

我们知道,终止一个进程的函数是exit,那么终止一个线程的函数又是什么呢? 在多线程编程中,线程的终止是一个重要的操作。线程可以通过多种方式终止,例如正常执行完毕、显式调用终止函数、或者被其他线程强制终止。 线程终止主要有3种方式,pthread_exitpthread_cancel和正常返回。

2.3.1 pthread_exit函数
代码语言:javascript
代码运行次数:0
复制
void pthread_exit(void *retval);

参数说明: retval:

  • 类型:void *
  • 作用:用于指定线程的退出状态(返回值)。
  • 使用场景:
    • 如果线程被 pthread_join 回收,则通过 pthread_join 的第二个参数获取该值。
    • 可以将简单的返回值(如整型或字符串指针)通过 retval 传递给回收线程。
    • 注意:retval 的内存管理由调用者负责,通常在退出前动态分配或者使用静态内存。
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void* run(void* arg){
    int cnt = 3;
    while(cnt--){
        cout<<"我是子线程:"<<(char*)arg<<endl;
        sleep(1);
    }
    pthread_exit(arg);
}

int main(){
    pthread_t pth[5];
    void* ret;
    for(int i = 1;i<=5;++i){
        char* name = new char[64];
        snprintf(name,64,"pthread%d",i);
        pthread_create(&pth[i-1],nullptr,run,name);
    }
    int cnt = 1;
    for(int i = 1;i<=5;++i){
        
        if(pthread_join(pth[i-1],&ret)!=0){
            perror("等待失败");
            exit(1);
        }else{
            cout<<"成功等待线程:"<<(char*)ret<<endl;
        }
        sleep(1);
    }
}

/*
打印结果:
我是子线程:pthread3
我是子线程:pthread2
我是子线程:pthread1
我是子线程:pthread4
我是子线程:pthread5
我是子线程:pthread3
我是子线程:pthread2
我是子线程:pthread1
我是子线程:pthread4
我是子线程:pthread5
我是子线程:我是子线程:pthread1pthread3
我是子线程:pthread2

我是子线程:pthread4
我是子线程:pthread5
成功等待线程:pthread1
成功等待线程:pthread2
成功等待线程:pthread3
成功等待线程:pthread4
成功等待线程:pthread5
*/

如此一来,我就可以拿到线程的名字了。 注意:因为retval的类型是void*,所以甚至我可以返回一个结构体/对象。

2.3.2 pthread_cancel函数
代码语言:javascript
代码运行次数:0
复制
int pthread_cancel(pthread_t thread);

参数说明

  • thread:
    • 类型:pthread_t
    • 作用:表示目标线程的线程标识符。
    • 使用场景:
      • 调用者通过该标识符指定需要被取消的线程。
      • 通常此标识符由 pthread_create 返回。

返回值

  • 类型:int
  • 含义:
    • 0: 成功向目标线程发送了取消请求。
    • 非零:函数调用失败,返回错误码。
      • 常见错误:
        • ESRCH: 目标线程不存在或无效。
        • EINVAL: 无效的参数,例如未启用线程取消功能。
代码语言:javascript
代码运行次数:0
复制
#include <pthread.h>
#include <iostream>
#include <unistd.h>

void* threadFunc(void* arg) {
    while (true) {
        std::cout << "子线程正在运行...\n";
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t thread;

    pthread_create(&thread, nullptr, threadFunc, nullptr);
    sleep(3);

    pthread_cancel(thread); // 发送取消请求
    pthread_join(thread, nullptr);

    std::cout << "子线程已被取消.\n";
    return 0;
}

/*
打印结果:
子线程正在运行...
子线程正在运行...
子线程正在运行...
子线程已被取消.
*/

3. 线程实战

实战环节,我会用线程来计算[1,100*i](1<=i<=5)的和。

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
using namespace std;
enum Status{//枚举状态
    OK=0,
    ERROR
};
/*
创建一个线程数据类,其中的属性有线程名字,线程id,线程状态,线程准备计算和的右边界。
*/
class ThreadData{
public:
    ThreadData(const string& name,int n,int id)
    :_name(name),
    _n(n),
    _id(id),
    _result(0),
    _status(Status::OK)
    {}
    ~ThreadData(){}
    
public:
    string _name;
    int _n;
    int _id;
    int _result;
    Status _status;
};

void* run(void* arg){
    ThreadData* td = static_cast<ThreadData*>(arg);
    for(int i = 0;i<=td->_n;++i){
        td->_result+=i;
    }
    cout<<"线程: "<<td->_name<<"执行完毕"<<endl;
    pthread_exit(arg);
}

int main()
{
    pthread_t pths[5];
    //创建线程
    for(int i = 1;i<=5;++i){
        char* name = new char[64];
        snprintf(name,64,"thread-%d",i);
        ThreadData* threadData = new ThreadData(name,i*100,i);
        pthread_create(&pths[i-1],nullptr,run,threadData);
        sleep(1);
    }
    void* retval = nullptr;
    //等待线程
    for(int i = 1;i<=5;++i){
        if(pthread_join(pths[i-1],&retval)!=0){
            perror("线程等待失败!");
            exit(1);
        }
        ThreadData* td = static_cast<ThreadData*>(retval);
        if(td->_status == Status::OK){
            printf("线程%s,计算[1~%d]的结果为:%d\n",td->_name.c_str(),td->_n,td->_result);
        }
        delete td;
    }
    cout<<"所有线程退出完毕"<<endl;
    return 0;
}

/*
打印结果:
线程: thread-1执行完毕
线程: thread-2执行完毕
线程: thread-3执行完毕
线程: thread-4执行完毕
线程: thread-5执行完毕
线程thread-1,计算[1~100]的结果为:5050
线程thread-2,计算[1~200]的结果为:20100
线程thread-3,计算[1~300]的结果为:45150
线程thread-4,计算[1~400]的结果为:80200
线程thread-5,计算[1~500]的结果为:125250
所有线程退出完毕
*/

程序可以正常运行,各个线程也能正常计算出结果;这里只是简单累加求和,线程还可以用于其他场景。比如:网络传输、密集型计算、多路IO等等,无非就是修改线程的业务逻辑。

4.总结

线程控制是多线程编程中不可或缺的一部分,通过合理管理线程的创建、运行、同步和终止,可以提升程序的效率、稳定性和资源利用率。在实际开发中,理解线程的生命周期、掌握线程间通信与同步机制,以及灵活运用线程控制函数如 pthread_create、pthread_exit 和 pthread_join,能够帮助开发者更好地实现并发编程的目标。同时,线程控制的精髓不仅在于技术的掌握,更在于对资源的合理调配和对任务的高效分配。掌握这些技巧,无疑将为构建高效、可靠的多线程程序打下坚实的基础。

往期专栏:Linux专栏:Linux

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 线程概念补充
    • 1.1 线程的私有资源
    • 1.2 线程的共享资源
  • 2. 线程控制
    • 2.1 创建线程
    • 2.2 等待线程
    • 2.2.1 pthread_join函数
    • 2.3 终止线程
      • 2.3.1 pthread_exit函数
      • 2.3.2 pthread_cancel函数
  • 3. 线程实战
  • 4.总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档