当前位置: 首页 > news >正文

线程控制学习

1、线程创建:

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

参数 thread:返回线程ID;attr:设置线程的属性,attr为nullptr表示使用默认属性(一般这个都是nullptr);start_routine:是个函数地址,线程启动后要执行的函数;arg:传给线程启动函数的参数 返回值:成功返回0;失败返回错误码。

注意上面的start_routine是返回值为void*,参数也是void*。

pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID。pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

在创建线程时要使用到线程库,所以在使用线程库时,会将对应的库文件加载到内存中,然后通过页表和虚拟地址完成映射。最终线程库会被映射到虚拟地址空间的mmap区域。下图所示:

当我们调用pthread_create创建线程后,在线程库中就会新建一个描述线程的结构体,来保存线程的相关数据,而这一部分也是实际的内存,有对应的地址,而pthread_t本质就是一个进程地址空间上的地址。

这就相当于线程的TCB。

通过上面对进程地址空间分布的认识也可以学习一个操作:_thread

对于全局变量,主线程和新线程是共享的,所以当某一个线程访问全局变量时,访问的都是同一个变量,下图可以看到新线程改变g_val,主线程读出的也跟着改变:

但是对全局变量添加_thread修饰后,就将一个内置类型设置为局部存储,这种情况下,虽然这个g_val仍然是全局变量,但是每个线程都有属于自己的一个,互不影响,当在自己的线程中对其改变时,不会影响其他的线程,下图可以看出,新线程改变g_val,但是主线程并没跟着变:

而且通过上面的两个图也可以发现,他们的地址变换很大,因为刚开始,只是一个全局表量,保存在已初始化数据段,而添加_thread修饰后,主线程的g_val就在主线程栈中存储,而新线程的g_val就在对应线程空间的线程栈上。

2、线程退出:

1)新线程函数return;

void *routine(void *args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    int cnt = 5;
    while (cnt)
    {
        cout << "我是新线程!name: " << td->namebuffer << endl;
        cnt--;
        sleep(1);
        // exit(0); //不能终止线程,是用来终止进程的,这里任何一个执行流调用exit,会让所有的线程和主线程都终止。
    }
    
    //一般终止线程return即可
    //nullptr;
    return (void*)100; //1. 线程函数结束,return的时候,线程就算终止了
}

2)线程可以调用pthread_ exit终止自己;

void pthread_exit(void *value_ptr); 参数 value_ptr:value_ptr不要指向一个局部变量。

void *routine(void *args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    int cnt = 5;
    while (cnt)
    {
        cout << "我是新线程!name: " << td->namebuffer << endl;
        cnt--;
        sleep(1);
    }

    ThreadReturn* tr = new ThreadReturn();
    tr->code_quit = td->number;
    tr->code_result = 1;
    //注意:返回值需要是void*
    pthread_exit((void*)tr);
}

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

上面的ThreadReturn是自己定义的一个结构体,用来保存退出信息。这里的返回值是new出来的,在主线程的线程等待时记得delete。

3)一个线程可以调用pthread_ cancel终止同进程中的另一个线程。

int pthread_cancel(pthread_t thread); 参数 thread:线程ID;成功返回0;失败返回错误码。

线程取消要注意:只有运行的线程才能被取消:

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

using namespace std;

class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    int number;
};

class ThreadReturn
{
public:
    int code_quit;
    int code_result;
};

void *routine(void *args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    int cnt = 5;
    while (cnt)
    {
        cout << "我是新线程!name: " << td->namebuffer << endl;
        cnt--;
        sleep(1);
        // exit(0); //不能终止线程,是用来终止进程的,这里任何一个执行流调用exit,会让所有的线程和主线程都终止。
    }

    return nullptr;
}

int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for (int i = 0; i < NUM; ++i)
    {
        ThreadData* td = new ThreadData();
        td->number = i+1;
        snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
        pthread_create(&(td->tid), nullptr, routine, td);
        threads.push_back(td);
        // sleep(1);
    }

    for(auto& iter : threads)
    {
        cout << "create thread" << iter->namebuffer << " : " << iter->tid << "success" << endl;
    }


    //线程取消:pthread_cancel,只有执行中的线程才能被取消
    sleep(2);
    for(int i = 0; i < threads.size()/2; ++i)
    {
        pthread_cancel(threads[i]->tid);
        cout << "cancel: " << threads[i]->namebuffer << " success!" << endl;
    }
    
    /*
    for(auto& iter : threads)
    {
        void* ret = nullptr;
        //这里的第二个参数的类型需要是void**,因为外面的ret是void*,
        //如果传ret进去,那么函数内部改变ret并不会影响外面的值,所以需要传地址进去,那么ret的地址就是void**的了。
        pthread_join(iter->tid, &ret);
        cout << "join: " << iter->namebuffer << " success! code_quit--> " << (long long)ret << endl;
        //释放new的资源
        delete ret;
        delete iter;
    }
    */


    cout << "main thread quit!" << endl;

    return 0;
}

