Linux之线程控制

线程控制

Linux下没有真正意义的线程, 只有轻量级进程的概念, 所以Linux OS只会提供轻量级进程创建的系统调用, 不会直接提供线程创建的接口.

用户在使用线程接口创建了一个线程, 实际是在中间的软件层把一个线程对应到内核里的一个LWP, 用户认为的线程在内核就是对应一个LWP. 所以Linux对于线程的解决方案是中间的软件层解决, 但这个软件层并不属于OS, 而是设计者封装的名为pthread原生线程库. 

pthread线程库是应用层原生线程库, 应用层指的是这个线程库并非系统调用接口提供的, 而是第三方为我们提供的, 原生指的是大部分Linux系统都会默认帮我们安装好该线程库.

线程创建

之前在线程的简单控制下, 我们只给线程传递了一个参数, 要想传递多个参数, 我们直接给线程传递一个对象即可:

我们创建了一个数据对象, 其中包括了线程名称, 创建时间, 执行的函数, 向pthread_create传参时只需把Thread_Data*转为void*, 然后在ThreadRoutine内使用的时候转换回去即可:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <functional>
#include <vector>

using func_t = std::function<void()>;
const int threadnum = 5;

class Thread_Data
{
public:
    Thread_Data(const std::string& s, uint64_t t, func_t f)
    :threadName(s)
    ,createTime(t)
    ,func(f)
    {}

    std::string threadName;
    uint64_t createTime;
    func_t func;
};

void Print()
{
    std::cout << "I am one of the threads!" << std::endl;
}

void* ThreadRountine(void* arg)
{
    Thread_Data* td = static_cast<Thread_Data*>(arg);//隐式类型转换
    usleep(1000);//调整一下函数执行次序

    while(true)
    {
        std::cout << "new thread" << " thread name: " << td->threadName << " create time: " << td->createTime << std::endl;
        td->func();

        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    std::vector<pthread_t> tids;

    for(int i = 1; i <= threadnum;i++)
    {
        char name[64];
        snprintf(name, sizeof(name), "%s-%d", "thread", i);
        Thread_Data* arg = new Thread_Data(name, time(nullptr), Print);
        
        pthread_create(&tid, nullptr, ThreadRountine, (void*)arg);
        tids.push_back(tid);

        sleep(1);
    }
    
    std::cout << std::endl;
    
    while(true)
    {
        std::cout << "I am main  thread" << std::endl;
        sleep(1);
    }

    return 0;
}

调用ps -aL可以发现有6个线程在运行, 运行结果有些不整齐, 不能确定哪个线程先被调度: 

 在运行函数内故意触发一个异常:

void* ThreadRountine(void* arg)
{
    Thread_Data* td = static_cast<Thread_Data*>(arg);//隐式类型转换
    usleep(1000);//调整一下函数执行次序

    while(true)
    {
        std::cout << "new thread" << " thread name: " << td->threadName << " create time: " << td->createTime << std::endl;
        td->func();

        if(td->threadName == "thread-4")
        {
            std::cout << td->threadName << " 触发了异常!!!!!" << std::endl;
            int a = 1;
            a /= 0; // 故意制作异常
        }
        sleep(1);
    }
}

如我们之前所说,  多线程如果一个线程崩溃, 整个进程都会崩溃:


线程获取自身id

pthread_ create函数会产生一个线程ID, 存放在第一个参数指向的地址中. 该线程ID和前面说的线程LWP不是一回事, 线程LWP属于进程调度的范畴, 因为线程是轻量级进程, 是操作系统调度器的最小单位, 所以需要一个数值来唯一表示该线程.
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴. 线程库的后续操作, 就是根据该线程ID来操作线程的.
线程库NPTL提供了pthread_ self函数, 可以获得线程自身的ID:

pthread_t pthread_self(void);

头文件: pthread.h

功能: 获取线程自己的tid

返回值: 返回自己的tid 


线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return. 这种方法对主线程不适用, 从main函数return相当于调用exit.
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程

方法一: return

线程执行的函数返回值是void*, 不需要返回值的话, 我们返回空指针就能终止该线程:

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    while(true)
    {
        std::cout << "I am main thread" << std::endl;
        sleep(1);
    }
    return 0;
}

方法二: pthread_ exit 

POSIX线程库提供了一个接口用于结束线程, void pthread_exit(void* retval);

void pthread_exit(void* retval);

头文件:pthread.h

功能:终止当前线程

