Linux 多线程编程详解

简介: Linux 多线程编程详解

为什么要使用多线程

在编写代码时,是否会遇到以下的场景会感觉到难以下手?


要做 2 件事,一件需要阻塞等待,另一件需要实时进行。例如播放器:一边 在屏幕上播放视频,一边在等待用户的按键操作。如果使用单线程的话,程序必 须一会查询有无按键,一会播放视频。查询按键太久,就会导致视频播放卡顿; 视频播放太久,就无法及时响应用户的操作。并且查询按键和播放视频的代码混杂在一起,代码丑陋。


如果使用多线程,线程 1 单独处理按键,线程 2 单独处理播放,可以完美解决上述问题


线程概念

所谓线程,就是操作系统所能调度的最小单位。


普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量, 每个线程都可以去访问它,与进程共享“4G”内存空间,使得系统资源消耗减少。 本章节来讨论 Linux 下 POSIX 线程。


线程的标识 pthread_t

对于进程而言,每一个进程都有一个唯一对应的 PID 号来表示该进程


而对于线程而言,也有一个“类似于进程的 PID 号”,名为 tid,其本质是一个 pthread_t 类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。


获取线程号


#include <pthread.h>


pthread_t pthread_self(void);


成功:返回线程号


在程序中,可以通过函数 pthread_self,来返回当前线程的线程号,例程 1 给出了打印线程 tid 号。

测试例程 1:(Phtread_txex1.c)

#include <pthread.h>
#include <stdio.h>
 
int main()
{
  pthread_t tid = pthread_self();
  printf("tid = %lu\n",(unsigned long)tid);
  return 0;
}

因采用 POSIX 线程接口,故在要编译的时候包含 pthread 库

使用 gcc 编译应 gcc xxx.c -lpthread 方可编译多线程程序

编译结果:

线程的创建

怎么创建线程呢?


使用 pthread_create 函数:


创建线程


#include <pthread.h>


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


  • 第一个参数为 pthread_t 指针,用来保存新建线程的线程号;
  • 第二个参数表示了线程的属性,一般传入 NULL 表示默认属性;
  • 第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为 void*, 形参为 void*。
  • 第四个参数则表示为向线程处理函数传入的参数,若不传入,可用 NULL 填充, 有关线程传参后续小节会有详细的说明,接下来通过一个简单例程来使用该函数创建出一个线程。

测试例程 2:(Phtread_txex2.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
 
void *fun(void *arg)
{
  printf("pthread_New = %lu\n",(unsigned long)pthread_self());
}
 
int main()
{
 
  pthread_t tid1;
  int ret = pthread_create(&tid1,NULL,fun,NULL);
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
 
  /*tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间*/
  printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
  
  /*因线程执行顺序随机,不加sleep可能导致猪线程先执行,导致进程结束,无法执行到子线程*/
  sleep(1);
 
  return 0;
}

运行结果:

通过 pthread_create 确 实 可 以 创 建 出 来 线 程 , 主 线 程 中 执 行 pthread_create 后 的 tid 指向了线程号空间,与子线程通过函数 pthread_self 打印出来的线程号一致。


特别说明的是,当主线程伴随进程结束时,所创建出来的线程也会立即结束, 不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一 个线程会先运行。可以将上述代码中 sleep 函数进行注释,观察实验现象。

上述运行代码 3 次,其中有 2 次被进程结束,无法执行到子线程的逻辑,最后一 次则执行到了子线程逻辑后结束的进程。如此可以说明,线程的执行顺序不受控制,且整个进程结束后所产生的线程也随之被释放,在后续内容中将会描述如何 控制线程执行。

向线程传入参数

pthread_create()的最后一个参数的为 void*类型的数据,表示可以向线 程传递一个 void*数据类型的参数,线程的回调函数中可以获取该参数,例程 3 举例了如何向线程传入变量地址与变量值。

测试例程 3:(Phtread_txex3.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
 
void *fun1(void *arg)
{
  printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
}
 
void *fun2(void *arg)
{
  printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
}
 
int main()
{
 
  pthread_t tid1,tid2;
  int a = 50;
  int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); // 21 创建线程传入变量 a 的地址
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a); // 27 创建线程传入变量 a 的值
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  sleep(1);
  printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
  return 0;
}

