【Linux线程】

发布于:2024-05-06 ⋅ 阅读:(25) ⋅ 点赞:(0)

线程是操作系统的一个执行流

在计算机操作系统中,线程是一个执行流,是被CPU调度的一个基本最小单位。是在一个执行的单一进程上下文的控制逻辑流。一个进程可以有多个线程,可以并发执行不同的任务。

并发编程

如果一个单核的计算机要在普通用户层面上看到有多个程序运行,就需要采用并发编程的技术来实现,可以通过进程来创建一个子进程或多个子进程来运行。操作状态给每个进程分配一定的时间片来让进程给CPU执行。
两个客户端和一个服务端之间的工作。
在这里插入图片描述

进程并发的优劣

对于父子进程来说,各自有一份独立的虚拟地址空间,当在内存的共享文件有其中的一个进程对数据进行修改的时候,操作系统就会对在内存的文件进行写实拷贝。这样就不会造成父子进程之间的数据混淆和覆盖的情况,这是并发进程的优点。但是对于进程来说,他们是独立的,所有进程间的交互就变得复杂,共享信息的时候需要进程间通信的机制。而且基于进程的并发设计的程序运行效率慢。

基于线程的并发编程

线程是一个执行在一个运行的进程之中的上下文的一个执行流。每个线程都是由进程创建,所以进程获取操作系统的资源,然后分配给线程,让线程可以被CPU进行调度。线程会共享进程的内存空间和其他资源。具体来说,同一个进程下的多个线程共享父进程的地址空间,包括代码段、数据段、堆和栈等内存区域。这意味着线程之间可以访问和操作这些共享的内存区域,包括全局变量、静态变量和动态分配的堆内存等。但每个线程都有之间独立的线程上下文来执行它的任务,由独立的栈和独立的寄存器中数据还有线程的id,错误码,优先级等。
在这里插入图片描述
每个执行流都可以对共享区的数据进行读写操作。
在这里插入图片描述
线程不同与进程,线程上下文要比进程小得多,占用的资源小。它在进程间执行,因为上下文下,所以线程间的切换要比进程间的切换要快的多,这就是多线程对比与多进程的优势。一个进程可以创建许多的进程,所以线程的创建先是主进程创建。这些线程都可以使用一些数据结构来对这些线程进行管理,把这些线程组织成为线程池。

Linux当中的线程

在linux当中,线程被称为轻量级进程,轻量级进程是基于进程实现,直接复用了进程的原理,但也可以实现和多线程的一样的功能。是一个多进程中存在多个轻量级的线程的多个执行流。
在这里插入图片描述

线程的创建

使用Posix的接口创建线程

使用pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void (*start_routine) (void *), void *arg);
//创建成功返回值为0,创建失败放回错误码,错误信息被设置
#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* thread(void *args)
{
    std::cout << "我是线程" << std::endl;
    return nullptr;
}

int main()
{
    pthread_t pid;
    pthread_create(&pid, nullptr, thread, nullptr);
    //使用pthread_create函数接口时我要把pid的地址传参到第一个参数
    //第二个参数设置为nullptr或NULL,让操作系统自动的帮我们找到物理地址的页框,让线程占用该空间
    //第三个参数设置为一个返回值为void*,参数也时void*的函数
    //第四个参数为第三个参数的函数的参数,该参数作为指针,如果想把多个参数传递到线程,可以把参数进行封装成一个对象,然后把该对象的地址转化为void*的类型进行传参,同样。返回值也可以返回一个多个参数的对象的指针。
    while(true)
    {
        std::cout << "我是主进程" << std::endl;
        sleep(2);
    }
    return 0;
}

在Linux中,线程的库并不属于操作系统的系统调用的库,而是第三方的库。所以我们编译的时候需要链接第三方的库。

g++ os_pthread.cpp -std=c++11 -pthread
//-pthread就是链接第三方的库
while :; do ps -al | head -1 && ps -al | grep threadname; sleep 1;done
//查看线程的id

在这里插入图片描述

pthread_join对线程进行等待

上述的代码,主进程实际上是一个不退出的进程。如果不让主进程一直的执行下去,主进程结束退出,如果线程要处理的任务的时间是100个单位,而主进程创建线程完成后进程就立即结束,则线程也跟着退出。因为线程是进程里的一个执行流,使用着进程的资源。进程退出了,资源被回收,线程就无法继续执行了,所以,我们可以使用接口pthread_join像父进程等待子进程那样,主线程等待线程。

int pthread_join(pthread_t thread, void **retval)
//retval可以获得线程退出时的信息
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>