参数:void* retval是线程的返回值, 目前暂时设置为空指针, 注意不要指向一个局部变量.

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    //return nullptr;
    pthread_exit(nullptr);
}

 运行结果和上面一样.

方法三: pthread_cancel

int pthread_cancel(pthread_t thread);

头文件:pthread.h

功能:取消标识符为thread的线程。

参数:pthread_t thread是需要取消的线程标识符

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

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    return (void *)10;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    sleep(2);

    int n = pthread_cancel(tid);//主线程休眠2秒后, 终止进程
    std::cout << "cancel success: " << tid << ", n: " << n << std::endl;

    return 0;
}

可以看到主线程把创建的子线程取消成功, 返回值为0 

注意: 如果直接调用exit()呢?

进程直接被终止, 所以多线程中线程退出不要轻易使用exit, 会导致整个进程都退出. 


线程等待与线程返回值

和进程一样, 线程在执行完毕时, 如果task_struct结构体不回收, 就会导致内存泄漏(类似未被回收的僵尸进程). 所以我们需要使用 pthread_join 函数将线程加入等待队列, 加入等待队列的线程会被回收, 但是回收的现象我们是看不到的.

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

头文件: pthread.h

功能: 将标识符为tid的线程加入等待队列。

参数: pthread_t thread是需要等待的线程标识符, void** retval是线程结束返回的信息, 是一个输出型参数

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

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    pthread_exit(nullptr);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    pthread_join(tid, nullptr);
    std::cout << "wait success: " << tid << std::endl;
    return 0;
}

即使不调用pthread_join, 线程依然会正常退出, 但是资源将不会被回收, 从而导致资源泄漏

返回值

1. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数, 正如我们上面那样.

如果需要线程给主线程一个返回值呢? 分为3种情况:

2. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值

3. 如果thread线程是自己调用pthread_exit终止的, value_ptr所指向的单元存放的是pthread_exit的参数

比如, 可以在threadRoutine函数中以void*的格式返回10:

但是这个变量要怎么让主线程接收到呢?

pthread_join函数有一个输出型参数void** retval, 由于是输出型参数, 需要被修改的内容是void*类型, 所以使用二级指针. 我们在主线程内定义一个void*类型的ret指针变量, 当一个线程被回收的时候, 将&ret传参, 它的返回值就会被放进这个ret里.

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

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    pthread_exit((void *)10);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    void* ret = nullptr;
    pthread_join(tid, &ret);
    std::cout << "wait success: " << tid << ", ret: "<< (int64_t)ret <<std::endl;
    return 0;
}

 不仅传参可以传递一个对象参数, 我们也可以返回一个对象:

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

class ThreadReturn
{
public:
    ThreadReturn(pthread_t id, const std::string &info, int code)
        : _id(id), _info(info), _code(code)
    {}

public:
    pthread_t _id;
    std::string _info;
    int _code;
};

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    ThreadReturn* ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);
    pthread_exit((void*)ret);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    std::cout << "wait success: " << tid << "n: " << n << std::endl;

    ThreadReturn* r = static_cast<ThreadReturn*>(ret);
    std::cout << "main thread get new thread info:" << r->_code << ", " << r->_id << ", " << r->_info << std::endl;
    delete r;

    return 0;
}

4. 如果thread线程被别的线程调用pthread_ cancel异常终掉, value_ ptr所指向的单元里存放的是常数-1, 库中其实是一个宏 PTHREAD_ CANCELED:

#define PTHREAD_CANCELED ((void *) -1)
#include<pthread.h>
#include<iostream>
#include<unistd.h>

class ThreadReturn
{
public:
    ThreadReturn(pthread_t id, const std::string &info, int code)
        : _id(id), _info(info), _code(code)
    {}
    
public:
    pthread_t _id;
    std::string _info;
    int _code;
};

std::ostream& operator<<(std::ostream& o, ThreadReturn* tr)
{
    o << tr->_code << ", " << tr->_id << ", " << tr->_info;
    return o;
}

void* threadRoutine(void*arg)
{
    usleep(1000);
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }

    ThreadReturn* ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);
    pthread_exit((void*)ret);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    sleep(1);
    int n = pthread_cancel(tid);//1秒后取消tid进程
    std::cout << "cancel success: " << tid << ", n: " << n << std::endl;

    void* ret = nullptr;
    n = pthread_join(tid, &ret);//等待被取消的进程
    std::cout << "wait success: " << tid << ", n: " << n << ", ret: " << (int64_t)ret << std::endl;
   
    return 0;
}


