Linux线程控制

news2024/11/16 3:19:44

写在前面

我们今天来看线程的知识点,这个博客的内容很多,主要就是为了我们后面的网络做铺垫,最关键的是相比较于进程而言,线程是更加优秀的,我们现在的计算机大多采用的就是线程.

线程

我之前谈过在创建子进程的时候,也就是fork的时候,我们可以通过条件判断来让父子进程执行不同的代码,也就是父子进程有不同的执行流,此时我们对特定的资源进行了划分.

但是我们知道创建一个进程要实例化一个task_struct,还要创建页表,虚拟地址空间…这些资源准备是在是太浪费时间了.于是程序员提出一个叫作线程(Thread)的概念,我们不创建所有的资源,只创建一个pcb,他和原本的进程共用一片空间.由于OS看待资源的时候是通过虚拟地址空间结合页表去看待的.我们通过某种方式去控制不同的PCB就可以了,此时我们把每一个PCB称之为一个线程.这就像原本有一个富豪,有很多产业,每天都要去工厂,去金融,去自己手下各种产业去转转,每天富豪很充实也很忙,年纪大了,有了很多的孩子,把家产分给自己的儿子,富豪自己也保留一个产业.原本每天串行化的去查看自己的产业,现在只需要关心自己手底下的就可以了.

image-20221220175115854

上面谈的是Linux环境下线程的实现方法,这意味不同的平台对线程的实现和管理是不一样的.我们可以很容易的想到线程和进程的数量比是n:1的,既然进程都存在自己的数据结构,按道理说线程也是应该存在的,很多教材说线程被描述为tcp,这是很对的.但是我们不得不谈到一个理念问题,不同的系统对线程的实现是不一样的,如果是一个真系统,例如Windows,他就是真正的实现出了tcp,这样的设计者把线程和进程在执行流层面看作不一样的,各自不同.那么这就带来很大的维护成本,线程和进程之间的耦合关系变得非常的复杂.

Linux并未单独的为线程创建数据结构.它是这么想的,他认为这个世界上没有进程和线程在概念的区别.他认为为只有一个执行流,以同一的视角去看它们,无非线程比较轻罢了.所以Linux中是不存在真正意义上线程,Linux使用进程模拟线程的.或者说是用pcb模拟的.你说了这么多?也就是Linux没有tcb?不是的,有的,他就叫做pcb.大家不要固执的认为不他们不一样,只不过理念的不同罢了,Linux是世界上最聪明的一批工程师来搞的…认为分开来是非常麻烦.

由于Linux内核没有为线程准备独立的接口,但是它为我们准备自己的线程库,这是Linux自身带的库.以后关于线程的只需要库里就可以了.

image-20221120110256185

进程重构

前面我们说了进程就是PCB,也就是内核数据结构 和进程对应的代码和数据.之前这么说只是为了更容易让我们理解一点,当然上面说的也是非常对的.只不过今天谈到了线程,这里重构一下.我们这里从内核视角给一个全新的定义.进程是承担分配资源的基本实体,他创建页表,链接物理内存,实例化task_struct,mmstruct这些都是为了申请资源.进程最大的意义不是被执行,是申请资源,进程是申请资源的基本单位.那么我们如何理解老的进程

  • 内部有单个执行流的进程叫做 单执行流进程
  • 内部有多个执行流的进程叫做 多执行流进程

线程VS进程

这里给大家举一个例子,在社会中假设家庭是分配资源的基本单位,而家里面的每一个人相互依存但有相互独立,我们家里面每一个人都在忙着自己的事情,例如父母勤劳工作,爷爷奶奶照顾好自己的身体,孩子好好学习,我们在忙着自己工作的最大目的也就是为了这个家庭过的更加的好,此时我们可以把每一个家庭成员看作一个线程.那么我们知道在家庭中有些东西是我们共同拥有的,例如冰箱,洗衣机等等.进程和线程也是如此,由于线程是由PCB模拟出来的.进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) \
  • 当前工作目录
  • 用户id和组id

