linux线程概念
linux中线程如何理解
线程<=执行流<=进程
linux中的线程模拟进程实现(线程就是轻量级进程)
与独立的进程相比,线程创建和销毁的开销较小,因为它们共享相同的内存空间和资源。
线程是进程内的执行分支,线程的执行粒度比进程要细(只需要运行一部分代码)
我们认为线程是操作系统调度的基本单位(进程内部执行流资源)
进程:承担分配系统资源的实体(分给线程)
linux中用进程的内核数据结构模拟线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行(任何执行流都要有资源,地址空间是资源窗口)
- 在linux系统中,在cpu眼中,看到的pcb都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程 执行流
页表也有寄存器,帮忙找到页表
地址空间:进程的资源窗口(进程通过地址空间看到资源)
如何理解资源分配给线程
线程分配资源本质就是分配地址空间范围
- 虚拟地址不是一个整体(分为10+10+12)
- 页表也不是一整块,被拆成了三部分,第一个10查找第一个页表(因为是全0——全1,直接充当了页表的下标)
- 一级页表(页目录)里存二级页表地址
- 二级页表存放页框起始地址
- 前两个找到页框
- 12的那个部分在页框内进行索引(偏移量)找到制定物理内存
访问虚拟地址时,虚拟地址有没有调用内存:
1.查页目录时,2级页表不存在(中间10位),没用被加载到内存就发生缺页中断,二级页表和页框没有建立映射关系
2.内存管理基本单位:4kb
- c/c++中任意一个变量只有一个地址(取第一个字节为起始地址找偏移量。我们的地址只拿第一个就可以拿到数据(即使数据好几个字节),起始地址+类型=起始地址+偏移量
- cr2寄存器存放上次引起缺页中断异常的虚拟地址
- cr3寄存器存放页目录的地址(页表创建好后可以没有后边两个,但是页目录一定要有)
linux线程周边概念
- 创建线程只要创建pcb即可,只有最后一个pcb被干掉才会释放
- 线程在切换时一定要有上下文(页表地址空间不用切换)
- 线程执行的本质:进程调度
- cache中的在进程切换时会更改
- 线程被创建时也有自己的时间片(来自进程)(不能重新分配)=》进程总时间片不变
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速i/o操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- i/o密集型应用,为了提高性能,将i/o操作重叠。线程可以同时等待不同的i/o操作。
线程的缺点
性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。
- 如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些os函数会对整个进程造成影响。
编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高cpu密集型程序的执行效率
- 合理的使用多线程,能提高io密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
linux进程vs线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据: 线程id 一组寄存器 栈 errno 信号屏蔽字 调度优先级
进程具有独立上下文(独立运行)和栈(运行时变量数据(临时))(线程执行不会出现错乱)
进程的多个线程共享 同一地址空间,
进程和线程的关系如下图:
linux线程控制
线程创建
linux中没有很明确的线程概念,有轻量级进程概念,故没有直接提供线程的系统调用接口
我们需要使用第三方的pthread库
- pthread_t *thread:输出型参数(thread id)
- const pthread_atte_t(线程的属性)设为nullptr即可
- void*(*start_routine)(void*)新线程执行入口函数,将函数入口地址传进来可以调用函数
- void *arg:创建线程成功,新线程回调线程函数的时候,需要参数,这个参数就是给线程函数传递的
错误检查: 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。 pthreads函数出错时不会设置全局变量errno(而大部分其他posix函数会这样做)。而是将错误代码通 过返回值返回 pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误, 建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
lwp是系统层次概念,用户只用知道线程id
ps -al查看所有轻量级进程
示例代码
#include<iostream> #include<unistd.h> #include<pthread.h> using namespace std; void* threadrun(void* args) { while(1) { cout<<"new thread:"<<getpid()<<endl; sleep(1); } return nullptr; } int main() { pthread_t tid; pthread_create(&tid,nullptr,threadrun,nullptr); while(1) { cout<<"main thread"<<getpid()<<endl; sleep(1); } return 0; }
问题分析
输出混乱的表现:
- 输出内容混在一起(如"main thread201314new thread:")
- 换行符位置不正确
- 输出顺序不可预测
根本原因:
- 主线程和新线程同时向标准输出(stdout)写入数据
cout
不是线程安全的,多个线程同时使用会导致输出内容交叉- 线程调度由操作系统决定,执行顺序不确定
具体解释:
cout << "main thread" << getpid() << endl;
不是原子操作
它实际上分为多个步骤:
- 输出"main thread"
- 调用getpid()
- 输出pid值
- 输出换行符
在这些步骤之间,另一个线程可能插入自己的输出
在后续可以用互斥锁保护输出
线程等待
为什么需要线程等待? 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。 创建新的线程不会复用刚才退出线程的地址空间。
在多线程程序中,主线程最后退出是良好的编程实践,主要原因包括:
- 资源管理:确保所有子线程完成资源释放
- 结果收集:获取子线程的执行结果或状态
- 程序稳定性:避免子线程因主线程退出而意外终止
- 有序关闭:实现优雅的进程终止流程
#include<iostream> #include<unistd.h> #include<pthread.h> using namespace std; int g_val = 100; void show(const string& name) { cout<<name<<"say:"<<"hello thread"<<endl; } void *threadrun(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val); sleep(1); // int a = 10; // a /= 0; cnt--; if(cnt == 0) break; } return nullptr; } int main() { pthread_t tid; pthread_create(&tid,nullptr,threadrun,nullptr); // while(1) // { // cout<<"main thread"<<getpid()<<endl; // sleep(1); // } sleep(5); pthread_join(tid,nullptr); cout<<"main thread quit"<<endl; return 0; }
任何变量传参都会产生临时变量
返回值的拿到要用二级指针,因为返回类型是void*
线程终止
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。 2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 pthread_ canceled。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。 4
. 如果对thread线程的终止状态不感兴趣,可以传null给value_ ptr参数。
return
#include<iostream> #include<unistd.h> #include<pthread.h> using namespace std; int g_val = 100; void show(const string& name) { cout<<name<<"say:"<<"hello thread"<<endl; } void *threadrun(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val); sleep(1); // int a = 10; // a /= 0; cnt--; if(cnt == 0) break; } return (void*)1; } int main() { pthread_t tid; pthread_create(&tid,nullptr,threadrun,nullptr); // while(1) // { // cout<<"main thread"<<getpid()<<endl; // sleep(1); // } //sleep(5); void *retval; pthread_join(tid,&retval); //cout<<"main thread quit"<<endl; cout << "main thread quit ..., ret: " << (long long int)retval << endl; return 0; }
等待默认阻塞等待,不考虑异常情况(异常是进程考虑的)任何一个线程出异常整个进程都要被干掉,无法返回
exit是终止进程的,不能用来终止线程,任何一个线程调用exit直接整个进程退出了
可以用pthread_exit退出
void *threadrun(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val); sleep(1); // int a = 10; // a /= 0; cnt--; if(cnt == 0) break; } pthread_exit((void*)100); }
线程取消
#include<iostream> #include<unistd.h> #include<pthread.h> using namespace std; int g_val = 100; void show(const string& name) { cout<<name<<"say:"<<"hello thread"<<endl; } void *threadrun(void *args) { const char *name = (const char*)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val); sleep(1); // int a = 10; // a /= 0; cnt--; if(cnt == 0) break; } //pthread_exit((void*)100); } int main() { pthread_t tid; pthread_create(&tid,nullptr,threadrun,nullptr); sleep(1); pthread_cancel(tid); return 0; }
线程的参数和返回值不仅可以传递一般参数,也可以传递对象
#include <iostream> #include <unistd.h> #include <pthread.h> #include <string> using namespace std; class threadcalculator { public: // 请求结构体 struct request { int start; int end; string threadname; request(int s, int e, const string &name) : start(s), end(e), threadname(name) {} }; // 响应结构体 struct response { int result; int exitcode; response(int r = 0, int e = 0) : result(r), exitcode(e) {} }; // 静态线程函数 static void* threadfunc(void* arg) { request* req = static_cast<request*>(arg); response* resp = new response(); for(int i = req->start; i <= req->end; ++i) { cout << req->threadname << " is running, calculating... " << i << endl; resp->result += i; usleep(100000); // 100ms延迟 } delete req; return resp; } // 启动计算 response calculate(int start, int end, const string& name) { pthread_t tid; request* req = new request(start, end, name); pthread_create(&tid, nullptr, &threadcalculator::threadfunc, req); void* ret; pthread_join(tid, &ret); response* resp = static_cast<response*>(ret); response result = *resp; delete resp; return result; } }; int main() { threadcalculator calculator; auto result = calculator.calculate(1, 100, "worker thread"); cout << "calculation result: " << result.result << ", exit code: " << result.exitcode << endl; return 0; }
语言已经把系统调用封装了
// 目前,我们的原生线程,pthread库,原生线程库 // // c++11 语言本身也已经支持多线程了 vs 原生线程库 void threadrun() { while(true) { cout << "i am a new thead for c++" << endl; sleep(1); } } int main() { thread t1(threadrun); t1.join(); return 0; }
线程id
pthread_ create函数会产生一个线程id,存放在第一个参数指向的地址中。该线程id和前面说的线程id 不是一回事。
前面讲的线程id属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程id, 属于nptl线程库的范畴。线程库的后续操作,就是根据该线程id来操作线程的。
线程库nptl提供了pthread_ self函数,可以获得线程自身的id:
#include <iostream> #include <string> #include <pthread.h> #include <cstdlib> #include <unistd.h> using namespace std; std::string tohex(pthread_t tid) { char hex[64]; snprintf(hex, sizeof(hex), "%p", tid); return hex; } void *threadroutine(void *args) { while(true) { cout << "thread id: " << tohex(pthread_self()) << endl; sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, nullptr, threadroutine, (void*)"thread 1"); cout << "main thread create thead done, new thread id : " << tohex(tid) << endl; pthread_join(tid, nullptr); return 0; }
线程的概念是库给我们维护的,注定了线程库要维护多个线程属性集合,线程库要管理这些线程
原始线程库要加载到内存中
每一个线程的库级别的tcb的其实地址叫做线程的tid
除了主线程,其他线程的独立栈都在共享区,具体来说是pthread库中,tid指向的用户tcb中
tid指向的那一块就是tcb
每个线程是个执行流,为了不让受干扰,每个执行流要有自己的栈结构
多线程
__thread编译选项
只能定义内置类型,可以在线程启动前获得想要的属性,在线程内直接获取即可,不用再调系统调用了
#include <iostream> #include <pthread.h> #include <unistd.h> #include <vector> #include <string> using namespace std; // 使用__thread修饰的线程局部变量 __thread int g_val = 100; void* threadroutine(void* args) { int thread_num = *(int*)args; // 每个线程有自己的g_val副本 g_val += thread_num; cout << "thread " << thread_num << ": g_val = " << g_val << ", address: " << &g_val << endl; return nullptr; } int main() { vector<pthread_t> tids; const int num = 3; for (int i = 0; i < num; i++) { pthread_t tid; int* arg = new int(i); pthread_create(&tid, nullptr, threadroutine, arg); tids.push_back(tid); } for (auto tid : tids) { pthread_join(tid, nullptr); } cout << "main thread g_val: " << g_val << endl; return 0; }
全局变量是共享的:
#include <iostream> #include <pthread.h> #include <unistd.h> #include <vector> #include <string> using namespace std; // 全局变量,所有线程共享 int g_val = 100; void* threadroutine(void* args) { int thread_num = *(int*)args; // 每个线程都会修改同一个全局变量 g_val += thread_num; cout << "thread " << thread_num << ": g_val = " << g_val << ", address: " << &g_val << endl; sleep(1); return nullptr; } int main() { vector<pthread_t> tids; const int num = 3; for (int i = 0; i < num; i++) { pthread_t tid; int* arg = new int(i); pthread_create(&tid, nullptr, threadroutine, arg); tids.push_back(tid); sleep(1); } for (auto tid : tids) { pthread_join(tid, nullptr); } cout << "final g_val: " << g_val << endl; return 0; }
每个线程有自己的栈空间
#include <iostream> #include <pthread.h> #include <unistd.h> #include <vector> using namespace std; // 全局变量,用于存储线程栈变量的地址 vector<void*> g_stack_addresses; void* threadroutine(void* args) { int thread_num = *(int*)args; // 局部变量(栈变量) int stack_var = thread_num * 100; cout << "thread " << thread_num << ": stack_var address = " << &stack_var << ", value = " << stack_var << endl; // 保存栈变量地址到全局vector(仅用于演示,实际开发中要小心) g_stack_addresses.push_back(&stack_var); // 模拟线程工作 sleep(1); // 注意:这里stack_var的地址在函数返回后将无效 // 只是演示每个线程有自己的栈空间 return nullptr; } int main() { const int num = 3; vector<pthread_t> tids; // 主线程的栈变量 int main_stack_var = 999; cout << "main thread: stack_var address = " << &main_stack_var << endl; // 创建子线程 for (int i = 0; i < num; i++) { pthread_t tid; int* arg = new int(i); // 动态分配,避免栈地址共享问题 pthread_create(&tid, nullptr, threadroutine, arg); tids.push_back(tid); } // 等待所有线程结束 for (auto tid : tids) { pthread_join(tid, nullptr); } // 打印所有线程的栈变量地址 cout << "\nall stack variable addresses:" << endl; for (size_t i = 0; i < g_stack_addresses.size(); i++) { cout << "thread " << i << " stack_var @ " << g_stack_addresses[i] << endl; } return 0; }
主线程和每个子线程数据地址不同
但是栈与栈之间没有秘密
#include <iostream> #include <pthread.h> #include <unistd.h> #include <vector> #include <string> using namespace std; // 全局指针,用于演示栈共享的危险性 int* shared_ptr = nullptr; void* threadroutine(void* args) { int thread_num = *(int*)args; int stack_var = thread_num * 10; // 栈变量 if (thread_num == 1) { shared_ptr = &stack_var; // 线程1将自己的栈变量地址暴露出去 } sleep(1); // 确保线程1先运行 if (thread_num == 2) { // 线程2尝试访问线程1的栈变量(危险!) if (shared_ptr) { cout << "thread 2 accessing thread 1's stack: " << *shared_ptr << endl; } } return nullptr; } int main() { pthread_t t1, t2; int num1 = 1, num2 = 2; pthread_create(&t1, nullptr, threadroutine, &num1); pthread_create(&t2, nullptr, threadroutine, &num2); pthread_join(t1, nullptr); pthread_join(t2, nullptr); return 0; }
线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
线程的分离可以由主线程来做也可以由线程自己分离
共享一部分资源但不是“一家人”了,出了问题由系统自动回收
示例代码
#include <iostream> #include <cstdio> #include <cstring> #include <vector> #include <unistd.h> #include <pthread.h> using namespace std; #define num 3 // int *p = null; // __thread int g_val = 100; __thread unsigned int number = 0; __thread int pid = 0; struct threaddata { string threadname; }; // __thread threaddata td; string tohex(pthread_t tid) { char buffer[128]; snprintf(buffer, sizeof(buffer), "0x%lx", tid); return buffer; } void initthreaddata(threaddata *td, int number) { td->threadname = "thread-" + to_string(number); // thread-0 } // 所有的线程,执行的都是这个函数? void *threadroutine(void *args) { pthread_detach(pthread_self()); // int test_i = 0; threaddata *td = static_cast<threaddata *>(args); // if(td->threadname == "thread-2") p = &test_i; string tid = tohex(pthread_self()); int pid = getpid(); int i = 0; while (i < 10) { cout << "tid: " << tid << ", pid: " << pid << endl; // cout << "pid: " << getpid() << ", tid : " // << tohex(number) << ", threadname: " << td->threadname // << ", g_val: " << g_val << " ,&g_val: " << &g_val <<endl; sleep(1); i++; } delete td; return nullptr; } int main() { // 创建多线程! vector<pthread_t> tids; for (int i = 0; i < num; i++) { pthread_t tid; threaddata *td = new threaddata; initthreaddata(td, i); pthread_create(&tid, nullptr, threadroutine, td); tids.push_back(tid); //sleep(1); } sleep(1); // 确保复制成功 // for(auto i : tids) // { // pthread_detach(i); // } // cout << "main thread get a thread local value, val: " << *p << ", &val: " << p << endl; for (int i = 0; i < tids.size(); i++) { int n = pthread_join(tids[i], nullptr); printf("n = %d, who = 0x%lx, why: %s\n", n, tids[i], strerror(n)); } return 0; }
关键特性
特性 | 描述 |
---|---|
自动资源回收 | 分离线程终止后系统自动回收其资源(线程id、栈等) |
不可连接(join) | 分离后不能再使用pthread_join等待该线程 |
立即生效性 | 若线程已终止,分离操作会立即触发资源回收 |
使用场景
- 不需要获取线程返回值时
- 不关心线程何时结束时
- 需要避免僵尸线程(类似僵尸进程的概念)
注意事项
- 不要重复分离:对同一线程多次detach会导致未定义行为
- 分离后禁止join:尝试join已分离线程会返回einval错误
- 主线程退出影响:若主线程先退出,分离的子线程也会被强制终止
- 资源释放时机:分离线程的资源释放可能稍有延迟(由系统调度决定)
- 分离操作是不可逆的
分离 vs 非分离线程对比
特性 | 分离线程 | 非分离线程(默认) |
---|---|---|
资源回收 | 自动 | 需手动pthread_join |
可连接性 | 不可join | 可join |
线程返回值 | 无法获取 | 可通过pthread_join获取 |
僵尸线程风险 | 无 | 未join时会产生 |
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论