class ThreadReturn
{
public:
    ThreadReturn(pthread_t& pid,std::string threadmessage,int code)
        :_pid(pid),_threadmessage(threadmessage),_code(code)
    {}
public:
    pthread_t _pid;
    std::string _threadmessage;
    int _code;
};
void* thread(void *args)
{
    int cnt = 6;
    ThreadReturn *td = static_cast<ThreadReturn*>(args);
    std::cout << "pid: " << td->_pid << " threadmessage: " << td->_threadmessage << " code: " << td->_code << std::endl;
    while(cnt--)
    std::cout << "我是线程" << "cnt "<< cnt << std::endl;
    ThreadReturn* ret = new ThreadReturn(td->_pid, "线程退出", 11);
    pthread_exit(ret);
    //return ret;//也可以采用return来放回
}

int main()
{
    pthread_t pid;
    ThreadReturn* td = new ThreadReturn(pid, "线程开始", 10);
    pthread_create(&pid, nullptr, thread, td);
    //传了一个类的对象的指针
    void* ret = nullptr;
    pthread_join(pid, &ret);//对线程进行等待
    //使用一个类型为void*的变量来获取线程放回的信息
    ThreadReturn* ans = static_cast<ThreadReturn*>(ret);
    //用一个对应对象的指针变量来接收强转的ret
     std::cout << "pid: " << ans->_pid << " threadmessage: " << ans->_threadmessage << " code: " << ans->_code << std::endl;
    return 0;
}

pthread_exit和pthread_cancel

着两个函数接口都是让线程退出的函数接口。当线程调用pthread_exit时,线程会直接终止,如果时主线程调用,则会等待其他线程结束时再终止整个进程。如果使用pthread_cancel会对特点的id线程进行终止。

void pthread_exit(void *retval);
int pthread_cancel(pthread_t thread);
//取消线程成功返回值为0,pthread_join时的线程的返回值为-1//PTHREAD_CANCELED;

pthread_detach线程分离

int pthread_detach(pthread_t thread);
//分离成功放回0,失败错误码被设置

该函数最好在线程执行的函数体中进行编写。
任何时刻,线程都是课结合或者时分离的。结合的线程课被其他的线程所影响。只有当自己手动退出或主线程退出的时候,操作系统才回收资源。如果一个线程被脱离了,就不能被其它的线程所影响,分离的线程资源只有它自己结束的时候才能自动释放并入系统回收。就好比网络上的服务器,一般来说,一旦执行了,就不会退出。所以为了避免其他的线程出现的异常信号影响该线程,就要进行线程的分离,不受影响。线程一旦脱离了,pthread_join的返回值就是22。无意义。如果主线程不需要线程的返回值,就可以设置为分离。

void* thread(void *args)
{
	pthread_detach(pthread_self());
	//pthread_self函数获取自己的id
    int cnt = 6;
    ThreadReturn *td = static_cast<ThreadReturn*>(args);
    std::cout << "pid: " << td->_pid << " threadmessage: " << td->_threadmessage << " code: " << td->_code << std::endl;
    while(cnt--)
    std::cout << "我是线程" << "cnt "<< cnt << std::endl;
    ThreadReturn* ret = new ThreadReturn(td->_pid, "线程退出", 11);
    pthread_exit(ret);
    //return ret;//也可以采用return来放回
}

注意事项

如果线程触发了异常的信号,整个线程和主进程也会跟着异常退出,因为该主线程的其他线程和自己使用的是同一个信号集。

原生线程库,详谈Linux的线程

上述的函数接口都不是系统直接提供的接口,而是pthread原生库提供的接口。pthread原生库在Linux操作系统要有的。因为Linux的底层实现并没有线程的,而是使用轻量级进程(LWP)来让上层用户来作为线程使用的。而用户又不能很好的使用轻量级进程来实现多线程的功能,所以可以使用pthread原生库来对Linux系统的轻量级进程进行管理。上层就可以用用户级的线程了。
在这里插入图片描述
在这里插入图片描述

int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, ...
                 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

clone函数就是Linux创建进程的函数接口,通过对flags参数的不同来创建进程和轻量级进程,fork的底层就有clone,pthread线程库也有该函数接口来创建线程。fn就是pthread_create函数的第三个参数,函数调用方法的指针,arg就是第四个参数。clone会在地址空间申请一段空间做为线程的独立栈。默认地址空间的栈为主线程的栈空间。
在这里插入图片描述

pthread库管理线程

pthread库要作为动态库加载到内存当中,然后通过页表映射到进程地址空间。
在这里插入图片描述
pthread库加载到内存作为动态库,再通过页表映射到虚拟地址空间,所有的进程都公有该线程库,所以Linux的轻量级进程就会由pthread来进行管理。
在这里插入图片描述
在这里插入图片描述
同一个进程创建的线程pid都是一样的,但是LWP不一样。

在这里插入图片描述

所以得出结论,线程id是虚拟地址空间的地址。可以使用%lx来对线程id进行观察。

printf("tid address: %lx\n",tid);