但是我们家里面每一个人也有自己的小秘密不想别人知道,例如我们平常写的日记就不想被别人看到,线程也是如此,他也存在属于自己的数据.

  • 线程ID
  • 一组寄存器
  • errno
  • 信号屏蔽字
  • 调度优先级

你刚才给我说了这么多,我们如何证明呢?我们这里直接上代码,后面都会谈到,我们先把线程创建起来玩玩.

#include <pthread.h>
#include <iostream>
#include <unistd.h>

using namespace std;
void *callback1(void *args)
{
  while (true)
  {
    cout << "我是新线程   " << (char *)args << "... " << endl;
    sleep(1);
  }
}

void *callback2(void *args)
{
  while (true)
  {
    cout << "我是新线程   " << (char *)args << "... " << endl;
    sleep(1);
  }
}

void *rout(void *args)
{
  while (true)
  {
    cout << "我是新线程..." << endl;
    sleep(1);
  }
}

int main()
{
  pthread_t tid1;
  pthread_t tid2;
  pthread_create(&tid1, nullptr, callback1, (void *)"thread1"); // 创建线程
  pthread_create(&tid2, nullptr, callback2, (void *)"thread2"); // 创建线程

  while (true)
  {
    cout << "我是主线程..." << endl;
    sleep(1);
  }

  pthread_join(tid1, nullptr); // 等待线程
  pthread_join(tid2, nullptr); // 等待线程

  return 0;
}

image-20221220193517291

线程优点

线程相比较于 进程而言存在这很多的优点,毕竟它是接受前面的经验发展起来的.

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

我来解释一下什么是计算密集型,这个我们可以理解为解密和加密文件,IO密集型指的是我们IO频率很高.

线程缺点

  • 性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
  • 健壮性降低 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 缺乏访问控制 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高 编写与调试一个多线程程序比单线程程序困难得多

线程控制

这里来看线程的控制,很简单,主要这些思想我们在进程那里都谈过,所以大家理解起来是非常容易的.不过这个之前,我们添加一点附加的知识.

页表

我们看一下下面的代码,在学C语言的时候我们就已经知道了字符串常量是不能被修改的,那么请问既然不能被修改,它是如何被初始化的呢?这里就涉及到页表的知识点.

int main()
{
  const char* str = "hello";
  *str = 'H';
  return 0;
}

所有的一切都是和页表有关的,这里我给大家说一个观点,对于物理内存,任意一块空间都是可以读取和修改的,当我们加上了一个页表后,准确来说是当我们虚拟地址空间通过页表映射物理内存的时候,虚拟地址空间的字符常量区等等通过页表加上了某些限制使得我们对物理内存的更改变得不同.这里我想一下,那么页表是如何设计的呢?此时我们按照32位系统来和大家分析.

我们想如果是32位系统,对于每一个虚拟地址,也就是每一个字节我们都要给他映射到物理地址,也就是我们需要232个条目,每一个条目都应该有一个虚拟地址和物理地址的映射关系,我们保守假设这一个条目是8个字节.232个条目也就是32g的空间.想一想4g的空间这个是在是太大了,这样设计确实有点错.那么Linux是这么设计的吗?不是的.它的设计可以说是巧夺天公.

Linux是这样设计的,我们把32位的地址分开来算,我们把他分成10,10,12这个三个.首先我们拿第一个10个比特位来做一个索引,我们为他创建一个页表,其中左侧的就是这个10位索引,右侧的每一个条目都是一个指向页表的指针,这些指针指向的页表的左侧又是一个中间的10位,这又是一个索引.那么右侧的是什么呢?这里就要谈另外一个内容了.我们前面说了OS访问内存的基本但是是4kb,也就是一页.那么对于物理内存,我们是4g的空间.此时我们可以把物理内存划分成220个页,顺便我们也把这一个页称之为页帧.既然我们存在那么多的页,OS是不是要把它管理起来呢?是的.我们需要先描述在组织.

