赞
踩
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
在Linux系统里如何保证让正文部分的代码可以并发的去跑呢?
世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!
提示:以下是本篇文章正文内容,下面案例可供参考
无论是多进程,还是多线程,它的核心思想:把串行的东西变成并行的东西。
进程地址空间上布满了虚拟地址,进程地址空间以及上面的虚拟地址的本质是一种资源。
我们之前说的,代码可以并行或并发的去跑,比如:父子进程的代码是共享的,数据写实拷贝各自一份,所以可以让父子执行不同的代码块,这样就可以将代码块进行两个各自调度运行了。
目标不是为了多进程,是为了多执行流并发执行,为了让多个进程之间可以并发的去跑相同或不同的代码。
线程跟进程一样,也是要被调度的。
线程在一个进程内部,就意味着一个进程内部可能会存在很多个线程。
如果我们要设计线程,OS也要对线程进行管理!先描述,再组织。描述线程:线程控制块(struct TCB),要保证线程被OS管理,比如用链表将线程管理,还要保证进程PCB和这些线程进行关联,PCB中的对应的指针指向对应的线程,但是这样是非常复杂的。
性能损失
健壮性降低
缺乏访问控制
编程难度提高
什么是进程呢?
- 内核的数据结构+进程的代码和数据(也就是一个或者多个执行流、进程地址空间、页表和进程的代码和数据)
- 线程(task_struct)叫做进程内部的一个执行分支。
- 线程是调度的基本单位。
- 进程的内核角度:承担分配系统资源的基本实体。
- 不要站在调度角度理解进程,而应该站在资源角度理解进程。
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
讲一个故事:
共享同一地址空间,因此代码段(Text Segment)、数据段(Data Segment)都是共享的:
- 如果定义一个函数,在各线程中都可以调用;
- 如果定义一个全局变量,在各线程中都可以访问到;
除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
CPU在选择执行流去调度的时候,用不用区分一个进程内部唯一的执行流呢?还是一个进程内部某一个执行流呢?
不需要管。因为线程也有PCB、进程地址空间、页表、进程代码和数据,与进程一致,都是执行流,不需要区分。
先描述,再组织!用struct page结构体来描述内存中的4KB数据块,假设有万4GB,4GB内存中有100多个4KB的数据块,用struct page mem[100多万]数组来组织,天然的每一个4KB就有了它的编号(物理地址),编号就是下标,对内存进行管理,就是对该数组的增删查改。未来加载程序时,有多少个4KB的数据块要加载,我们就在内存当中申请多少个数据块,将程序数据块中的内容加载到数组下标中的数据块空间中。
以32位平台下为例:
将虚拟地址转换成物理地址:
结论:给不同的线程分配不同的区域,本质就是给让不同的线程,各自看到全部页表的子集。就是让不同的线程看到不同的页表。
- 功能:创建一个新的线程
- 原型
- int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*
- (*start_routine)(void*), void* arg);
- 参数
- 参数1:输出型参数,创建成功会带出新线程id;
- 参数2: 设置线程的属性,attr为NULL表示使用默认属性
- 参数3:返回值为void* ,参数为void* 的函数指针,让新线程来执行这个函数方法
- 参数4:传递给线程函数的参数,参数会传递到参数3中去
- 返回值:成功返回0;失败返回错误码
内部创建线程之后,将来会有两个执行流,一个是主线程,一个是新创建的线程,新创建的线程会回调式的调用参数3(函数指针)。
错误检查:
- #include <iostream>
- #include <pthread.h> // 在Liinux中使用线程,要包含头文件
- #include <unistd.h>
- #include <sys/types.h>
-
- // 新线程
- void* newthreadrun(void* args)
- {
- while (true)
- {
- std::cout << "I am new thread, pid: " << getpid() << std::endl;
- sleep(1);
- }
- }
-
- int main()
- {
- pthread_t tid;// 线程id
- // 创建新线程
- pthread_create(&tid, nullptr, newthreadrun, nullptr);
-
- while (true)
- {
- std::cout << "I am main thread, pid: " << getpid() << std::endl;
- sleep(1);
- }
- }
用户知道"轻量级进程"这个概念吗?
没有。用户只认进程和线程。其实轻量级进程就是线程。
pthread库不属于OS内核,只要是库就是在用户级实现的,所以Linux的线程也别叫做用户级线程。所以编写多线程时,都必须要链接上这个pthread库:-lpthread
- testthread:testThread.cc
- g++ - o $@ $^ -std=c++11 -lpthread
- .PHONY:clean
- clean:
- rm - f testthread
- man pthread_self // 那个线程调用pthread_self()函数,就获取那个线程的id
- pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质 就是一个进程地址空间上的一个地址。
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
功能:线程终止
原型
void pthread_exit(void* value_ptr);// 那个线程调用该函数,就终止那个线程
参数
value_ptr: value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函 数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread : 线程ID
返回值:成功返回0;失败返回错误码
为什么需要线程等待?
man pthread_join
int pthread_join(pthread_t thread, void **value_ptr); 等待一个已经结束的线程
- 参数1:等待指定的一个线程,如果该线程没有退出,会阻塞式等待,若该线程退出了,则返回等待的结果;
- 参数2:输出型参数,拿到的是新线程对应的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
把一个线程设置成分离:
man pthread_detach
int pthread_detach(pthread_t thread);
参数:要分离哪一个线程的id
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
- // 同一个进程内的线程,大部分资源都是共享的. 地址空间是共享的!
- // 比如:初始化和未初始化区域、还有正文部分没有被线程分走的其它代码也是这个进程所有的线程共享的。
- int g_val = 100;
-
- // 将新的线程id转换成16进制的形式
- std::string ToHex(pthread_t tid)
- {
- char id[64];
- snprintf(id, sizeof(id), "0x%lx", tid);
- return id;
- }
-
- // 线程退出
- // 1. 代码跑完,结果对
- // 2. 代码跑完,结果不对
- // 3. 出异常了 --- 重点 --- 多线程中,任何一个线程出现异常(div 0, 野指针), 都会导致整个进程退出!
- // ---- 多线程代码往往健壮性不好
- void *threadrun(void *args)
- {
- // 将函数的参数传递过来,字符串的地址来用于初始化新线程的名字
- std::string threadname = (char*)args;
- int cnt = 5;
- while (cnt)
- {
- printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
-
- // std::cout << threadname << " is running: " << cnt << ", pid: " << getpid()
- // << " mythread id: " << ToHex(pthread_self())
- // << "g_val: "<< g_val << " &g_val: " << &g_val << std::endl;
- g_val++;
- sleep(1);
- // int *p = nullptr;
- // *p = 100; // 故意一个野指针
- cnt--;
- }
- // 1. 线程函数结束 法1:(return)
- // 2. 法2:pthread_exit()
- // pthread_exit((void*)123);// 终止新线程
- // exit(10); // 不能用exit终止线程,因为它是终止进程的.
- return (void*)123; // warning
- }
- // 主线程退出 == 进程退出 == 所有线程都要退出(资源都被释放)
- // 1. 往往我们需要main thread最后结束
- // 2. 线程也要被"wait", 要不然会产生类似进程哪里的内存泄漏的问题(线程是需要被等待的)
- int main()
- {
- // 1. id
- pthread_t tid;// pthread_t就是一个无符号的长整型
- pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");
- // 参数1:输出型参数,得到的是新线程的id
- // 法3:
- // 在主线程中,你保证新线程已经启动
- // sleep(2);
- // pthread_cancel(tid);
- // 取消tid线程,那么pthread_join()函数拿到的就是线程的退出码-1,-1就是宏,-1表示这个线程是被取消的
- // 2. 新和主两个线程,谁先运行呢?不确定,由调度器决定
- int cnt = 10;
- while (true)
- {
- std::cout << "main thread is running: " << cnt << ", pid: "
- << getpid() << " new thread id: " << ToHex(tid) << " "
- << " main thread id: " << ToHex(pthread_self())
- << "g_val: "<< g_val << " &g_val: " << &g_val << std::endl;
- printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
- sleep(1);
- cnt--;
- }
-
- // 如果主线程比新线程提前退出了呢?
- void* ret = nullptr;// void:是不能定义变量的;void*:能定义变量,指针变量是已经开辟了空间的
- // PTHREAD_CANCELED; // (void*)-1
- // 我们怎么没有像进程一样获取线程退出的退出信号呢?只有你手动写的退出码
- // 所等的线程一旦产生信号了,线程所在的进程就被干掉了,所以pthread_join没有机会获得信号。
- // 所以pthread_join()函数不考虑线程异常情况!
- int n = pthread_join(tid, &ret);
- std::cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;
- return 0;
- }
新线程所产生的异常由父进程去考虑。
- std::string ToHex(pthread_t tid)
- {
- char id[64];
- snprintf(id, sizeof(id), "0x%lx", tid);
- return id;
- }
-
- __thread uint64_t starttime = 100;
- // __thread int tid = 0;
- // 全局变量g_val属于已初始化区域,是所有线程共享的资源
- // __thread:让这个进程中所有的线程都私有一份g_val全局变量
- // __thread:编译器在编译时将g_val变量拆分出来,放到了每个线程的局部存储空间内
- int g_val = 100;
-
- // 主线程一直在等待新线程,在等待期间,不会创造价值,所以有类似于非阻塞等待:
- // 线程是可以分离的: 默认线程是joinable(需要被等待)的。
- // 如果我们main thread不关心新线程的执行信息,我们可以将新线程设置为分离状态:
- // 你是如何理解线程分离的呢?底层依旧属于同一个进程!只是不需要等待了
- // 一般都希望mainthread 是最后一个退出的,无论是否是join、detach
- void *threadrun1(void *args)
- {
- starttime = time(nullptr);
- // pthread_detach(pthread_self());// 该线程自己分离自己,则主线程不会再等待新线程
- std::string name = static_cast<const char *>(args);
-
- while(true)
- {
- sleep(1);
- printf("%s, g_val: %lu, &g_val: %p\n", name.c_str(), starttime, &starttime);
- }
-
- return nullptr;
- }
-
- void *threadrun2(void *args)
- {
- sleep(5);
- starttime = time(nullptr);
-
- // pthread_detach(pthread_self());
- std::string name = static_cast<const char *>(args);
-
- while(true)
- {
- printf("%s, g_val: %lu, &g_val: %p\n", name.c_str(), starttime, &starttime);
- sleep(1);
- }
-
- return nullptr;
- }
-
- int main()
- {
- pthread_t tid1;
- pthread_t tid2;
- pthread_create(&tid1, nullptr, threadrun1, (void *)"thread 1");
- pthread_create(&tid2, nullptr, threadrun2, (void *)"thread 2");
-
- pthread_join(tid1, nullptr);
- pthread_join(tid2, nullptr);
- // pthread_detach(tid);// 可以由主线程来进行使新线程进行分离
-
- // std::cout << "new tid: " << tid << ", hex tid: " << ToHex(tid) << std::endl;
- // std::cout << "main tid: " << pthread_self() << ", hex tid: " << ToHex(pthread_self()) << std::endl;
- // int cnt = 5;
- // while (true)
- // {
- // if (!(cnt--))
- // break;
- // std::cout << "I am a main thread ..." << getpid() << std::endl;
- // sleep(1);
- // }
- // std::cout << "main thread wait block" << std::endl;
- // 主线程要等待新线程,否则会出现类似于僵尸进程的问题
- // 若是新线程是分离的状态,等待的话,会出错返回
- int n = pthread_join(tid, nullptr);
- std::cout << "main thread wait return: " << n << ": " << strerror(n) << std::endl;
- return 0;
- }
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
在CPU内部,每一次我们读取当前进程的代码和数据时,CPU上硬件上有一个cache,cache是一个集成在CPU内部的,一段比寄存器容量大的多的一段缓存区,当CPU将虚拟地址转物理地址,进行寻址的时候,找到物理内存中的代码,只找到了一行代码,但是下一次大概率还需要这一行代码的下一行代码,所以会将这块相关的代码全部搬到CPU内部的cache中缓存起来,从此CPU访问代码数据的时候,不用从内存中读取了,而直接从CPU中较近的cache中读取,从而大大提高CPU寻址的效率。
进程间切换时,假设A进程被切换下去,那么CPU内部cache中的数据就被清空,由新切换上来的B进程来重新填充cache中的代码和数据,这个过程很漫长,因此进程间的切换,成本很高。对于线程切换来说,因为进程地址空间、页表、进程的代码和数据都是共享的,所以CPU中cache的缓存区中的数据不需要被丢弃,所以线程切换的成本要比进程要低。
一组寄存器:
线程都有自己的临时变量,在C语言中在函数中的临时变量都是在栈区上保存的,比如:主线程要形成自己的临时变量,新线程也要形成自己的临时变量,函数调用要压栈和出栈,如果两个线程使用的是同一个进程地址空间上的栈区,两个都在访问这个栈区,如果一个栈区被多个线程共享的话,每个线程都要向栈区中压栈入自己的临时数据,那么在栈中压入的临时变量,无法分清是那个线程的,所以库在设计的时候,都必须保证给每个线程都要有自己独立的用户栈。每个线程都有自己独立的栈结构。
哪些属于线程私有的?
线程共享:
一个线程出问题,导致其它线程也出问题,导致整个进程退出---线程安全问题。
多线程中,公共函数如果被多个线程同时进入---该函数被重入了。
- #include <iostream>
- #include <string>
- #include <vector>
- #include <cstdio>
- #include <unistd.h>
- #include <cstdlib>
- #include <pthread.h> // 原生线程库的头文件
-
- const int threadnum = 5;
-
- class Task
- {
- public:
- Task()
- {}
- void SetData(int x, int y)
- {
- datax = x;
- datay = y;
- }
- // 执行的任务
- int Excute()
- {
- return datax + datay;
- }
- ~Task()
- {}
- private:
- int datax;
- int datay;
- };
-
- class ThreadData : public Task
- {
- public:
- ThreadData(int x, int y, const std::string& threadname) :_threadname(threadname)
- {
- _t.SetData(x, y);
- }
- std::string threadname()
- {
- return _threadname;
- }
- int run()
- {
- return _t.Excute();
- }
- private:
- std::string _threadname;
- Task _t;
- };
- // 结果
- class Result
- {
- public:
- Result() {}
- ~Result() {}
- void SetResult(int result, const std::string& threadname)
- {
- _result = result;
- _threadname = threadname;
- }
- void Print()
- {
- std::cout << _threadname << " : " << _result << std::endl;
- }
- private:
- int _result;
- std::string _threadname;
- };
-
- // 每个线程都会执行这个函数
- void* handlerTask(void* args)
- {
- ThreadData* td = static_cast<ThreadData*>(args);
-
- std::string name = td->threadname();
-
- Result* res = new Result();
- int result = td->run();
-
- res->SetResult(result, name);
-
- std::cout << name << "run result : " << result << std::endl;
- delete td;
- sleep(2);
- return res;
- // 这个函数没有使用全局变量,在函数中定义的threadname变量在自己的独立栈上,所以多个线程并不影响
- // 虽然该函数重入了,但是函数并不会出问题
- // // std::string threadname =static_cast<char*>(args);
- // const char *threadname = static_cast<char *>(args);
- // while (true)
- // {
- // std::cout << "I am " << threadname << std::endl;
- // sleep(2);
- // }
- // 虽然对于线程来说堆空间是共享的,但是每个线程都只能拿到自己堆空间的起始地址,其它线程的堆空间看不到
- // delete []threadname;
- // return nullptr;
- }
- // 1. 多线程创建
- // 2. 线程传参和返回值,我们可以传递基本信息,也可以传递其他对象(包括你自己定义的!)
- // 3. C++11也带了多线程,和我们今天的是什么关系???
- int main()
- {
- std::vector<pthread_t> threads;
- // 创建5个线程
- for (int i = 0; i < threadnum; i++)
- {
- char threadname[64];// 第二次循环时,第一次循环时的缓冲区中的数据就被释放掉或者被后来的数据覆盖
- snprintf(threadname, 64, "Thread-%d", i + 1);// 将线程名为参数传递给线程函数
- // 我们不能让每一个线程的threadname的变量都指向同一块缓冲区,我们要给每一个线程申请一个属于自己的空间
- ThreadData* td = new ThreadData(10, 20, threadname);
- pthread_t tid;
- pthread_create(&tid, nullptr, handlerTask, td);
- threads.push_back(tid);// 将线程id保存到vector中
- }
- std::vector<Result*> result_set;// 结果
- void* ret = nullptr;
- // 循环等待线程
- for (auto& tid : threads)
- {
- pthread_join(tid, &ret);
- result_set.push_back((Result*)ret);
- }
- for (auto& res : result_set)
- {
- res->Print();
- delete res;
- }
- }
新线程处于分离状态,新线程无线循环的跑下去,主线程5秒之后,就退出,会发生什么事情呢?
新线程处于分离状态,新线程无线循环的跑下去,但是新线程中会出现异常,主线程5秒之后,就退出,会发生什么事情呢?
好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。