线程分离

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

但是有时根本不关心线程的返回值, 那pthread_join的阻塞式等待就会成为负担. 这个时候, 我们可以用pthread_detach函数将线程分离, 当线程退出时, 系统自动释放线程资源.

int pthread_detach(pthread_t thread);

头文件:pthread.h

功能:设置标识符为thread的线程分离状态。

参数:pthread_t thread是需要分离的线程标识符。

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

这个函数既可以分离线程组内的其他线程, 也可以调用pthread_self分离自己. 我们创建一个新线程, 让新进程在第一步就分离执行, 观察主线程的返回值判断能否成功回收它.

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

void* threadRoutine(void*arg)
{
    pthread_detach(pthread_self());//线程函数内部分离自己
    std::string name = static_cast<const char*>(arg);
    int cnt = 2;
    while(cnt--)
    {
        std::cout << "new thread is running, thread name: " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    sleep(1);

    int n = pthread_join(tid, nullptr);//等待被取消的进程
    std::cout << "wait success: " << tid << ", n: " << n << std::endl;

    return 0;
}

线程只要分离, 主线程就管不了它了, 而且我们发现确实不能回收该分离的线程了, 返回错误码22

其它线程也可以分离它, 只要有tid即可.

总结: 如果线程需要给主线程返回结果, 主线程用pthread_join回收; 主线程如果不关心线程的结果, 那么就可以把这个线程设为分离状态.


关于pthread进一步讨论

如何理解pthread_t? 

在正式讨论之前, 首先之前说过, 我们之前用到的关于线程控制的接口, 都不是系统直接提供的接口, 而是原生线程库pthread提供的.

Linux系统不存在真的线程, 而是用轻量级进程模拟线程, OS它不会提供创建线程的接口, 顶多会提供创建轻量级进程的接口, 但是我们平时只谈线程, 所以在系统之上有一层软件层(pthread库)提供线程接口. 所以Linux中的线程称为用户级线程, 内核中只会创建轻量级进程. 轻量级进程由内核进行管理, 那线程由谁管理呢? ----pthread库

所以管理就涉及先描述再组织, 就要涉及类似struct tcb的概念, 对内核中的LWP之类的属性作封装, 所以tcb是在pthread库内管理, 而不是内核中. 因为pthread是动态库, 属于用户空间.

既然程序运行期间要使用pthread库里的函数, 就要把pthread库动态加载进内存中, 并通过页表映射到地址空间中的共享区.

此时进程内部所有线程都可以看到动态库中的数据, 而我们所说的每个线程都有自己私有的栈, 其中除了主线程采用的栈是进程地址空间原生的栈其余的线程采用的就是中开辟的, 在共享区中用指针和size等属性维护这些堆空间. 当然, 共享区中也有每个线程自己的tcb, 还要有自己的线程局部存储(TLS), 下个话题再谈. (TLS和线程上下文不是一个东西, 线程上下文信息保存在PCB里属于内核空间不需要用户关心, 而TLS在共享区是属于用户空间的)

此外, 线程库是动态库, 动态库是共享的, 整个系统只有一份, 所以动态库的内部要管理整个系统的由多个用户创建的所有线程.

现在再来谈什么是pthread_t tid.

库为了能够快速的找到每一个线程, 所以提供了一个pthread_t tid, 代表线程属性集合在库中的起始地址. 因此我们想要找到一个用户级线程, 只需要知道线程的 tid, 之后就可以从该结构体中获取线程的各种信息(比如返回值).


线程局部存储

正如预期, 主线程和新线程能看到同样的全局变量: 

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

int g_val = 100; //全局变量本身就是被所有线程所共享的

void* threadRoutine(void* argc)
{
    std::string name = static_cast<const char*>(argc);
    while(true)
    {
        std::cout << name << ", g_val: " << g_val << std::endl;  
        g_val++;
        sleep(1);
    }    
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
    while(true)
    {
        std::cout <<  "main thread, " << "g_val: " << g_val << std::endl; 
        sleep(1);
    }

    pthread_join(tid, nullptr);
    return 0;
}

现在在g_val的前面加上__thread修饰:

再次编译运行发现只有新线程的g_val在发生变化, 实际上__thread是一个编译选项, 告诉编译器被__thread修饰的变量在每一个线程内保留一份自己的线程局部存储.


程序语言角度理解pthread

C++11内部的多线程的本质是对原生线程库的封装. 具体在其它文章展开.


fork和exec*

线程中可以fork吗? 线程中可以exec*程序替换吗? 如何理解?

1. 可以, 子进程将会成为调用 fork 的线程的副本.

2. 可以, 在一个多线程程序中调用exec*函数时, 它会替换当前进程为新的程序. 当前进程的所有线程都将被终止, 只有调用exec*的线程会继续执行(但实际上是作为新程序的入口点)。


  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux线程同步的方式有很多,以下是其中一些常见的: 1. 互斥锁(Mutex):用于保护共享资源,只允许一个线程访问共享资源。当一个线程获取到互斥锁后,其他线程就必须等待这个线程释放锁后才能获取锁。 2. 读写锁(Reader-Writer Lock):在读多写少的情况下,使用读写锁可以提高并发性能。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。 3. 条件变量(Condition Variable):用于等待某个条件满足时才执行。当一个线程等待某个条件变量时,它会被阻塞,直到另外一个线程发出信号,通知条件已经满足,该线程才会继续执行。 4. 信号量(Semaphore):用于控制同时访问共享资源的线程数量。信号量可以是计数信号量或二进制信号量。计数信号量可以用来控制多个线程同时访问一个资源的数量,而二进制信号量只允许一个线程访问资源。 5. 屏障(Barrier):用于让多个线程在某个点上同步执行。当多个线程执行到某个点时,它们会被阻塞,直到所有线程都到达该点,才会继续执行。 这些同步机制可以根据具体的场景选择使用。在实际开发中,需要考虑多线程的安全性、性能等因素,选择合适的同步机制来实现线程同步。 ### 回答2: 在Linux中,线程同步是指多个线程之间的操作需要协调,以确保它们在执行任务时能够按照预期的顺序进行。 线程同步的目的是确保多个线程共享的资源(如共享内存、文件、网络连接等)能够被有序地访问和操作,避免出现竞态条件和资源争夺等问题,确保程序的正确性和性能。 常见的线程同步机制包括互斥锁、条件变量、读写锁、信号量等。 互斥锁是最基本的一种线程同步机制,它可以确保在任何时候只有一个线程可以访问共享资源。当某个线程获取了互斥锁之后,其他线程必须等待该线程释放锁后才能继续执行。互斥锁通过使用标志位和原子操作来确保线程的互斥性。 条件变量是一种线程同步机制,它可以使线程在满足某些条件之前一直等待,从而避免忙等待和浪费资源。条件变量常与互斥锁一起使用,当共享资源不满足条件时,线程可以使用条件变量进入等待状态,直到该条件被满足,另一个线程发出信号来唤醒等待线程。 读写锁是一种用于多线程读写共享资源的机制,它允许多个线程同时进行读操作,但只允许一个线程进行写操作。读写锁可以提高程序的并发性能,但需要注意避免读-写之间的竞争条件。 信号量是一种基于计数器的线程同步机制,它可以控制共享资源的访问数量和顺序。信号量可以实现互斥锁、条件变量等多种功能,是一种比较通用的线程同步机制。 除了上述机制,Linux中还有其他一些线程同步工具和算法,如屏障、自旋锁、分段锁、标记等。不同的线程同步机制和算法适用于不同的场景和需求,需要根据具体情况进行选择和使用。 ### 回答3: 在Linux中,由于多线程同时访问共享资源可能导致竞争条件的出现,因此需要使用线程同步技术来避免这种情况。除了使用互斥锁和条件变量来实现线程同步之外,也可以使用Linux提供的信号量机制。 信号量是一个整数值,用于控制对共享资源的访问。它包括两个主要的操作:PV操作和初始化操作。PV操作分为两种:P操作(等待操作)和V操作(释放操作)。一个线程在访问共享资源之前,必须执行P操作,如果信号量的值为0,则该线程将被阻塞。当线程使用完共享资源后,必须执行V操作来释放信号量,并唤醒其他等待访问共享资源的线程。 在Linux中使用信号量需要包含头文件<sys/sem.h>,并使用semget函数创建一个新的信号量集。接着,使用semctl函数可对信号量进行初始化或者删除操作。使用semop函数可进行PV操作。 与互斥锁和条件变量相比,信号量机制的优点是可以在不同进程间进行线程同步,而且可以实现多个线程同时访问共享资源的问题。但是,使用信号量需要特别小心,因为它比互斥锁和条件变量更难调试,如果使用不当会导致死锁等问题。 总之,Linux提供了多种线程同步机制,开发人员需要根据实际需求选择合适的机制来避免竞争条件的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值