运行结果:

例程展示了如何利用线程创建函数的第四个参数向线程传入数据,举例了如何以地址的方式传入值、以变量的方式传入值,

例程代码的 21 行,是将变量 a 先行取地址后,再次强制类型转化为 void*后传入线程,线程处理的回调函数 中,先将万能指针 void*转化为 int*,再次取地址就可以获得该地址变量的值, 其本质在于地址的传递。


例程代码的 27 行,直接将 int 类型的变量强制转化为 void*进行传递(针对不同位数机器,指针对其字数不同,需要 int 转化为 long 在转指针,否则可能会发生警告),在线程处理回调函数中,直接将 void*数据转 248 / 566 化为 int 类型即可,本质上是在传递变量 a 的值。 上述两种方法均可得到所要的值,但是要注意其本质,一个为地址传递,一 个为值的传递。当变量发生改变时候,传递地址后,该地址所对应的变量也会发 生改变,但传入变量值的时候,即使地址指针所指的变量发生变化,但传入的为 变量值,不会受到指针的指向的影响,实际项目中切记两者之间的区别。具体说明见例程 4。


测试例程 4:(Phtread_txex4.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
 
void *fun1(void *arg)
{
  while(1){
  
    printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
    sleep(1);
  }
}
 
void *fun2(void *arg)
{
  while(1){
  
    printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
    sleep(1);
  }
}
 
int main()
{
 
  pthread_t tid1,tid2;
  int a = 50;
  int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  sleep(1);
  ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  while(1){
    a++;
    sleep(1);
    printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
  }
  return 0;
}

运行结果:

上述例程讲述了如何向线程传递一个参数,在处理实际项目中,往往会遇到 传递多个参数的问题,我们可以通过结构体来进行传递,解决此问题。

测试例程 5:(Phtread_txex5.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
 
struct Stu{
  int Id;
  char Name[32];
  float Mark;
};
 
void *fun1(void *arg)
{
  struct Stu *tmp = (struct Stu *)arg;
  printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark);
  
}
 
int main()
{
 
  pthread_t tid1,tid2;
  struct Stu stu;
  stu.Id = 10000;
  strcpy(stu.Name,"ZhangSan");
  stu.Mark = 94.6;
 
  int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu);
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
  sleep(1);
  return 0;
}

运行结果:

线程的退出与回收

线程的退出情况有三种:第一种是进程结束,进程中所有的线程也会随之结束。


第二种是通过函数 pthread_exit 来主动的退出线程。


第三种被其他线程调用 pthread_cancel 来被动退出。


当线程结束后,主线程可以通过函数 pthread_join/pthread_tryjoin_np 来回收线程的资源,并且获得线程结束后需要返回的数据。


线程主动退出

pthread_exit 函数原型如下:


线程主动退出


#include <pthread.h>


void pthread_exit(void *retval);


pthread_exit 函数为线程退出函数,在退出时候可以传递一个 void*类型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL。


线程被动退出

pthread_cancel 函数原型如下:


线程被动退出,其他线程使用该函数让另一个线程退出


#include <pthread.h>


int pthread_cancel(pthread_t thread);


成功:返回 0


该函数传入一个 tid 号,会强制退出该 tid 所指向的线程,若成功执行会返回 0。


线程资源回收(阻塞方式)

thread_join 函数原型如下:


线程资源回收(阻塞)


#include <pthread.h>


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


该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的 tid 号,第二个参数为线程回收后接受线程传出的数据。


线程资源回收(非阻塞方式)

pthread_tryjoin_np 函数原型如下:


线程资源回收(非阻塞)


#define _GNU_SOURCE


#include <pthread.h>


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


该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回 收则返回 0,其余参数与 pthread_join 一致。


测试例程 6:(Phtread_txex6.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
 
void *fun1(void *arg)
{
  static int tmp = 0; // 8 必须要static修饰,否则pthread_join无法获取到正确值
  //int tmp = 0;
  tmp = *(int *)arg;
  tmp+=100;
  printf("%s:Addr = %p tmp = %d\n",__FUNCTION__,&tmp,tmp);
  pthread_exit((void *)&tmp);
}
 
 
int main()
{
 
  pthread_t tid1;
  int a = 50;
  void *Tmp = NULL;
  int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); // 23
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  pthread_join(tid1,&Tmp);
  printf("%s:Addr = %p Val = %d\n",__FUNCTION__,Tmp,*(int *)Tmp);
  return 0;
}