image-20221124232044580

那么请问我们谈了这么多和页表的设计又有什么关系呢?要知道212就是4kb.我们第三个部分就是一个页的大小,12位就是在这个页中的业内偏移量.即使我们页表是这么设计的,那么还是应该存在220个条目,感觉你说的很对,这里确实有点大了.实际上,我们这些页表也不是一下就全部加载出来的,它是用的时候再去加载.

线程创建

我们开始学习线程的创建,我们后面用到的函数都是线程库里面别人帮助我们封装好的,先来看第一个接口.

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

先来和大家蒴一下这些个参数,第一个参数是线程id,其中pthread_t就是一个无符号整型,第二个参数是一个与线程性质的有关的参数,我们不谈,这里直接设置成空指针.第三个参数是一个函数指针,后面我们线程有关的代码都是在这函数中执行的,第四个参数我们这里可以理解为是一个线程的名字,这是我们手动传入的,这个参数会作为我们第三个指针指向的函数的参数出现,后面这个用处也是很大的,要知道指针可以指向很多东西.

void *callback(void *args)
{
    while (true)
    {
      cout << "我是新线程   " << (char *)args << "... " << endl;
      sleep(1);
    }
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, nullptr, callback, (void *)"thread1");
  while (1)
  {
      cout << "我是主线程... " << endl;
      sleep(1);
  }
  
  return 0;
}

image-20221222105129141

此时我们就创建了一个新线程,我们需要查看一下.

image-20221222110244972

此时你就会发现我们只有一个进程,这作证了我们多个线程是共用一个进程的资源的,我们上面的指令是查看进程的,这里 换一下产看线程的代码.下面才是我们查看轻量级进程的指令.

image-20221222111750897

我们可以很容易看到存在两个线程,这两个线程的pid是一样的,最关键的是我们发现了另外一个编号,叫做LWP,这个我们可以理解为是线程的编号,当线程的编号和我们的pid是一样的时候,这就是我们的主线程,也就是我们之前谈到的进程.

pthread_self

那么我们疑惑了,第一个参数的作用是是什么,总不能就是一i个简单的输出型参数吧,这里有两个层次的要谈,我们先说第一层.它是该线程的线程id,算是线程的标识吧.我们这里打印一下.

int main()
{
  pthread_t tid;
  pthread_create(&tid, nullptr, callback, (void *)"thread1");
  while (1)
  {
      cout << "我是主线程... 新线程id是 " << tid << endl;
      sleep(1);
  }
  
  return 0;
}

image-20221222113127932

除了我们在主线程可以访问到线程的id,新线程也是可以访问到自己的id的,这里需要调用一个函数.

pthread_t pthread_self(void);
void *callback(void *args)
{
  while (true)
  {
    cout << "我是新线程,线程id  " << pthread_self() << "... " << endl;
    sleep(1);
  }
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, nullptr, callback, (void *)"thread1");
  while (1)
  {
    cout << "我是主线程... 新线程id是 " << tid << endl;
    sleep(1);
  }

  return 0;
}

image-20221222113950159

线程等待

和进程一样,线程结束后我们也是需要等待的,如果不进行等待,就会出现和僵尸进程类似的东西,造成内存泄漏这一点是需要我们明白的.但是我看下面的的代码也没有显示出线程变成类似僵尸那样的状态啊,这是由于我们这个指令对于已经死去的线程不在进行展示了.

