自助建站平台设计器,企业网站提供商,网站免费正能量直接进入,百度网页电脑版入口线程控制 1.创建线程2.线程终止3.线程等待4.线程分离5.对线程的简单封装 喜欢的点赞#xff0c;收藏#xff0c;关注一下把#xff01; 进程概念上篇文章已经讲完了#xff0c;下面我们就来说说线程控制。
我们使用的接口是pthread线程库#xff0c;也叫做原生线程库给我… 线程控制 1.创建线程2.线程终止3.线程等待4.线程分离5.对线程的简单封装 喜欢的点赞收藏关注一下把 进程概念上篇文章已经讲完了下面我们就来说说线程控制。
我们使用的接口是pthread线程库也叫做原生线程库给我们提供的这个库遵守POSIX标准的跟我们System V是相对应的一种标准。
POSIX线程库
与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以“pthread_”打头的要使用这些函数库要通过引入头文pthread.h链接这些线程函数库时要使用编译器命令的“-lpthread”选项
1.创建线程 pthread_create 创建一个新的线程 thread:返回线程ID attr:设置线程的属性attr为NULL表示使用默认属性 start_routine:是个函数地址线程启动后要执行的函数 arg:传给线程启动函数的参数 返回值成功返回0失败返回错误码
错误检查:
传统的一些函数是成功返回0失败返回-1并且对全局变量errno赋值以指示错误。pthreads函数出错时不会设置全局变量errno而大部分其他POSIX函数会这样做。而是将错误代码通过返回值返回pthreads同样也提供了线程内的errno变量以支持其它使用errno的代码。对于pthreads函数的错误建议通过返回值判定因为读取返回值要比读取线程内的errno变量的开销更小
这个接口线程概念哪里我们已经用过。创建了一个线程。 今天我们想创建多个线程并给每个线程都写上编号。
#includeiostream
#includepthread.h
#includestring
#includeunistd.h
#includecstdiousing namespace std;void* start_rountine(void* args)
{//安全的进行强制类型转化string namestatic_castconst char*(args);while(true){coutnew thread create success, name: nameendl;sleep(1);}
}int main()
{#define NUM 10for(int i0;iNUM;i){pthread_t id;//pthread_create(id,nullptr,start_rountine,(void*)thread new); char namebuffer[64];snprintf(namebuffer,sizeof(namebuffer),%s:%d,thread,i);pthread_create(id,nullptr,start_rountine,namebuffer); }while(true){coutnew thread create success, name: main threadendl;sleep(1);}return 0;
}for(int i0;iNUM;i){pthread_t id;char namebuffer[64];snprintf(namebuffer,sizeof(namebuffer),%s:%d,thread,i);pthread_create(id,nullptr,start_rountine,namebuffer); //这里sleep一秒再看一下运行结果sleep(1);}为什么不加sleep线程编号都变成9了呢 因为无法保证谁先运行所以在运行时可能出现一些奇怪的情况。
就如上面我们创建线程时有可能这个线程创建出来了但是还没来得及执行后序代码而优先跑的主线程主线程上来之后直接对缓冲区写入而上一个线程只是把缓冲区地址给过去了但缓冲区本身被下一次格式化显示所覆盖了。所以最终到底线程编号都是9。
所以如果想创建一批线程这种写法不太对的。 正确写法如下 #includeiostream
#includepthread.h
#includestring
#includeunistd.h
#includecstdio
#includevectorusing namespace std;struct ThreadDate
{pthread_t tid;char namebuffer[64];
};void* start_rountine(void* args)//这个是传值传参形参是实参的拷贝
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt10;while(cnt){//该函数拿到之后解引用就可以访问到了coutnew thread create success, name: td-namebuffer cnt: cnt-- endl;sleep(1);}return nullptr;
}int main()
{#define NUM 10for(int i0;iNUM;i){//每一次都会新创建一个ThreadDate对象ThreadDate* tdnew ThreadDate();snprintf(td-namebuffer,sizeof(td-namebuffer),%s:%d,thread,i1);//每个线程都调用start_rouytine都会把每次新创建对象的地址传给该函数pthread_create(td-tid,nullptr,start_rountine,td); //new出现新对象每个都是独立的不会互相影响的}while(true){coutnew thread create success, name: main threadendl;sleep(1);}return 0;
}为了后序方便处理因此还可以添加一个vector对象
int main()
{vectorThreadDate* threads;
#define NUM 10for(int i0;iNUM;i){ThreadDate* tdnew ThreadDate();snprintf(td-namebuffer,sizeof(td-namebuffer),%s:%d,thread,i1);pthread_create(td-tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了主线程也全部拿到了threads.push_back(td);}for(auto iter:threads){coutcreate thread: iter-namebuffer : iter-tid successendl;}while(true){coutnew thread create success, name: main threadendl;sleep(1);}return 0;
}那我现在就有几个问题了。 1.start_routine现在是被几个线程执行的呢 回答10个 那这个函数现在是什么状态 回答重入信号时谈到一个函数被多个执行流访问这个函数就是可重入状态。
2.该函数是可重入函数吗 回答是的
3.start_routine内定义的变量多个执行流进来会不会互相影响 回答不会 如何证明
void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt10;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);}return nullptr;
}由运行结果说明。
在函数内定义的变量都叫做局部变量具有临时性这是语言上的概念今天依旧适用。在多线程情况下也没有问题。通过这个概念我们也证明了一件事情其实每一个线程都有自己独立的栈结构。
在哪呢下面说。
2.线程终止
进程终止有三种方式我们先说两种然后把线程等待说一说下面再说第三种。 return 线程函数结束return的时候线程就算终止了或者在任意位置return。 关于返回值void*下个话题再说。
以前我们也用exit终止过进程这里可以用来终止线程吗
void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt10;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);exit(1);}return nullptr;
}不能因为exit是终止进程的任何一个执行流调用exit都会让整个进程退出 pthread_exit 那一个线程调用这个函数就终止哪一个线程。 参数先不管等会说。
void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt10;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);pthread_exit(nullptr);}return nullptr;
}由此说明每一个线程调用pthread_exit都把自己终止了并不影响主线程
这个调用也可以放在return的位置都是一样的。
void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt10;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);}pthread_exit(nullptr);
}注意到不管是return还是pthread_exit的参数返回类型都是void*有返回值如果我想拿到这个返回值该怎么办呢
这里我们不得不先谈线程等待的问题
3.线程等待
线程也是要被等待的。 如果不等待会有什么问题呢
以前说过僵尸进程子进程退出了你不wait等待子进程是僵尸状态从而造成内存泄漏。那么在多线程这里每一个线程在退出时它的PCB并没有被释放如果不等待会造成类型僵尸进程的问题-----内存泄漏
所以线程也必须要被等待目的有两个 1.获取新线程的退出信息(可以不关心但还是要等) 2.回收新线程对应的PCB等内核资源防止内存泄漏(暂时无法查看) pthread_join 等待线程结束 thread:线程ID(你等哪一个线程) retvalvoid**一个void*就很难理解了。这是什么意思呢
返回值成功返回0失败返回错误码
我们先使用这个函数进行等待再说。
int main()
{vectorThreadDate* threads;
#define NUM 10for(int i0;iNUM;i){ThreadDate* tdnew ThreadDate();snprintf(td-namebuffer,sizeof(td-namebuffer),%s:%d,thread,i1);pthread_create(td-tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了主线程也全部拿到了threads.push_back(td);}for(auto iter:threads){coutcreate thread: iter-namebuffer : iter-tid successendl;}for(auto iter:threads){//阻塞式的等待每一个线程int npthread_join(iter-tid,nullptr);assert(n 0);(void)n;coutjoin : iter-namebuffer successendl;delete iter;}//这里就可以看出主线程式阻塞式的等待全部等待成功才打印这句话coutmain thread quit!!endl;return 0;
}要等待的时候设置一下join操作系统就会自动帮我收回曾经为线程所创建的轻量级进程相关的资源。
现在谈一谈线程退出返回值问题。 我们发现线程退出用return返回时它的类型是void*用pthread_exit需要我们传入一个线程的参数类型也是void*而我们想获得线程执行完的结果对我们来说也可以通过join获得它当然也可以回收它。
我们看到join的第二个参数void** 也和void有关难道都是巧合吗
我们主线程join新线程第二个参数是void** 到底是什么意思呢 你想作为输出型参数把结果拿回去而返回值可是void*你想把结果带出去就必须是void**
下面我们演示一下。
struct ThreadDate
{int number;pthread_t tid;char namebuffer[64];
};void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt5;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);}return (void*)td-number;//传值返回
}这里返回可能会有warning因为整数4个字节而linux默认release编译指针大小8字节但并不影响。 那么请问这个整数返回的时候如果返回到void*里面请问这个整数或者说这个变量在哪里呢
其实上面的返回就相当于把这个数字写到指针变量里。4字节写到8字节里
void* ret(void*)td-number;for(auto iter:threads){void* retnullptr;//注意是void*//阻塞式的等待每一个线程int npthread_join(iter-tid,ret);//地址是void** assert(n 0);(void)n;coutjoin : iter-namebuffer successendl;delete iter;}这个join函数内部就做了这样一件事情
void** retpret;
*retpreturn (void*)td-number //*retp解引用就是ret这样就相对于把返回值写到ret变量里。
然后再打印出来。
for(auto iter:threads){void* retnullptr;//注意是void*//阻塞式的等待每一个线程int npthread_join(iter-tid,ret);//地址是void** assert(n 0);(void)n;//注意这里如果用int打印会有精度损失8-4//所以用long long//coutjoin : iter-namebuffer success , number: (int)retendl;coutjoin : iter-namebuffer success , number: (long long)retendl;delete iter;}确实拿到了返回结果。
这里想告诉大家的时未来线程的返回值可以通过join第二个参数拿到。
如果这里还是不懂话我们换个值。 我想让每个线程都能返回这个值。 这里可以理解成pthread库里面为了保存当前线程的返回值定义了一个变量类型是void*return 这里可以认为把111强转成void*我们把它当成指针来看return就是把111写到void变量当中 接下来就是我在自己的用户空间里定义了一个指针变量void ret 而pthread_jion本质是从库中获取执行线程的退出结果 ret就是指向这块空间 然后join内部就相当于做 (ret)就是我们的定义的ret然后把111拷贝到ret里 因此return就相当于把这个数字返回到库中了而库为当前线程维护一个小的变量它保存的是该线程退出时void变量直接把这个数字写入到这个变量中而我们在用户空间定义一个void*ret变量如果不用pthrad_join函数怎么接收这个返回值呢假设返回值变量名称是下是不是就可以直接retx了啊。但是我们又不能直接读取库里的内容必须通过函数来调用所以只能ret然后就如void** retpretretpx而retp就是ret这样而能把x赋给ret了。
还有一种角度就是形参是是实参的临时拷贝如果就传ret形参的改变并不会影响实参这个值你就拿不回来。
同样pthread_exit也可以传返回值
void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt5;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);}pthread_exit((void*)111);}既然假的地址整数都能被外部拿到那么如果返回的是堆空间的地址呢对象的地址呢
struct ThreadReturn
{int exit_code;int exit_result;
};void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt5;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);}//注意千万不能直接申请一个对象//ThreadReturn tr; //这是在栈上开辟空间出了栈就销毁了//tr.exit_code//tr.exit_result//return (void*)trThreadReturn *trnew ThreadReturn();//堆开辟的空间tr-exit_code1;tr-exit_result111;return (void*)tr;}int main()
{vectorThreadDate* threads;
#define NUM 10for(int i0;iNUM;i){ThreadDate* tdnew ThreadDate();td-numberi1;snprintf(td-namebuffer,sizeof(td-namebuffer),%s:%d,thread,i1);pthread_create(td-tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了主线程也全部拿到了threads.push_back(td);}for(auto iter:threads){coutcreate thread: iter-namebuffer : iter-tid successendl;}for(auto iter:threads){ThreadReturn* trnullptr;int npthread_join(iter-tid,(void**)tr);//地址是void** assert(n 0);(void)n;coutjoin : iter-namebuffer success , exit_code: tr-exit_code exit_result: tr-exit_resultendl;delete iter;}//这里就可以看出主线程式阻塞式的等待全部等待成功才打印这句话coutmain thread quit!!endl;return 0;
}还有一个小问题以前进程退出的时候我们除了可以拿到退出码还有退出信号 pthread_join为什么没有对应的参数可以接收线程异常的信号呢
为什么没有见到线程退出的时候对应的退出信号呢 线程异常了怎么呢
线程出异常收到信号整个进程都会退出pthread_join连返回的机会都没有pthread_join默认就会认为函数会调用成功不考虑异常问题异常问题是你进程该考虑的问题。
上面说了线程终止有三种方式接下来就说这第三种方式。
线程是可以被cancel取消的 注意线程要被取消前提是这个线程已经跑起来了。 pthread_cancel 取消一个执行中的线程 thread:线程ID
void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* tdstatic_castThreadDate*(args);int cnt5;while(cnt){coutcnt: cnt cntcntendl;cnt--;sleep(1);}//正常跑完返回的100,那被取消的线程返回的是什么呢return (void*)100;
}int main()
{vectorThreadDate* threads;
#define NUM 10for(int i0;iNUM;i){ThreadDate* tdnew ThreadDate();td-numberi1;snprintf(td-namebuffer,sizeof(td-namebuffer),%s:%d,thread,i1);pthread_create(td-tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了主线程也全部拿到了threads.push_back(td);}for(auto iter:threads){coutcreate thread: iter-namebuffer : iter-tid successendl;}//线程取消sleep(5);//先让线程跑起来for(int i0;ithreads.size()/2;i){pthread_cancel(threads[i]-tid);coutpthread_cancel: threads[i]-namebuffer successendl;}for(auto iter:threads){void* retnullptr;//注意是void*int npthread_join(iter-tid,ret);//地址是void** assert(n 0);(void)n;coutjoin : iter-namebuffer success , number: (long long)retendl;delete iter;}//这里就可以看出主线程式阻塞式的等待全部等待成功才打印这句话coutmain thread quit!!endl;return 0;
} 一个线程如果是被取消的退出码是-1。 一般都是主线程取消新线程的。
接下来重新认识我们的线程库语言版 C也有多线程使用要包含一个thread库文件
mythread:mythread.ccg -o $ $^ -stdc11
.PHONY:clean
clean:rm -f mythread#includeiostream
#includethread
#includeunistd.husing namespace std;
void* thread_run(void* args)
{while(true){cout我是新线程...endl;sleep(1);}
}int main()
{//创建,并执行对应的方法thread t1(thread_run);while(true){cout我是主线程...endl;sleep(1);}//回收t1.join();return 0;
}我故意把makefile中pthread弄掉了编译不能通过了。 我们明明没有用过pthread_create怎么会有这个呢我用的可是C的多线程从来没有用过pthread库啊。
那把它加上去看看就没有报错了。我用的是C的多线程但是我照样看的是轻量级进程。 任何语言在Linux中如果要实现多线程必定要用pthread库。 如何看到C11中多线程库呢 C11的多线程库在Linux环境中本质是对pthread库的封装
4.线程分离
默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值join是一种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。
也就是说线程是可以等待的等待的时候join的阻塞式的等待如果我们不想等待呢? pthread_detach 线程分离线程结束后自动释放线程资源。 thread:线程ID
可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离自己。
如果是自己分离自己该如何得知自己的线程ID呢 pthread_self 谁调用这个函数就获得线程ID
#includeiostream
#includepthread.h
#includeunistd.husing namespace std;string changeId(const pthread_t thread_id)
{char tid[128];snprintf(tid, sizeof(tid), 0x%x, thread_id);return tid;
}void* start_routine(void* args)
{string threadnamestatic_castconst char*(args);while(true){coutthreadname running ... : changeId(pthread_self())endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid,nullptr,start_routine,(void*)thread 1);coutmain thread running ... new thread id:changeId(tid)endl;pthread_join(tid,nullptr);return 0;
}由此证明pthread_self可以获取线程ID的
下面使用pthread_detach分离线程
void* start_routine(void* args)
{string threadnamestatic_castconst char*(args);//pthread_detach(pthread_self());//设置自己为分离状态while(true){coutthreadname running ... : changeId(pthread_self())endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid,nullptr,start_routine,(void*)thread 1);string main_idchangeId(pthread_self());coutmain thread running ... new thread id:changeId(tid)main thread id: main_idendl;//一个线程默认是joinable的如果设置了分离状态不能够进行等待了int npthread_join(tid,nullptr);coutresult: strerror(n)endl;return 0;
}先不分离看看结果
void* start_routine(void* args)
{string threadnamestatic_castconst char*(args);pthread_detach(pthread_self());//设置自己为分离状态int cnt5;while(cnt--){coutthreadname running ... : changeId(pthread_self())endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,start_routine,(void*)thread 1);string main_idchangeId(pthread_self());coutmain thread running ... new thread id:changeId(tid) main thread id: main_idendl;//一个线程默认是joinable的如果设置了分离状态不能够进行等待了int npthread_join(tid,nullptr);coutresult: n : strerror(n)endl;return 0;
}现在分离再看运行结果
为什么分离和不分离的运行结果都是一样的呢
之前说过主线程和新线程谁先跑是不确定的。 可能先跑的主线程然后就先等待了还是阻塞式等待而新线程还没有执行到pthread_detach等到新线程分离了但是主线程不知道所以等新线程退出了还是回收你。
接下来我们先让新进程分离然后主进程在等待看一看运行结果。
int main()
{pthread_t tid;pthread_create(tid,nullptr,start_routine,(void*)thread 1);string main_idchangeId(pthread_self());coutmain thread running ... new thread id:changeId(tid) main thread id: main_idendl;//先让新进程跑上2秒sleep(2);//一个线程默认是joinable的如果设置了分离状态不能够进行等待了int npthread_join(tid,nullptr);coutresult: n : strerror(n)endl;return 0;
}所以推荐分离应该在线程创建成功后由主线程进行分离新线程
int main()
{pthread_t tid;pthread_create(tid,nullptr,start_routine,(void*)thread 1);pthread_detach(tid);//主线程对新线程进行分离string main_idchangeId(pthread_self());coutmain thread running ... new thread id:changeId(tid) main thread id: main_idendl;//sleep(2);//一个线程默认是joinable的如果设置了分离状态不能够进行等待了int npthread_join(tid,nullptr);coutresult: n : strerror(n)endl;return 0;
}这样结果就更明显了。 因为新线程一跑起来就分离了主线程在等待就报错了
如果主线程对新线程分离了就不用再等待了主线程做自己的事情就好了。
void* start_routine(void* args)
{string threadnamestatic_castconst char*(args);int cnt5;while(cnt--){coutthreadname running ... : changeId(pthread_self())endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,start_routine,(void*)thread 1);pthread_detach(tid);//主线程对新线程进行分离string main_idchangeId(pthread_self());while(true){coutmain thread running ... new thread id:changeId(tid) main thread id: main_idendl;sleep(1);}return 0;
}其实线程控制到这里就结束了。
但是我们一直还有一些问题没有解决 线程ID是什么 每个线程都有独立的栈结构可是虚拟地址空间的栈只有一个那这个栈在哪
下面我们就说一说。 Linux无法直接提供创建系统调用接口而只能给我们提供创建轻量级进程的接口 但是我们的程序员用户可不管这些轻量级进程我只要线程。 没有办法所以在操作系统和应用程序员之间设计了一个库这就是我们一直在用的pthread库我们称之为原生线程库。 当一个程序员想用线程的时候是不是得有线程的ID线程的状态优先级等独立栈这些属性。 但还要做一个事情我们程序员要线程时并不是直接向操作系统要的而是从库中要。所以原生线程库可能要存在多个线程。 也就是说你用库中的接口创建了线程别人可以同时在用吗当然是可以的。你在开发时用。别人在运行时用。
所以当前原生线程库要不要对线程进行管理呢 当然要的不然你怎么知道你的线程ID是多少你怎么知道你当前线程的栈在哪里栈大小你怎么你的线程其他属性有哪些。
下面这就是我们线程的属性它是联合体。 将来线程属性字段就会按照特定的要求填充到这个数组里。每一个字段都有自己的含义。
虽然要对线程做管理但是不像进程那么多。 如何管理 先描述在组织 只不过这个描述(线程的属性)比较少
可以理解成每次创建线程都需要在库中先创建一个结构体这里每一个结构体都会对应一个轻量级进程 Linux方案用户级线程用户关心的线程属性在库中内核提供线程执行流的调度 Linux 用户级线程内核级轻量级进程 11
库不关心线程如何调度只关心线程ID栈在哪里栈的大小等属性这是由库维护的相当于库也帮我们创建一个数据结构来描述这个线程。
下面在看一张图两张图结合一起能让我们对这块知识有更清楚的认识。
虚拟地址空间有一块区域叫做共享区这个我们在动静态库的时候说过。 说到底pthread库是一个磁盘文件只不过是库文件罢了 。进程用了这个库是一定要加载到内存中然后映射到进程地址空间的共享区里面。 每一个线程当被创建时除了要在内核中创建对应的轻量级线程还要库中创建对应的描述线程属性的相关结构体 这里只是给出了重要的三条属性第一个是线程ID第二个线程的局部存储第三个线程栈结构。 这就是库中描述的对象那怎么组织的呢 有一个线程就创建一个线程的结构体我们可以称之为TCB这个线程控制块是放在一起的你可以想象成看出是一个数组有了数组之后为了更好的找到某一个线程我们只需要找到数组的起始地址而这个数组的起始位置并没有采用下标动态库加载到内存之后不是有起始地址吗所以对应的每个TCB都有自己的起始地址。 所以实际上线程ID就是在共享区线程库中对应线程TCB的地址。 根据这个地址就可以找到对应线程的相关属性了。 所以当一个线程结束时库会把该进程的返回值填到该线程ID所对应的TCB中主线程在进程join的时候根据该线程的ID去对应TCB中拿返回值。
现在也可以回答之前的问题了地址空间里只有一个栈可是说每个线程都有自己独立的栈结构那么这个栈在那呢 这个栈在库中的对应TCB中每个栈都有自己的私有栈。 换句话说未来创建多个线程主线程的栈在虚拟地址空间中用的时主线程栈而其他线程你想用对应的栈这个栈在共享区的线程库中的对应的TCB结构体中库帮我们维护好的。
库帮我们创建轻量级进程调用的接口是clone第一个参数指明回调函数第二个指向线程的独立栈当TCB创建好后把栈空间的起始地址传递给child_stack而未来每个新线程都用的是自己独立栈。不和主线程用同一个栈。
总结一下 当你创建线程时对应的线程实际上它帮我们在线程库中创建对应的线程控制块(可以认为时TCB)线程控制块对应的地址就是线程控制块起始地址这个TCB里面包含线程的ID线程私有栈结构线程的局部存储创建好之后底层库继续帮我们调用clone在底层创建轻量级进程然后把我们创建好的TCB对应的回调方法私有栈等参数传给clone所以底层的线程一旦创建好了它就时依赖我们的原生线程库进而新线程在使用的临时变量就是压到了自己的child_statck所指的栈中也就是在我们的库中直接保存了。
所以线程的ID就是我们传说中在共享区的线程库中对应线程TCB的地址。 这种解决方案的线程我们称之为用户级线程
线程的局部存储我们说一下。
我们知道线程大部分资源都是共享的关于全局变量我们也演示过了。
int g_val0;string changeId(const pthread_t thread_id)
{char tid[128];snprintf(tid, sizeof(tid), 0x%x, thread_id);return tid;
}void* start_routine(void* args)
{string threadnamestatic_castconst char*(args);//pthread_detach(pthread_self());//设置自己为分离状态//int cnt5;while(true){coutthreadname running ... : changeId(pthread_self())\ g_val: g_val g_val: g_valendl;g_val;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(tid,nullptr,start_routine,(void*)thread 1);pthread_detach(tid);//主线程对新线程进行分离string main_idchangeId(pthread_self());while(true){coutmain thread running ... new thread id:changeId(tid) main thread id: main_id\ g_val: g_val g_val: g_valendl;sleep(1);}return 0;
}上面运行结果符合预期
下面我们改一下代码在运行一下看结果
__thread int g_val0;发现新线程值一直在而且地址也变得很大而主线程值没有变但是地址也变得很大并且两个地址都不一样。这是为什么呢
添加__thread可以将一个内存类型设置为线程局部存储 这个变量依旧是全局变量只不过在编译得时候给每一个线程都来一份每个线程都有一份那么它们在访问这个变量时就互不影响了 那为什么地址会有那么大变化呢 原因在于刚开始定义得全局变量在已初始化数据段而你一旦把它设置了局部存储那么这个变量就被映射到了对应得线程TCB中得线程局部存储而线程局部存储可是在共享区中共享区可是接近栈了比已初始化数据段可要高而虚地址空间从低地址到高地址的。 如果未来想给线程定义一些私有属性不想放在栈上或者new或者malloc你就可以定义成这样这种局部存储是介于全局变量和局部变量之间的线程特有的存储方案。
以上就是线程控制的全部东西
既然学完了那现在就用一下我们尝试把线程接口进行封装就像C那样调用我们的线程
5.对线程的简单封装
#pragma once
#includeiostream
#includepthread.h
#includestring
#includefunctionalusing namespace std;class Thread
{
public://这里也可以按照C的方法typedef void*(*func_t)(void*)typedef functionvoid*(void*) func_t;private:string _name;func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程ID
};以后我们写代码难免会遇见C和C混编的情况可能会觉得C提供了很多方便的接口给我们用但是不好意思有些C提供的接口底层不认识。底层可能用纯C写的。
class Thread
{
public:typedef functionvoid*(void*) func_t;const int num1024;
public:Thread(func_t func,void* argsnullptr,int number0):_func(func),_args(args){char namebuffer[num];snprintf(namebuffer,sizeof(namebuffer),thread-%d,number);_namenamebuffer;//调用回调函数//报错int npthread_create(_tid,nullptr,_func,_args);assert(n 0);(void)n;}private:string _name;func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程ID
};pthread_create接口不认识C提供的这个东西。 解决方法有很多换成C的或者别的方法下面提供一种方法
我写一个能pthread_create库能认识的回调函数方法。 void* start_routine(void* args){return _func(_args);}Thread(func_t func,void* argsnullptr,int number0):_func(func),_args(args){char namebuffer[num];snprintf(namebuffer,sizeof(namebuffer),thread-%d,number);_namenamebuffer;//调用回调函数int npthread_create(_tid,nullptr,start_routine,_args);assert(n 0);(void)n;}但这里还有错。考虑一下为什么。
原因在于 void* start_routine(void* args)//类内函数有缺省参数(this指针){return _func(_args);}那我static一下就变成静态函数了没有this指针了 static void* start_routine(void* args)//类内函数有缺省参数(this指针){return _func(_args);}但问题又出来了这又是什么问题呢 static void* start_routine(void* args)//类内函数有缺省参数(this指针){//静态函数只能调用静态成员和静态函数return _func(_args);}那把这两个成员变量都变成静态成员变量可不可以 不可以的把这个两个成员变成静态的了那这个两个变量就是所有线程共享的了。
这里我们再写一个类把this指针保存起来然后再去调用顺便把剩下成员函数谢谢简单的线程封装就写好了
#pragma once
#includeiostream
#includepthread.h
#includestring
#includefunctional
#includecstdio
#includecassertusing namespace std;//声明
class Thread;class Context
{
public:Thread* _this;void* args_;Context():_this(nullptr),args_(nullptr){}
};class Thread
{
public:typedef functionvoid*(void*) func_t;const int num1024;
public:static void* start_routine(void* args)//类内函数有缺省参数(this指针){Context* trstatic_castContext*(args);tr-_this-_func(tr-args_);//静态函数只能调用静态成员和静态函数//return _func(_args);}Thread(func_t func,void* argsnullptr,int number0):_func(func),_args(args){char namebuffer[num];snprintf(namebuffer,sizeof(namebuffer),thread-%d,number);_namenamebuffer;Context* trnew Context();tr-_thisthis;tr-args__args;//调用回调函数int npthread_create(_tid,nullptr,start_routine,tr);// int npthread_create(_tid,nullptr,start_routine,_args);assert(n 0);(void)n;}void join(){int npthread_join(_tid,nullptr);assert(n0);(void)n;}~Thread(){}private:string _name;func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程ID
};