3、线程等待:

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。 创建新的线程不会复用刚才退出线程的地址空间。如果不等待,会造成类似僵尸进程的问题:内存泄漏。

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

参数 thread:线程ID;value_ptr:它指向一个指针,后者指向线程的返回值。

等待成功返回0;失败返回错误码。

1)如果新线程通过return返回,value_ ptr所指向的单元里存放的是新线程函数的返回值。

2)如果新线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。

3)如果新线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

4)如果对新线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

要注意:这里的value_ptr是输出型参数,用来获取新线程的返回值,因为新线程的返回值是void*的,返回的数据保存在pthread库中;想要同过该函数将返回值带出,就得在外面用个void* 的变量,然后再将该变量的地址(void**)传进去,最终就能拿到新线程的返回值信息。

4、线程分离:

1)默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

2)如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

所以对分离的线程,就不需要再join了,可以自动释放资源。

int pthread_detach(pthread_t thread);

线程分离函数,参数thread是线程ID。线程ID可以通过pthread_self()来获得。

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


void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    //将自己设置为分离状态,pthread_self()是获取自己的ID
    // pthread_detach(pthread_self()); 
    int cnt = 5;
    while(cnt--)
    {
        cout << name << " running..." << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
    //将创建的新线程分离,分离后该线程退出时自动回收
    pthread_detach(tid);

    //一个线程默认是joinable的,如果设置了分离状态,就不能够进行等待了。
    int n = pthread_join(tid,nullptr);
    cout << n << " : " << strerror(n) << endl;

    return 0;
}

这里需要注意的是:进行线程分离时,可以在主线程创建后分离,也可以在新线程内部设置分离。

这里先看创建新线程后就分离,如果对分离的线程再等待就会出问题,最终导致程序崩溃:

而在新线程内部分离,可能有一些问题,因为创建新线程后,并不能确定是哪一个线程先运行,可能主线程先运行,已经开始等待新线程,而线程运行时虽然设置分离状态,主线程并不知道,但是主线程仍然在等待,并且等待成功,但是实际上对于已经分离的线程再等待就会出现问题:

相关文章:

  • MCP Facade Generator:助力 MCP 协议接口实现的强大工具
  • 【C语言】C语言使用随机数srand,rand
  • nacos-actuator漏洞
  • apt 常见报错及解决方法
  • 本地部署 Firecrawl
  • 【nodejs】爬虫路漫漫,关于nodejs的基操
  • CAJ转PDF:复杂的转换背后有哪些挑战?
  • rocky linux yum源配置
  • 图论 | 98. 所有可达路径
  • 机器学习、深度学习解决方案设计方案通用审核流程(solution architect review)
  • 从扩展黎曼泽塔函数构造物质和时空的结构-5
  • 【页面组件】——1
  • 使用DeepSeek进行审稿和反馈审稿意见相关流程和提示词分享
  • 【算法】常见排序算法(插入排序、选择排序、交换排序和归并排序)
  • LLM之RAG理论(十四)| RAG 最佳实践
  • Pi型隶属函数(Π-shaped Membership Function)的详细介绍及python示例
  • 2025年3月 CCF GESP C++ 二级 真题解析
  • 仅靠prompt,Agent难以自救
  • DeepSeek,PowerBI一般有哪些问题?
  • 操作系统导论——第13章 抽象:地址空间
  • 欧洲观察室|欧盟对华战略或在中欧建交50年时“低开高走”
  • 1至4月国家铁路发送货物12.99亿吨,同比增长3.6%
  • 玛丽亚·凯莉虹口连唱两夜,舞台绽放唤醒三代人青春记忆
  • 科普|认识谵妄:它有哪些表现?患者怎样走出“迷雾”?
  • 我使馆就中国公民和企业遭不公正待遇向菲方持续提出严正交涉
  • 泽连斯基:乌代表团已启程,谈判可能于今晚或明天举行