void *callback(void *args)
{
  int cnt = 10;
  while (cnt)
  {
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
}

int main()
{
  pthread_t tid1;
  pthread_t tid2;
  pthread_t tid3;
  pthread_create(&tid1, nullptr, callback, (void *)"thread1");
  pthread_create(&tid2, nullptr, callback, (void *)"thread1");
  pthread_create(&tid3, nullptr, callback, (void *)"thread1");
  while (1)
  {
    cout << "我是主线程... " << endl;
    sleep(1);
  }

  return 0;
}

20221222_114725

看一下线程等待函数,这个可以等待我们指定的线程

int pthread_join(pthread_t thread, void **retval);

关于参数,第一个就是我们要等待的线程id,第二个是一个二级指针,这里先暂时不谈,直接置为空,它的等待是阻塞式等待

void *callback(void *args)
{
  int cnt = 5;
  while (cnt)
  {
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
}

int main()
{
  pthread_t tid1;
  pthread_t tid2;
  pthread_t tid3;
  pthread_create(&tid1, nullptr, callback, (void *)"thread1");
  pthread_create(&tid2, nullptr, callback, (void *)"thread1");
  pthread_create(&tid3, nullptr, callback, (void *)"thread1");

  pthread_join(tid1, nullptr);
  pthread_join(tid2, nullptr);
  pthread_join(tid3, nullptr);

  cout << "线程等待结束 " << endl;
  sleep(5);

  return 0;
}

线程分离

我们上面的线程等待是阻塞式的等待,也就是如果我们要是在等待某一个新线程,这个时候我们主线程就不能执行自己的任务,那么能不能不等待这个线程呢?可以的,此时我们就需要线程分离,我们可以把一个线程和主线程进行分离,不再关心他的运行情况,如果被分离的线程结束了,资源会被自动的回收.

int pthread_detach(pthread_t thread);
void *callback(void *args)
{
  int cnt = 5;
  while (cnt)
  {
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
}

int main()
{
  pthread_t tid1;

  pthread_create(&tid1, nullptr, callback, (void *)"thread1");

  pthread_detach(tid1);

  cout << "线程等待结束 " << endl;
  sleep(10);

  return 0;
}

image-20221223114513878

这里还是不能说明我们线程已经分离.被分离的线程是不能被join的,否则就会报错,这句话我们可以验证一下.

void *callback(void *args)
{
  pthread_detach(pthread_self());
  cout << "线程分离" << endl;
  int cnt = 50;
  while (cnt)
  {
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
}
int main()
{
  pthread_t tid1;
  pthread_t tid2;
  pthread_t tid3;
  pthread_create(&tid1, nullptr, callback, (void *)"thread 1");
  pthread_create(&tid2, nullptr, callback, (void *)"thread 2");
  pthread_create(&tid3, nullptr, callback, (void *)"thread 3");

  int n = pthread_join(tid1, nullptr);
  cout << n << ":" << strerror(n) << endl;
  n = pthread_join(tid2, nullptr);
  cout << n << ":" << strerror(n) << endl;
  n = pthread_join(tid3, nullptr);
  cout << n << ":" << strerror(n) << endl;

  return 0;
}

20221223_140643

我们很是疑惑,我们不是线程分离了吗?为何我看main主线程还在阻塞等待,这是由于我们再创建新线程的时候,新线程去执行自己的了,但是主线程还是需要向下执行的,这就是一旦主线程先碰到join,那么线程分离就没有效果了.那么是不是呢?,我们主线程等待一下.

int main()
{
  pthread_t tid1;
  pthread_t tid2;
  pthread_t tid3;
  pthread_create(&tid1, nullptr, callback, (void *)"thread 1");
  pthread_create(&tid2, nullptr, callback, (void *)"thread 2");
  pthread_create(&tid3, nullptr, callback, (void *)"thread 3");
  sleep(1);
  int n = pthread_join(tid1, nullptr);
  cout << n << ":" << strerror(n) << endl;
  n = pthread_join(tid2, nullptr);
  cout << n << ":" << strerror(n) << endl;
  n = pthread_join(tid3, nullptr);
  cout << n << ":" << strerror(n) << endl;

  return 0;
}

20221223_140643_1

此时我们就知道我们想法是没有错的,也就是我们线程分离最好是在主线程进行分离,这样会极大的避免上面分离不成功的现象.

void *callback(void *args)
{
  int cnt = 50;
  while (cnt)
  {
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
}
int main()
{
  pthread_t tid1;
  pthread_t tid2;
  pthread_t tid3;
  pthread_create(&tid1, nullptr, callback, (void *)"thread 1");
  pthread_create(&tid2, nullptr, callback, (void *)"thread 2");
  pthread_create(&tid3, nullptr, callback, (void *)"thread 3");

  pthread_detach(tid1);
  pthread_detach(tid2);
  pthread_detach(tid3);

  int n = pthread_join(tid1, nullptr);
  cout << n << ":" << strerror(n) << endl;
  n = pthread_join(tid2, nullptr);
  cout << n << ":" << strerror(n) << endl;
  n = pthread_join(tid3, nullptr);
  cout << n << ":" << strerror(n) << endl;

  return 0;
}

20221223_140643_2

线程结束

我们知道进程结束只能有三种情况,这里就不列举了,那么线程呢?线程如果遇到异常,会怎么样?我们来一个测试

void *callback(void *args)
{
  int cnt = 5;
  while (cnt)
  {
    if (cnt == 2)
    {
      cout << "注意我要除零了" << endl;
      int a = 10;
      a /= 0;
    }
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
  pthread_exit((void *)10);
}

void handler(int signo)
{
  cout << "我是一个进程 pid " << getpid() << ",刚刚获取了一个信号: " << signo << endl;
  exit(1); // 这里捕捉完直接退出
}

int main()
{
  signal(8, handler);
  pthread_t tid1;

  pthread_create(&tid1, nullptr, callback, (void *)"thread1");
  void *ret = nullptr;
  pthread_join(tid1, &ret);
  cout << "线程等待结束 ret  " << (long long)ret << endl;
  while (1)
  {
    cout << "我是主线程... " << endl;
    sleep(1);
  }

  return 0;
}

20221222_120508_1

此时我们就发现,一旦线程出现异常,是整个程序就会崩溃,这就线程的健壮性较低,我们把这一特性称之鲁棒性.

此时此时线程结束只能存在两种情况,一是正确跑完了,一是不正确跑完了.此时我们就可以谈join接口的第二个参数了,它是给主线程返回一个指针,该指针指向一片空间,不过我们返回的时候这片空间不要被销毁.

void *callback(void *args)
{
  int cnt = 5;
  while (cnt)
  {
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
  return (void *)10;
}
int main()
{
  pthread_t tid1;

  pthread_create(&tid1, nullptr, callback, (void *)"thread1");
  void *ret = nullptr;
  pthread_join(tid1, &ret);
  cout << "线程等待结束 ret  " << (long long)ret << endl;
  sleep(5);

  return 0;
}

20221222_120508_2

由于线程遇到异常整个程序就会退出,也就是我们不能使用exit函数了,不过线程库还是给我们准备了一个接口.

void pthread_exit(void *retval);
void *callback(void *args)
{
  int cnt = 5;
  while (cnt)
  {
    cout << "我是新线程   线程id" << (char *)args << " cnt  " << cnt << endl;
    sleep(1);
    cnt--;
  }
  pthread_exit((void *)10);
}

int main()
{
  pthread_t tid1;

  pthread_create(&tid1, nullptr, callback, (void *)"thread1");
  void *ret = nullptr;
  pthread_join(tid1, &ret);
  cout << "线程等待结束 ret  " << (long long)ret << endl;
  sleep(5);

  return 0;
}

20221222_120508_2

线程取消

除了线程自动退出外,我们也是可以在主线程中手动取消一个线程的,这里需要借助一个函数,不太常用

int pthread_cancel(pthread_t thread);
void *startRoutine(void *args)
{
  while (true)
  {
    cout << "thread 正在运行..." << endl;
    sleep(1);
  }
}
int main()
{

  pthread_t tid;
  int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");

  (void)n;

  sleep(3); // 代表main thread对应的工作
  pthread_cancel(tid);
  cout << "new thread been canceled" << endl;

  void *ret = nullptr;    
  pthread_join(tid, &ret);
  cout << "main thread join success, ret: " << (long long)ret << endl;
  sleep(1);
  return 0;
}

image-20221223133534833

.使用这个函数来取消线程,线程的退出结果是-1.这个-1就是一个宏

#define PTHREAD_CANCELED ((void *) -1)

线程栈

上面有关线程常见的接口我们已经谈完了,这里就剩下一个内容了,tid究竟是什么?我们按十六进制打印下.

void *callback(void *args)
{
  while (1)
  {
  }
  pthread_exit((void *)10);
}


int main()
{
  
  pthread_t tid;

  pthread_create(&tid, nullptr, callback, (void *)"thread1");
  while (1)
  {
    cout << "我是主线程... ";
    printf("新线程id %p\n", tid);
    sleep(1);
  }

  return 0;
}

image-20221222134524174

他就是一个地址,我们知道线程是一个独立的执行流,他在运行的过程中一定会产生数据,例如调用函数,定义局部变量,这个时候线程一定需要自己的独立的栈结构.前面我们一直说,Linux没有为线程提供独立的线程数据结构,但是它给我们提供了一个线程库,这个库做的非常好,其中线程库中就有一个线程栈,当我们程序遇到创建线程的接口,此时我们物理内存还没有线程库,OS会把存放在磁盘的线程库加载到物理内存中,然后通过页表映射到共享区.

20221222_133845

也就是对于没一个线程,我们都可以理解成加载一个线程库,那么请问我们计算机运行的线程可是不少的,要不要被管理呢?是的,我们就要把线程描述组织,其中我们需要描述线程,它可以认为是一个结构体,我这里给大家假设性的演示一下.

struct thread_info
{
  pthread_t tid;
  void* stack; // 线程栈
};

其中,我们得到的tid值就是这个结构体的地址,关于线程栈,我给大家演示一下.我们知道,线程是共享进程的,也就是如果我们定义一个全局变量,我们是都可以看到的.

void *callback(void *args)
{
  char *name = static_cast<char *>(args);

  while (1)
  {
    cout << "我是新线程 " << name << " 我看到一个全局变量 g_val: "
         << g_val << "  地址是 " << &g_val << endl;
    sleep(1);
  }
}

int main()
{

  pthread_t tid1;
  pthread_t tid2;
  pthread_t tid3;

  pthread_create(&tid1, nullptr, callback, (void *)"thread1");
  pthread_create(&tid2, nullptr, callback, (void *)"thread2");
  pthread_create(&tid3, nullptr, callback, (void *)"thread3");

  pthread_join(tid1, nullptr);
  pthread_join(tid2, nullptr);
  pthread_join(tid3, nullptr);

  return 0;
}

image-20221223112158801

这个时候,我们可以看到所有的线程都是可以看到这个全局变量,也就是如果我们一个线程修改了,其他的线程看到都是修改过的值(这里先不谈原子性).

局部存储

那么我们是不是可以把这个全局变量让各个线程私有呢?除了我们可以在自己的函数体内定义,我们可以给全局变量上面加上一个修饰词,这样我们看到 变量就不一样了,修改的就是自己线程的值,不会影响其他的.

using namespace std;
__thread int g_val = 10;

void *callback(void *args)
{
  char *name = static_cast<char *>(args);

  while (1)
  {
    cout << "我是新线程 " << name << " 我看到一个全局变量 g_val: "
         << g_val << "  地址是 " << &g_val << endl;
    sleep(1);
  }
}

int main()
{

  pthread_t tid1;
  pthread_t tid2;
  pthread_t tid3;

  pthread_create(&tid1, nullptr, callback, (void *)"thread1");
  pthread_create(&tid2, nullptr, callback, (void *)"thread2");
  pthread_create(&tid3, nullptr, callback, (void *)"thread3");

  pthread_join(tid1, nullptr);
  pthread_join(tid2, nullptr);
  pthread_join(tid3, nullptr);

  return 0;
}

image-20221223112311459

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/126127.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作系统期末考试必会题库4——设备管理

1、DMA方式和中断控制方式的主要区别是什么&#xff1f; 2、面向块设备和面向流设备有何区别&#xff1f;各举一些例子 面向块的设备将信息保存在块中&#xff0c;块的大小通常是固定的&#xff0c;传送过程中一次传送一块。通常可以通过块号访问数据。磁盘和USB智能卡都是面向…

【Linux】Linux权限(一)文件权限和目录权限

Linux权限1.Linux权限的概念2.Linux的用户分类3.Linux的文件类型3.1如何看待Linux下的文件后缀4.Linux的文件权限5.Linux下切换用户指令6.Linux文件访问者的分类&#xff08;拥有者、所属组、other&#xff09;6.1root 和普通用户 与 拥有者和所属组和其他人的关系6.2 如何描述…

matlab中ginput函数的用法

仅用来记录自己学习中不会的函数 ginput函数&#xff1a;来自鼠标或光标的图形输入 一、语法 [x,y] ginput(n) [x,y] ginput [x,y,button] ginput(…) 二、说明 ginput 提高当前坐标区中的交叉线以供您标识图窗中的点&#xff0c;从而使用鼠标定位光标。图窗必须具有焦点…

【Linux】 第八部分 Linux常用基本命令

【Linux】 第八部分 Linux常用基本命令 文章目录【Linux】 第八部分 Linux常用基本命令8. Linux常用基本命令8.1 帮助命令8.2 文件目录类命令pwd 显示当前工作目录的绝对路径cd 切换目录ls 列出目录的内容mkdir 创建目录rmdir 删除目录touch 创建文件cp 复制文件或者目录rm 删除…

网络编程 异步选择模型

目录 1.概念 2.代码样例 1.概念 基本概念&#xff0c;在这一个模型中的代码使用到了vs中窗口应用程序&#xff0c;可以看这一片文章https://blog.csdn.net/weixin_62859191/article/details/128415737?spm1001.2014.3001.5501https://blog.csdn.net/weixin_62859191/article/d…

站在2023起跑线,政企数字化如何深入“核心地带”?

今天&#xff0c;各行各业都积极开展数字化变革&#xff0c;以云为底座开展数字化已成为行业共识。而更进一步观察会发现&#xff0c;大型政企作为数字化转型的先行者和主力军&#xff0c;已经从资源上云、应用上云阶段&#xff0c;率先抵达了数字化深水区&#xff0c;迈入了深…

UDS-Data transmission functional unit

11 数据传输功能单元 11.1 概述 表185指定了数据传输功能单元。 注&#xff1a; ReadDataByIdentifier&#xff1a;客户端通过提供的dataIdentifier去请求读取标识记录的当前值。ReadMemoryByAddress&#xff1a;客户端请求读取所提供的内存范围的当前值。ReadScalingData…

机器学习100天(十九):019 分类模型评价指标-混淆矩阵

机器学习100天,今天讲的是:分类模型评价指标-混淆矩阵。 《机器学习100天》完整目录:目录 一、准确率(Accuracy) 逻辑回归是一个分类模型,那么对于分类模型,如何评估它的优劣呢? 好,我们先来认识一个名词:混淆矩阵(confusion matrix)。混淆矩阵是用来衡量一个分…

C语言预处理器

C 预处理器不是编译器的组成部分&#xff0c;但是它是编译过程中一个单独的步骤。简言之&#xff0c;C 预处理器只不过是一个文本替换工具而已&#xff0c;它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器&#xff08;C Preprocessor&#xff09;简写为 …

pybind11学习 | 使用CMake构建系统并生成pyd文件

文章目录一 通过Visual Studio生成二 通过CMake生成一 通过Visual Studio生成 在我之前的一篇文章pybind11学习 | VS2022下安装配置中&#xff0c;描述了如何通过配置VS2022实现pybind11项目构建和编译成Python拓展模块。 二 通过CMake生成 项目结构如下&#xff1a; 其中py…

(二十七)Vue组件的样式

文章目录scoped属性实现原理lang属性样式穿透Vue学习目录 上一篇&#xff1a;&#xff08;二十六&#xff09;Vue之插件 scoped属性 scoped 属性是 HTML5 中的新属性。如果使用该属性&#xff0c;则样式仅仅应用到 style 元素的父元素及其子元素。即让样式在局部生效&#x…

kafka多线程消费

Kafka consumer 多线程消费 kafka 消费者对象 - KafkaConsumer是非线程安全的。与KafkaProducer不同&#xff0c;KafkaProducer是线程安全的&#xff0c;因为开发者可以在多个线程中放心地使用同一个KafkaProducer实例。 但是对于消费者而言&#xff0c;由于它是非线程安全的…

FreeFileSync 11.29 发布

导读FreeFileSync 是一款开源软件&#xff0c;适用于 Windows、macOS 和 Linux。FreeFileSync 本质是一个用于文件夹对比和同步的软件&#xff0c;它可以创建和管理所有重要文件的备份副本。FreeFileSync 不是每次都复制每个文件&#xff0c;而是确定源文件夹和目标文件夹之间的…

使用 ClusterResourceSet 为 Cluster API 集群自动安装 CNI 插件

1 什么是 Cluster API Cluster API[1] 是一个 Kubernetes 子项目&#xff0c;它将声明式、Kubernetes 风格的 API 引入到集群的创建、配置和管理中。Cluster API 支持在 AWS, Azure, GCP, vSphere, KubeVirt 等多种环境中创建和管理 Kuberenetes 集群&#xff0c;并负责提供部…

单例(Singleton)设计模式

一、单例(Singleton)设计模式说明 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。就像是经典的棋谱&#xff0c;不同的棋局&#xff0c;我们用不同的棋谱&#xff0c;"套路"所谓类…

WebView以及使用HTTP访问访问网络

文章目录使用网络技术WebView的用法使用HTTP访问网络使用HttpURLConnection使用OkHttp使用网络技术 在Android开发当中,我们应该合理的使用网络编写出更加出色的应用程序,下面学习以下如何在手机端使用HTTP和服务器进行网络交互,并对服务器返回的数据进行解析,这也是在Android…

爽啊,这么多有趣好玩强大的 Python 库

Python语言简洁、易读以及可扩展&#xff0c;在国内外用 Python 做研究的非常多。 Python 语言向来以丰富的第三方库而闻名。这么多有趣好玩且强大&#xff0c;靠一个人去寻找太难了。 最近粉丝群小伙伴们又罗列了一些&#xff0c;分享给大家。喜欢记得点个赞&#xff0c;加入…

OpenHarmony#深入浅出学习eTs#(三)UI布局

本项目Gitee仓地址&#xff1a;深入浅出eTs学习: 带大家深入浅出学习eTs (gitee.com) 一、ArkUI介绍 框架介绍 方舟开发框架&#xff08;简称&#xff1a;ArkUI&#xff09;&#xff0c;是一套UI开发框架&#xff0c;提供开发者进行应用UI开发时所必需的能力。 基本概念 组…

力扣sql入门篇(二)

力扣sql入门篇(二) 1 计算特殊奖金 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT employee_id, case when employee_id%21 AND name not like "M%" then salary else 0 end bonus FROM Employees ORDER BY employee_id;1.3 运行…

【软件测试】测试人的一份“漂亮“的年终总结报告......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 不管这一年&#xf…