一、线程等待的原理
pThread_join 函数用于实现线程等待。其中的 retval 参数用于传递目标线程的退出状态。当目标线程结束时,pthread_join 会将目标线程的退出状态(即线程函数的返回值或通过 pthread_exit 传递的参数)存储在 *retval 所指向的内存位置上。换句话说,pthread_join 会修改 retval 所指向的 void * 类型变量的值。
以下是相关的代码示例:
#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int g_val = 100; void *threadRoutine(void *args) { const char *name = (const char *)args; int cnt = 5; while (true) { printf("%s, pid: %d, g_val: %d, &g_val: 0X%pn", name, getpid(), g_val, &g_val); sleep(1); cnt--; if (cnt == 0) break; } pthread_exit((void *)100); } int main() { pthread_t pid; pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1"); void *ret; pthread_join(pid, &ret); cout << "Thread returned: " << (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 <iostream> #include <pthread.h> #include <vector> #include <string> #include <unistd.h> using namespace std; #define NUM 3 int *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); while(i < 5) { printf("%s, tid: %s, pid: %d, val: %d, &val: 0X%pn", ti->threadname_.c_str(), toHex(pthread_self()).c_str(), getpid(), val, &val); val++; i++; sleep(1); } return nullptr; } int main() { vector<pthread_t> tids; vector<ThreadInfo> thread_datas; for(int i = 0; i < NUM; i++) { thread_datas.emplace_back("Thread-" + to_string(i + 1)); pthread_t tid; pthread_create(&tid, nullptr, threadroutine, &thread_datas.back()); tids.push_back(tid); } for(auto tid : tids) { pthread_join(tid, nullptr); } return 0; }
通过观察我们可以发现,在相同线程的情况下,val 的值是递增的,但对于不同的线程之间,val 值是没有关系的。因此,我们通过关键字 __thread 实现了线程的局部存储,这些属于每个线程的 val 的地址在线程的独立栈中。
三、初步理解线程互斥
-
互斥的概念
- 临界资源:多线程执行流共享的资源称为临界资源。
- 临界区:每个线程内部,访问临界资源的代码称为临界区。
- 互斥:任何时刻,有且只有一个执行流进入临界区,访问临界资源(对临界资源起保护作用)。
- 原子性:不会被任何调度机制打断的操作,是不可再分隔的动作,该操作只有两种状态,一是完成,二是未完成(早期化学中,原子是组成物质的最小的不可分割的单位,在这样的背景下提出的原子性)。
在大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况下,变量属于单个线程,其他线程无法获得这个变量。但有时候,很多变量需要在线程之间共享,这些变量被称为共享变量,可以通过数据的共享,完成线程之间的交互。
-
需要互斥的原因
在各个线程访问共享变量的时候,会出现多进程并发的操作,可能会带来一些问题。
下面是一个经典的抢票问题,每个线程访问到共享资源的票数就给它减一,就相当于是抢走一张票。
以下是相关的代码示例:
#include <iostream> #include <cstdio> #include <cstring> #include <vector> #include <unistd.h> #include <pthread.h> using namespace std; #define NUM 4 class 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: %dn", name, tickets); tickets--; } else break; } printf("%s ... quitn", name); return nullptr; } int main() { vector<pthread_t> tids; vector<threadData> thread_datas; for (int i = 1; i <= NUM; i++) { thread_datas.emplace_back(i); pthread_t tid; pthread_create(&tid, nullptr, getTicket, &thread_datas.back()); tids.push_back(tid); } for(auto tid : tids) { pthread_join(tid, nullptr); } return 0; }
我们将程序执行两遍:
第一遍:
第二遍:
我们发现,抢票怎么还能抢出第0票呢,甚至还有-1、-2票?而且竟然还有抢到一张票的情况,下面我们来详解一下。
首先,如果我们只讨论一个线程,整个抢票的过程就是,ticket 在内存中,线程读取 ticket,然后线程把 ticket 变量放到 CPU 上,CPU 进行 — 操作,然后再放回内存中,将原来的值覆盖。我们这么说,这个过程是不是变得很慢了呢,所以在我们读取 ticket 之后,其他线程也来读取了,最后我们执行一圈后,如果他们都是一起执行完的,那么原来1000的值就变成了999,他们都抢到了第1000张票,这就是重复抢到同一张票的原因。出现负数也是这个原因,只不过不是同一时间做出返回内存的行为,在 CPU 进行计算的时候,要重新读取数据,如果开始时所有线程都 ticket==1,判断这里就能过得去,然后一个线程拿到了最后一张票1,其他三个线程就拿到了“假票”0、-1、-2,这就是我们要进行进程互斥的原因。