Linux -- 线程的优点、pthread 线程库
目录
线程的优点
pthread 线程库
前言
认识线程库
简单验证线程的独立栈空间
线程的优点
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少得多。
调度进程时,CPU 中有一个 cache(缓存,提高运行效率),CPU在虚拟地址转物理地址后,在内存中找到了一行代码,然而程序在运行时,比如运行到了第50行代码,下一次大概率会运行下一行代码,也就是第51行代码,也有可能跳转到其他代码,但是大概率还是运行下一行代码,所以系统把第50行代码的周边代码都加载到 cache 中,CPU 在运行时直接从 cache 中读取代码,提高 CPU 寻址效率。
进程切换时,会把当前缓存到 cache 的数据都切换掉,而线程切换时,寄存器中的数据会被切换,但 cache 中的数据不会被丢弃,大概率还是可以用的,线程切换只需要更改少量寄存器和栈指针等信息,而不需要像进程那样进行完整的上下文切换。所以线程切换时操作系统做的工作比进程的切换少。
创建一个新线程的代价要比创建一个新进程小得多,线程占用的资源比进程少得多。
进程是资源分配的基本单位,线程是调度的基本单位,当多个线程属于同一个进程时,它们会共享以下资源:
地址空间:包括代码段、数据段、堆区和栈区(每个线程有自己的栈)。所有线程都可以访问进程的整个虚拟地址空间,这意味着它们可以读写相同的全局变量和静态变量。
打开的文件描述符:如文件、网络连接等。所有线程都能操作这些描述符,因此对文件或网络连接的操作可以在不同线程间共享。
环境变量:进程启动时设置的环境变量是所有线程共有的。
内存映射:如果进程使用了内存映射文件或其他形式的内存映射,那么这些映射也是所有线程可见并可访问的。
信号处理程序:虽然信号通常是针对整个进程的,但某些信号(如SIGSEGV)可以由特定线程捕捉到,并且线程可以安装自己的信号处理器。
当前工作目录:所有线程共享同一进程的工作目录,任何线程改变工作目录都会影响其他线程。
用户ID和组ID:与安全性和权限相关的标识信息是所有线程共有的。
资源限制:例如最大文件大小、CPU时间等,这些限制适用于整个进程,因此也适用于所有线程。
因为多个线程共享资源,所以一个线程占用的资源比进程少。
但是线程也会有自己私有的资源:
栈空间:每个线程都有自己的栈,用于存储函数调用时的局部变量、返回地址等信息。这是线程之间保持独立性的关键之一,因为每个线程可以在其栈上进行独立的操作而不干扰其他线程。
寄存器集合(上下文数据):当线程被调度执行时,它有自己的寄存器集,包括程序计数器(PC)、堆栈指针和其他通用寄存器。这些寄存器保存了线程执行的状态信息。
线程ID:操作系统赋予每个线程一个唯一的标识符(TID),用于区分不同的线程。这个ID是线程私有的,因为它唯一地识别了一个线程。
线程优先级和调度属性:某些系统允许为每个线程设定独立的调度参数,如优先级和策略,这些属性影响线程的执行顺序和时间片分配。
pthread 线程库
前言
Linux 的线程是用进程模拟的,线程在Linux底层被视为轻量级进程。对于同一个进程中的新、主线程,可以看出线程的 tid 和 LWP 的数值是不一样的:
线程被创建、等待、分离、终止,且拥有独立的栈结构,这些都是系统在管理线程,
1、系统管理线程时,并没有对用户暴露 在系统中线程被视为轻量级进程的事实,用户认为那就是线程;
2、系统中没有线程的概念,只有轻量级进程的概念,用户却能创建管理、操作线程。
能实现以上两点是因为系统对底层的轻量级进程进行了封装,用户能操作线程都是因为有了库,所以在Linux中线程也叫做用户级线程。既然线程因库而起,就应该由库来维护。
认识线程库
为了管理线程,“先描述再组织”,定义线程的控制块 TCB,TCB是操作系统内核用来管理和调度线程的数据结构。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
pthread_create 的第一个参数 thread 是一个 pthread_t 类型的输出型参数,函数调用结束后,thread 指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID,所以 pthread_t 类型的线程 ID 实际上就是一个进程地址空间的一个地址!线程库的后续操作就是根据该线程 ID 来操作线程的,用户也可以调用 pthread_self 函数来获得线程自身的 ID。
线程库中还包含了一系列用于创建、管理和操作线程的函数、类和数据结构。具体来说,一个典型的线程库会提供以下组件:
1. 线程管理
创建线程:函数或构造函数用来启动一个新的线程,通常需要指定要在线程中执行的函数(即线程函数)。
- 示例:
pthread_create()
(POSIX Threads)销毁/终止线程:方法来结束线程的执行,可以是自然结束(当线程函数返回时),也可以是通过特定API强制结束。
- 示例:
pthread_cancel()
,std::thread::join()
或std::thread::detach()
等待线程完成:允许主线程或其他线程等待某个特定线程完成其任务。
- 示例:
pthread_join()
,std::thread::join()
2. 线程同步机制
为了确保多个线程之间安全地共享资源,线程库提供了各种同步工具:
互斥锁(Mutex):防止多个线程同时访问临界区代码段。
- 示例:
pthread_mutex_t
,std::mutex
读写锁(Read-write Locks):允许多个读者或单个写者访问资源。
- 示例:
pthread_rwlock_t
条件变量(Condition Variables):用于线程间的通信,一个线程可以在满足特定条件时唤醒另一个线程。
- 示例:
pthread_cond_t
,std::condition_variable
信号量(Semaphores):控制对有限数量资源的访问。
- 示例:
sem_t
(POSIX Semaphores)
3. 线程属性设置
- 设置线程属性:在创建线程之前,可以设定一些线程属性,如栈大小、调度策略等。
- 示例:
pthread_attr_t
(POSIX Threads)
4. 线程本地存储(TLS)
- 线程局部数据:为每个线程提供独立的数据副本,即使这些变量是在全局范围内声明的。
- 示例:
pthread_key_create()
,pthread_getspecific()
,pthread_setspecific()
,std::thread_local
(C++)
5. 高级特性
线程池:预先创建一组工作线程,以便快速响应任务请求而不必频繁创建和销毁线程。
- 示例: C++ 中可以通过第三方库如Boost实现。
并发容器:线程安全的数据结构,例如队列、堆栈、哈希表等。
- 示例:
std::shared_timed_mutex
,concurrent_queue
(Intel TBB)原子操作:提供无锁编程的支持,保证某些操作的原子性。
- 示例:
std::atomic
(C++)
6. 工具和辅助函数
当前线程信息:获取当前线程ID等信息。
- 示例:
pthread_self()
,std::this_thread::get_id()
线程调度:调整线程优先级或让出CPU给其他线程。
- 示例:
sched_yield()
,std::this_thread::yield()
简单验证线程的独立栈空间
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
#include<string>
void* newthreadRun(void* args)
{
std:string threadname=(char*)args;
int cnt=5;
while(true)
{
std::cout<<"I am "<<threadname<<",cnt: "<<cnt<<", &cnt: "<<&cnt<<std::endl;
cnt--;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1,nullptr,newthreadRun,(void*)"thread-1");
pthread_create(&tid2,nullptr,newthreadRun,(void*)"thread-2");
pthread_join(tid2,nullptr);
pthread_join(tid1,nullptr);
return 0;
}
同一局部变量的地址不同, 说明每个线程的栈空间都私有一份该变量: