创建网站能赚钱吗,赣州企业网站建设推广,vps主机上新增网站,公众号模板网站线程概念
线程这个词或多或少大家都听过#xff0c;今天我们正式的来谈一下线程#xff1b;
在我一开始的概念中线程就是进程的一部分#xff0c;一个进程中有很多个线程#xff0c;这个想法基本是正确的#xff0c;但细节部分呢我们需要细细讲解一下#xff1b;
什么…线程概念
线程这个词或多或少大家都听过今天我们正式的来谈一下线程
在我一开始的概念中线程就是进程的一部分一个进程中有很多个线程这个想法基本是正确的但细节部分呢我们需要细细讲解一下
什么是线程
1.线程是进程执行流中的一部分就是说线程是进程内部的一个控制序列
2.线程是操作系统调度的基本单位 3.在linux中没有真正意义上的线程也就是操作系统中说的tcbthread ctrl block但是其他的操作系统是有的不同的操作系统实现不同如windows就是在pcb下再次构建了tcb的数据结构为什么linux下没有真正意义的线程呢因为线程再操作系统中也是需要被管理的可是线程的管理一定得创建数据结构创建复杂的数据结构一定需要增加维护的成本与难度而线程的管理其实和进程是相似的所以聪明的linux程序员将线程管理设计为了轻量化的进程将线程与进程统一管理减轻了代码的复杂度便于维护提高效率线程粒度细于进程
4.线程其实是进程的一部分所以线程运行的地方就是在进程的虚拟地址空间中的因为线程本身也是属于进程的一部分的只是被加载到了进程队列中运行而已进程就像是一个家庭线程就像是家庭中的每一个人每个人都有自己的工作所以需要分开执行也就是处于进程队列中进程会分配的资源给线程家庭中的资源会分配给每个人比如爸爸要去远的地方工作需要开车那车子这个资源就会分配给父亲这个资源包括代码和数据之前我们理解的进程可以当作是主线程通过分配自己的代码给它内部的线程内部的线程拿到数据和代码资源区执行分配给它的工作从而执行相应的操作
重谈虚拟地址空间
页表如何映射 计算页表大小 所以一个页表最大为4mb并且一个页表的二级页表不一定为1024个因为页表的映射也不是一次就完成的而已页表的映射使用完之后还会释放等所以一个页表大小不会大于4mb
就是这样的页表完成了我们的映射那我们的数据和代码都是存储在这个地址空间上的而函数就是一个现成的地址所以我们分配给线程代码数据是不是可以直接将这个函数分给线程呢这样不就等于把线程需要执行的工作划分给了线程吗
所以线程划分资源本质上是将地址空间中的资源进行分配
为什么我们要创建线程线程优点
1.同一进程中线程之间的切换更加轻量化
在我们的内存中最快的是寄存器cpu之间拿寄存器中的数据进行计算寄存器也需要获取数据而寄存器不是之间从内存中拿数据的因为内存相较于寄存器还是太慢了所以它们之间还有一个cache缓存这个cache中存放的是当前进程的数据和指令寄存器可以很快的就从cache中拿到一个进程中的数据cache命中率会很高因为都在同一进程都是热数据因为同一进程中的线程是共享数据的所以cache切换时只需要切换task_struct而进程之前切换所有数据都需要切换进程切换了进程间具有独立性cache中的数据一定都需要被切换所咦数据会变冷重新去命中数据这样的切换消耗会大的多
2.创建和销毁线程的代价要小很多因为线程的数据已经在内存中了线程只需要从它所在的进程中获取数据即可
3.io密集型程序通过多线程可以提高很大的效率在进行io的时候进程可以让其他线程进行计算等操作不需要等待io结束再操作相比单线程的等待要优化非常多
4.计算密集型程序在单核cpu中多线程没有什么提升想法线程之间的切换还会降低效率但是在多核cpu中多线程可以在多个核上进行计算计算线程数要小于等于核的数量也是大大提高了计算的效率的 线程缺点
由于线程之前没有独立性共享进程代码数据代码的健壮性要低一些所以需要进行同步于互斥缺乏访问控制-健壮性低相应的调试也会更难 线程数据
每个线程虽然都是进程的一部分从进程中获得数据的但是线程一定需要包含自己的数据
线程自己的数据
1.线程对应的上下文数据寄存器
2.线程运行时数据独立的栈空间
3.线程id
4.信号屏蔽字
5.调度优先级 6.errno 线程操作
上面讲解了线程的基本内容下面我们来对线程进行操作来理解线程
我们需要先了解这些linux中posix标准中的原生线程库中的函数
线程创建
pthread_create
这个函数是用来创建子线程的 第一个参数是一个输出型参数用来输出创建线程的tid
第二个参数是用来设置线程的属性的其实这是一个指向线程属性对象的指针通过传递我们设置好的对象传递给线程从而改变线程的默认属性一般我们都传递NULL使用默认属性即可
第三个参数是一个回调函数用来提供给线程运行的代码可以理解为让线程执行此函数
第四个参数就是一个传递给函数第三个参数——回调函数的参数这个参数既可以是普通的内置类型也可以是结构体这样可以很多的数据
返回值返回0为成功创建创建失败返回返回错误码不设置errno 下面可以看到我们的代码成功运行了
#include iostream
#include pthread.h
#include unistd.h
#include sys/types.h
using namespace std;void *routine(void *data)
{for (int i 0; i 5; i){usleep(100000);cout 线程1, pid: getpid() endl;}return nullptr;
}int main()
{pthread_t tid;pthread_create(tid, nullptr, routine, nullptr);for (int i 0; i 7; i){usleep(200000);cout 线程0, pid: getpid() endl;}return 0;
} 从上面的现象我们可以清楚的知道线程是一个独立的执行流虽然routine函数和main函数它们两个再同一个程序中且是两个循环但是这两个循环同时跑起来了所以证明了线程的独立性
编译时需要加-lpthread选项
在linux中使用原生线程库进行编程时我们编译选项总是需要带上-lpthread这个选项在我们前面学习动静态库的时候就很熟悉了用来连接指定的库而似乎我们在以往的编程中除开我们自己创建动静态库的情况之外我们从未出现过主动连接动静态库的情况
为什么我们不需要主动去连接呢这是因为编译器自动去帮我们连接了我们的cc语言级别的库也好linux的系统库也罢它们库的路径都是已经存储在编译器的配置文件中的编译器可以自动的找到库第一步然后编译器会自动连接这些库第二步为什么会自动连接呢我们可以认为这些系统库和标准库是编译器自己的库所以编译器会自动的连接而pthread这个库是posix标准中的原生线程库它是属于第三方库的而第三方库即使它被放到系统标准库的路径之下它也是不会被自动连接的所以我们需要带上-lpthread选项去主动连接这个库 查看线程
我们看到的线程的现象接下来我们从系统的角度的入手使用系统的指令来查看我们的线程的体现 ps -aL lwp的全称是light weight process轻量级进程
线程的等待与tid获取函数
pthread_join
子进程被创建父进程需要等待进程返回而线程被创建也需要被等待但是这里只有主线程和其他线程的区别主线程需要等待其他所有线程防止内存泄漏的问题 这里的第一个参数是指向被等待线程的tid
第二个参数是一个输出型参数可以用来接收线程的返回值这个返回值可以是任意类型的数据自定义类型也可以
返回值为0代表等待成功非0则返回错误值不设置errno码 pthread_self
可以获得线程的tid 这是一个无参函数和getpid的使用方式是一样的
代码实现 知道了这些基本的函数后我们下面用代码实践来展示现象并解释
#include iostream
#include pthread.h
#include unistd.h
#include sys/types.h
using namespace std;struct thread_data
{string threadName;string threadReturn;
};void *routine(void *data)
{thread_data *d1 static_castthread_data *(data);// thread_data *d1 (thread_data *)(data);int count 3;for (int i 0; i count; i){printf(tid: %p threadName: %s count: %d\n, pthread_self(), d1-threadName.c_str(), i);sleep(1);}// int a5/0;//除0错误 这里说明了当进程中的某个线程出现异常时整个进程都会退出// exit(0);//使用exit退出 这里也说明了使用exit会退出整个进程d1-threadReturnreturn_d1-threadName;return d1;
}void initThread(thread_data *data, int num)
{data-threadName thread_ to_string(num);
}int main()
{pthread_t tid;thread_data *data new thread_data;initThread(data, 1);int ret_create pthread_create(tid, nullptr, routine, (void *)data);void *ret_thread;printf(我是主线程tid: %p\n,pthread_self());pthread_join(tid, ret_thread);cout ((thread_data *)ret_thread)-threadReturn endl;//证明获得了一个类返回值delete data;return 0;
} 使用return正常退出的情况 下面是使用exit和异常退出的情况 通过代码和现象我们可以知道这些细节
1. 我们可以使用join获取线程的返回值线程返回值可以为任意类型的指针所以可以传递任意值
2.我们的子线程退出的时候不能使用exit退出这样会导致整个进程都退出我们可以使用returnpthread_exit(后面讲)使用cacel取消joined后面讲这3种方式退出
3.进程中的任意一个线程出现异常整个进程都会退出
4.线程的tid是一个地址这个地址是进程堆栈之间的内存区域通过上面的现象也可清楚的明白
由此我们可以知道这些函数的大致使用
线程结构体位置
上面我们通过概念与实现基本的了解了线程接下来我们通过图像来了解线程的结构体 其实我们的线程是这样存在在我们的进程中的因为linux程序员为了减轻代码的维护效率linux中没有真正的线程而是将线程作为轻量级进程而用来描述轻量级进程的结构体是存储在用户层的存储的位置就是共享区的原生线程库线程库中维护了线程的属性数据内核的执行流tcb控制块通过找到进程中的线程库中的线程结构体从而找到线程代码执行线程
所以线程的属性是由线程库来维护的而tid之所以是共享区之中的代码的原因就是因为tid指的是共享区中线程库中的某个线程结构体所在的首地址 线程空间的特点
1.线程之间的栈空间是独立的
这一点非常好理解因为函数在被调用的时候就会创建自己的栈帧嘛而线程执行其实就是执行了分给他的函数所以线程栈空间是独立的
2.线程之间是没有秘密的
为什么线程之间独立但是又没有秘密呢因为线程总是在一个进程中的嘛栈之间的数据只需要通过一个指针就可以获得了
代码示例
#include iostream
#include pthread.h
#include unistd.h
#include sys/types.h
#include vector
#include string
using namespace std;struct threadData
{string threadName;threadData(int num){threadName thread to_string(num);}threadData() default;
};int *g_index;void *routine(void *args)
{int val 0;threadData *data (threadData *)args;for (int i 1; i 3 ; i){printf(%s tid: %p val: %d\n, data-threadName.c_str(), pthread_self(), val);val;}if(data-threadNamethread1){val10000;g_indexval;sleep(5);}return (void *)0;
}int main()
{vectorpthread_t tids;for (int i 0; i 3; i){pthread_t tid;threadData *td new threadData(i);pthread_create(tid, nullptr, routine, td);tids.push_back(tid);sleep(1);}cout这是thread2的val值: *g_indexendl;for(auto t:tids){void *retData;pthread_join(t,retData);}return 0;
} 但是如果我们想要获得某个栈空间的数据时这也是可以轻松做到的
我们在routine函数中加入一段这样的代码并在main函数中读取数据 routine函数中if(data-threadNamethread1){val10000;g_indexval;sleep(5);}
main函数中cout这是thread2的val值: *g_indexendl; 线程的变量__thread选项
int *g_index;
//int g_val;
__thread int g_val;void *routine(void *args)
{int val 0;threadData *data (threadData *)args;for (int i 1; i 3 ; i){//printf(%s tid: %p val: %d\n, data-threadName.c_str(), pthread_self(), val);//val;printf(%s g_val: %d\n,data-threadName.c_str(),g_val);g_val;}// if(data-threadNamethread1)// {// val10000;// g_indexval;// sleep(5);// }return (void *)0;
}int main()
{vectorpthread_t tids;for (int i 0; i 3; i){pthread_t tid;threadData *td new threadData(i);pthread_create(tid, nullptr, routine, td);tids.push_back(tid);sleep(1);}//cout这是thread2的val值: *g_indexendl;for(auto t:tids){void *retData;pthread_join(t,retData);}return 0;
}
我们线程在使用 g_val全局变量时 g_val带上__thread编译选项时: __thread是编译选择不是cc的语法是编译器的选项
特点 1.将进程全局数据变为线程全局数据
2.只能给内置类型带上这个选项 C线程库说明
在我们的C中是有语言级别的线程库的C语言没有C中的线程库是跨平台的但是我们在使用C线程库时我们还是会发现我们需要带上编译选项-lpthread所以说明C的线程库是封装了原生线程库的而原生线程库在linux中是posix标准的在windows中又有不同的标准但是C的线程库是跨平台的所以说明C的线程库不仅封装了linux的posix标准线程库还封装了windows下的线程库
clone系统调用的封装
我们前面说线程是轻量级的进程为什么这么说呢其实我们在创建线程时使用的pthread_create函数和创建子进程的fork函数都是封装了clone的系统调用
int clone(int (*fn)(void *), void *child_stack
, int flags, void *arg
, ... /* pid_t *ptid, void *tls, pid_t *ctid */);
这个系统系统调用会指定一片栈空间给新开辟的线程我们不需要懂clone调用的细节我们只需要知道linux中其实在底层上线程的接口也是和进程用的一样的调用所以它们在内核层面上是处于同一级别的执行流的所以线程被称为轻量级进程 小提示
线程如何使用进程替换的调用会将当前的整个进程替换掉
线程终止
前面我们说了线程的3个正常退出方式我们下面来详细的讲解一下
pthread_exit 这个函数就是和return一样的作用返回一个retval给主线程这里需要注意的是retval最好是堆上的指针线程终止栈帧也会销毁会导致栈上的数据被释放所以返回值一定要是不被释放的数据
pthread_cancel
这是一个线程终止函数我们可以通过此函数终止掉tid的线程 这里终止了就不需要再join了如果join了会发返回非0值
这是gpt给出的提示
尽管 pthread_cancel 函数可以请求取消另一个线程但是线程是否真正被取消以及何时被取消是由目标线程自身来决定的。目标线程可以选择忽略取消请求或者在适当的时机响应取消请求并执行清理操作。 线程分离
pthread_detach 我们的主线程永远是最后退出的因为需要等待所有创建进程退出我们常见的服务器一般都是死循环不退出的程序而当主线程不关系创建的线程的结果时可以使用detach来断开创建线程与主线程之间的关系主线程就不需要等待子线程了
#include iostream
#include pthread.h
#include unistd.h
#include sys/types.h
#includecstring
using namespace std;void* routine(void*args)
{cout我是被创建线程endl;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,routine,nullptr);void* ret;pthread_detach(tid);int ret_joinpthread_join(tid,ret);printf(%s\n,strerror(ret_join));return 0;
}
当没有detach时 当创建的线程被detach时 所以说明线程不能被同时detach和join
此外线程可以自己detach自己
以上就是线程的控制与基本概念线程部分未完待续