【Linux】深入理解线程控制

一、线程等待的原理

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; }

【Linux】深入理解线程控制

通过上面的代码和图片,我们可以看到,新线程的输出参数可以被主线程获取,并且全局变量可以被所有线程访问,是共享资源,因此全局函数也可以被所有线程访问。

&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; }

【Linux】深入理解线程控制

通过观察我们可以发现,在相同线程的情况下,val 的值是递增的,但对于不同的线程之间,val 值是没有关系的。因此,我们通过关键字 __thread 实现了线程的局部存储,这些属于每个线程的 val 的地址在线程的独立中。

三、初步理解线程互斥

  1. 互斥的概念

    • 临界资源多线程执行流共享的资源称为临界资源。
    • 临界区:每个线程内部,访问临界资源的代码称为临界区。
    • 互斥:任何时刻,有且只有一个执行流进入临界区,访问临界资源(对临界资源起保护作用)。
    • 原子性:不会被任何调度机制打断的操作,是不可再分隔的动作,该操作只有两种状态,一是完成,二是未完成(早期化学中,原子是组成物质的最小的不可分割的单位,在这样的背景下提出的原子性)。

    在大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况下,变量属于单个线程,其他线程无法获得这个变量。但有时候,很多变量需要在线程之间共享,这些变量被称为共享变量,可以通过数据的共享,完成线程之间的交互。

  2. 需要互斥的原因

    在各个线程访问共享变量的时候,会出现多进程并发的操作,可能会带来一些问题。

    下面是一个经典的抢票问题,每个线程访问到共享资源的票数就给它减一,就相当于是抢走一张票。

    以下是相关的代码示例:

#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; }

我们将程序执行两遍:

第一遍:

【Linux】深入理解线程控制

第二遍:

【Linux】深入理解线程控制

我们发现,抢票怎么还能抢出第0票呢,甚至还有-1、-2票?而且竟然还有抢到一张票的情况,下面我们来详解一下。

首先,如果我们只讨论一个线程,整个抢票的过程就是,ticket 在内存中,线程读取 ticket,然后线程把 ticket 变量放到 CPU 上,CPU 进行 — 操作,然后再放回内存中,将原来的值覆盖。我们这么说,这个过程是不是变得很慢了呢,所以在我们读取 ticket 之后,其他线程也来读取了,最后我们执行一圈后,如果他们都是一起执行完的,那么原来1000的值就变成了999,他们都抢到了第1000张票,这就是重复抢到同一张票的原因。出现负数也是这个原因,只不过不是同一时间做出返回内存的行为,在 CPU 进行计算的时候,要重新读取数据,如果开始时所有线程都 ticket==1,判断这里就能过得去,然后一个线程拿到了最后一张票1,其他三个线程就拿到了“假票”0、-1、-2,这就是我们要进行进程互斥的原因。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享