运行结果:

上述例程先通过 23 行将变量以地址的形式传入线程,在线程中做出了自加 100 的操作,当线程退出的时候通过线程传参,用 void*类型的数据通过 pthread_join 接 受 。 此 例 程 去 掉 了 之 前 加 入 的 sleep 函 数 , 原 因 是 pthread_join 函数具备阻塞的特性,直至成功收回掉线程后才会冲破阻塞,因 此不需要靠考虑主线程会执行到 30 行结束进程的情况。


特别要说明的是例程第 8 行,当变量从线程传出的时候,需要加 static 修饰,对生命周期做出延续, 否则无法传出正确的变量值。


测试例程 7:(Phtread_txex7.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
 
void *fun(void *arg)
{
  printf("Pthread:%d Come !\n",(int )(long)arg+1);
  pthread_exit(arg);
}
 
 
int main()
{
  int ret,i,flag = 0;
  void *Tmp = NULL;
  pthread_t tid[3];
  for(i = 0;i < 3;i++){
    ret = pthread_create(&tid[i],NULL,fun,(void *)(long)i);
    if(ret != 0){
      perror("pthread_create");
      return -1;
    }
  }
  while(1){
    for(i = 0;i <3;i++){
      if(pthread_tryjoin_np(tid[i],&Tmp) == 0){
        printf("Pthread : %d exit !\n",(int )(long )Tmp+1);
        flag++; 
      }
    }
    if(flag >= 3) break;
  }
  return 0;
}

运行结果:

例程 7 展示了如何使用非阻塞方式来回收线程,此外也展示了多个线程可 以指向同一个回调函数的情况。

例程 6 通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的 线程无法及时的回收。


通过函数 pthread_tryjoin_np,使用非阻塞回收,线程可以根据退出先 后顺序自由的进行资源的回收。


测试例程 8:(Phtread_txex8.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
 
void *fun1(void *arg)
{
  printf("Pthread:1 come!\n");
  while(1){
    sleep(1);
  }
}
 
void *fun2(void *arg)
{
  printf("Pthread:2 come!\n");
  pthread_cancel((pthread_t )(long)arg); // 杀死线程 1,使之强制退出
  pthread_exit(NULL);
}
 
int main()
{
  int ret,i,flag = 0;
  void *Tmp = NULL;
  pthread_t tid[2];
  ret = pthread_create(&tid[0],NULL,fun1,NULL); // 27
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  sleep(1);
  ret = pthread_create(&tid[1],NULL,fun2,(void *)tid[0]); // 33 传输线程 1 的线程号
 
  if(ret != 0){
    perror("pthread_create");
    return -1;
  }
  while(1){ //通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至 2 个线程全数回收
    for(i = 0;i <2;i++){
      if(pthread_tryjoin_np(tid[i],NULL) == 0){
        printf("Pthread : %d exit !\n",i+1);
        flag++; 
      }
    }
    if(flag >= 2) break;
  }
  return 0;
}

运行结果:

例程 8 展示了如何利用 pthread_cancel 函数主动的将某个线程结束。

27 行与 33 行创建了线程,将第一个线程的线程号传参形式传入了第二个线程。第一个的线程执行死循环睡眠逻辑,理论上除非进程结束,其永远不会结束,但在第二个线程中调用了 pthread_cancel 函数,相当于向该线程发送一个退出的指令,导致线程被退出,最终资源被非阻塞回收掉。


此例程要注意第 32 行的 sleep 函数,一定要确保线程 1 先执行,因线程是无序执行,故加入该睡眠函数控制顺序

相关文章
|
7天前
|
安全 Java 数据处理
Java并发编程:解锁多线程的潜力
在数字化时代的浪潮中,Java作为一门广泛使用的编程语言,其并发编程能力是提升应用性能和响应速度的关键。本文将带你深入理解Java并发编程的核心概念,探索如何通过多线程技术有效利用计算资源,并实现高效的数据处理。我们将从基础出发,逐步揭开高效并发编程的面纱,让你的程序运行得更快、更稳、更强。
|
14天前
|
算法 Unix Linux
linux线程调度策略
linux线程调度策略
26 0
|
19小时前
|
Linux 程序员 开发者
源社区的兴起:从“代码隐士”到Linux引领的“全球编程嘉年华”
在编程的古老森林中,曾有“代码隐士”默默耕耘,惧怕智慧外泄。直到“开源”春风拂过,源社区如全球编程嘉年华盛开!开源文化颠覆了“独门秘籍”的传统,像“武林秘籍共享”般在网络上公开,鼓励知识传播与智慧碰撞。程序员组队开发,分享代码,提升科技实力。Linux则从“首席大厨”变身为“总导演”,以强大内核调制出诱人应用,引领潮流并推动技术创新。加入这场没有血腥厮杀,只有知识盛宴的“编程版《饥饿游戏》”吧!与全球开发者共享编程的乐趣与成就感!别忘了带上你的“独门秘籍”,可能下一个改变世界的创意就在其中!
12 5
|
6天前
|
安全 测试技术 调度
iOS开发-多线程编程
【8月更文挑战第12天】在iOS开发中,属性的内存管理至关重要,直接影响应用性能与稳定性。主要策略包括:`strong`(强引用),保持对象不被释放;`weak`(弱引用),不保持对象,有助于避免循环引用;`assign`(赋值),适用于基本数据类型及非指针对象类型;`copy`(复制),复制对象而非引用,确保不变性。内存管理基于引用计数,利用自动引用计数(ARC)自动管理对象生命周期。此外,需注意避免循环引用,特别是在block中。最佳实践包括理解各策略、避免不必要的强引用、及时释放不再使用的对象、注意block中的内存管理,并使用工具进行内存分析。正确管理内存能显著提升应用质量。
|
15天前
|
监控 Shell Linux
探索Linux操作系统下的Shell编程之魅力
【8月更文挑战第4天】本文旨在通过一系列精心设计的示例和分析,揭示在Linux环境下进行Shell编程的独特之处及其强大功能。我们将从基础语法入手,逐步深入到脚本的编写与执行,最终通过实际代码案例展现Shell编程在日常系统管理和自动化任务中的应用价值。文章不仅适合初学者构建扎实的基础,同时也为有一定经验的开发者提供进阶技巧。
28 11
|
15天前
|
Ubuntu Linux 开发工具
深入探索Linux内核模块编程
【8月更文挑战第4天】在这篇文章中,我们不仅将探讨Linux内核模块的基础知识,还将通过一个实际的例子来展示如何编写一个简单的内核模块。我们将从理论出发,逐步过渡到动手实践,最终实现一个可以在Linux系统上运行的模块。文章的目标是为读者提供足够的信息和知识,以便他们能够自己编写内核模块,从而对操作系统的内部工作原理有更深入的了解。
|
9天前
|
Java 调度 开发者
Java并发编程:解锁多线程同步的奥秘
在Java的世界里,并发编程是提升应用性能的关键所在。本文将深入浅出地探讨Java中的并发工具和同步机制,带领读者从基础到进阶,逐步掌握多线程编程的核心技巧。通过实例演示,我们将一起探索如何在多线程环境下保持数据的一致性,以及如何有效利用线程池来管理资源。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你对Java并发编程有更深入的理解和应用。
|
14天前
|
缓存 Linux C语言
Linux线程是如何创建的
【8月更文挑战第5天】线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。
|
15天前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
37 1
|
20天前
|
安全 Java 程序员
Java 并发编程:解锁多线程同步的奥秘
【7月更文挑战第30天】在Java的世界里,并发编程是一块充满挑战的领域。它如同一位严苛的导师,要求我们深入理解其运作机制,才能驾驭多线程的力量。本文将带你探索Java并发编程的核心概念,包括线程同步与通信、锁机制、以及并发集合的使用。我们将通过实例代码,揭示如何在多线程环境中保持数据的一致性和完整性,确保你的应用程序既高效又稳定。准备好了吗?让我们一同踏上这段解锁Java并发之谜的旅程。
26 5