【Linux】深入理解线程控制
个人主页~
深入理解线程控制
- 一、线程等待的原理
- 二、线程的局部存储
- 三、初步理解线程互斥
-
- 1、互斥的概念
- 2、需要互斥的原因
一、线程等待的原理
pthread_join
的作用是线程等待,其中retval
参数传递线程退出状态的原理是:当目标线程结束时,pthread_join
会将目标线程的退出状态(即线程函数的返回值或 pthread_exit
传递的参数)存储在 *retval
所指向的内存位置,也就是说,pthread_join
会修改 retval
所指向的那个 void *
类型变量的值
#include #include #include using namespace std;int g_val = 100;void *threadRoutine(void *args){//参数是线程名字,转化成字符串 const char *name = (const char *)args; int cnt = 5; while (true) { //线程打印线程pid,以及全局变量g_val和它的地址 printf(\"%s, pid: %d, g_val: %d, &g_val: 0X%p\\n\" , name, getpid(),g_val, &g_val); sleep(1); cnt--; if (cnt == 0) break; }//线程退出,返回指针100 pthread_exit((void *)100);}int main(){ pthread_t pid;//主线程id,线程属性设为无,新线程函数,新线程参数 pthread_create(&pid, nullptr, threadRoutine, (void *)\"Thread 1\"); void *ret;//等待新线程结束,获得新线程的返回值 pthread_join(pid, &ret); //打印线程返回值,这里强转为long long int是因为我的Linux是64位 //指针是八字节大小,long long int是八字节 cout << \"main thread quit..., Thread 1 return val: \" << (long long int)ret << endl; return 0;}
这给我们证明了,新线程的输出型参数是可以被主线程取到的,并且全局变量是可以被所有线程访问的,是共享资源,所以全局函数也是可以被所有线程访问的
&ret接受退出状态的具体过程:
当调用 pthread_join
时,pthread_join
会阻塞当前线程,直到由 thread
参数指定的目标线程终止,一旦目标线程终止,pthread_join
会将该线程调用 pthread_exit
时传递的 void*
指针(即退出状态)赋值给 &ret
所指向的 void*
变量,即ret
,pthread_join
成功完成等待和状态获取后,会返回 0,表示操作成功,当前线程可以继续执行后续代码
二、线程的局部存储
全局变量是被所有线程共享的,如果我们的线程需要有自己的私有的东西,也就是只能够自己访问,其他线程不能访问的,我们可以在全局变量前加关键字__thread
修饰,这是编译器为我们提供的只能用来修饰内置类型的关键字
#include #include #include #include #include using namespace std;#define NUM 3int *p = nullptr;//线程局部存储__thread int val = 100;class ThreadInfo{public: ThreadInfo(const string &threadname) :threadname_(threadname) {}public: string threadname_;};string toHex(pthread_t tid){ char buffer[64]; snprintf(buffer, sizeof(buffer), \"%p\", tid); return buffer;}void *threadroutine(void *args){ int i = 0; ThreadInfo *ti = static_cast<ThreadInfo*>(args); //线程循环,每次打印线程名称、线程ID、进程ID、被修饰变量val以及val地址 while(i < 10) { cout << ti->threadname_.c_str() << \" is running, tid: \" << toHex(pthread_self()) << \", pid: \" << getpid() << \", val: \" << val << \", &val: \" << &val << endl; i++; val++; usleep(10000); }delete ti; return nullptr;}int main(){ vector<pthread_t> tids; for(int i = 0; i < NUM; i++) { pthread_t tid; ThreadInfo *ti = new ThreadInfo(\"Thread-\"+to_string(i)); pthread_create(&tid, nullptr, threadroutine, ti); tids.push_back(tid); usleep(1000); }//线程等待 for(auto tid:tids) { pthread_join(tid, nullptr); } return 0;}
我们通过观察可以发现,在相同线程的情况下,val
的值是递增的,但对于不同的线程之间val
值是没有关系的,所以我们就通过关键字__thread
实现了线程的局部存储,这些属于每个线程的val
的地址在线程的独立栈中
三、初步理解线程互斥
1、互斥的概念
- 临界资源:多线程执行流共享的资源叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码
- 互斥:任何时刻,有且只有一个执行流进入临界区,访问临界资源(对临界资源起保护作用)
- 原子性:不会被任何调度机制打断的操作,是不可再分隔的动作,该操作只有两种状态,一是完成,二是未完成(早期化学中,原子是组成物质的最小的不可分割的单位,在这样的背景下提出的原子性)
在大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量属于单个线程,其他线程无法获得这个变量,但有时候,很多变量都需要在线程下共享,这样的变量被叫做共享变量,可以通过数据的共享,完成线程之间的交互
2、需要互斥的原因
在各个线程访问共享变量的时候,会出现多进程并发的操作,可能会带来一些问题
下面是一个经典的抢票问题,每个线程访问到共享资源的票数就给它减一,就相当于是抢走一张票
#include #include #include #include #include #include using namespace std;//四个线程一起抢票#define NUM 4class threadData{public: threadData(int number) { threadname = \"thread-\" + to_string(number); }public: string threadname;};//一次放出的票数int tickets = 1000; void *getTicket(void *args){ threadData *td = static_cast<threadData *>(args); const char *name = td->threadname.c_str(); while (true) { if(tickets > 0) { usleep(1000); //提示出是谁抢了票,以及抢到的票号 printf(\"who=%s, get a ticket: %d\\n\", name, tickets); tickets--; } else break; } printf(\"%s ... quit\\n\", name); return nullptr;}int main(){ vector<pthread_t> tids; vector<threadData *> thread_datas; for (int i = 1; i <= NUM; i++) { pthread_t tid; threadData *td = new threadData(i); thread_datas.push_back(td); //这里最后一个参数因为下标从0开始,而我们的i是从1开始的,所以i-1 pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]); tids.push_back(tid); } for (auto thread : tids) { pthread_join(thread, nullptr); } for (auto td : thread_datas) { delete td; } return 0;}
我们将形成的程序执行两遍
第一遍:
第二遍:
我们发现,抢票怎么还能抢出第0票呢,甚至还有-1、-2票?而且竟然还有抢到一张票的情况,下面我们来详解一下
首先,如果我们只讨论一个线程,整个抢票的过程就是,ticket
在内存中,线程读取ticket
,然后线程把ticket
变量放到CPU上,CPU进行--
操作,然后再放回内存中,将原来的值覆盖
我们这么说,这个过程是不是变得很慢了呢,所以在我们读取ticket
之后,其他线程也来读取了,最后我们执行一圈后,如果他们都是一起执行完的,那么原来1000的值就变成了999,他们都抢到了第1000张票,这就是重复抢到同一张票的原因
出现负数也是这个原因,只不过不是同一时间做出返回内存的行为,在CPU进行计算的时候,要重新读取数据,如果开始时所有线程都ticket==1
,判断这里就能过得去,然后一个线程拿到了最后一张票1,其他三个线程就拿到了“假票”0
、-1
、-2
,这就是我们要进行进程互斥的原因
今日分享就到这里啦~