Linux下的C编程实战(四)――“线程”控制与“线程”通信编程

简介:


Linux下的C编程实战(四)

――“线程”控制与“线程”通信编程
1.Linux “线程”
        笔者曾经在《基于嵌入式操作系统 VxWorks 的多任务并发程序设计》(《软件报》 2006 年第 5~12 期)中详细叙述了进程和线程的区别,并曾经说明 Linux 是一种“多进程单线程”的操作系统。 Linux 本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。大家知道,进程是资源分配的单位,同一进程中的多个线程共享该进程的资源(如作为共享内存的全局变量)。 Linux 中所谓的“线程”只是在被创建的时候“克隆” (clone) 了父进程的资源,因此, clone 出来的进程表现为“线程”,这一点一定要弄清楚。因此, Linux “线程”这个概念只有在打冒号的情况下才是最准确的,可惜的是几乎没有书籍留心去强调这一点。
       Linux 内核只提供了轻量进程的支持,未实现线程模型,但 Linux 尽最大努力优化了进程的调度开销,这在一定程度上弥补无线程的缺陷。 Linux 用一个核心进程(轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成。
目前 Linux 中最流行的线程机制为 LinuxThreads ,所采用的就是线程-进程“一对一”模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。 LinuxThreads Xavier Leroy ([email]Xavier.Leroy@inria.fr[/email]) 负责开发完成,并已绑定在 GLIBC 中发行,它实现了一种 BiCapitalized 面向 Linux Posix 1003.1c  pthread ”标准接口。 Linuxthread 可以支持 Intel Alpha MIPS 等平台上的多处理器系统。
按照 POSIX 1003.1c  标准编写的程序与 Linuxthread  库相链接即可支持 Linux 平台上的多线程,在程序中需包含头文件 pthread. h ,在编译链接时使用命令:
gcc -D -REENTRANT -lpthread xxx. c
其中 -REENTRANT 宏使得相关库函数 ( stdio.h errno.h 中函数 是可重入的、线程安全的 (thread-safe) -lpthread 则意味着链接库目录下的 libpthread.a libpthread.so 文件。使用 Linuxthread 库需要 2.0 以上版本的 Linux 内核及相应版本的 C (libc 5.2.18 libc 5.4.12 libc 6)
2. “线程”控制
线程创建
进程被创建时,系统会为其创建一个主线程,而要在进程中创建新的线程,则可以调用 pthread_create
pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
  (start_routine)(void*), void *arg);
start_routine 为新线程的入口函数, arg 为传递给 start_routine 的参数。
每个线程都有自己的线程 ID ,以便在进程内区分。线程 ID pthread_create 调用时回返给创建线程的调用者;一个线程也可以在创建后使用 pthread_self() 调用获取自己的线程 ID
pthread_self (void) ;
线程退出
线程的退出方式有三:
1 )执行完成后隐式退出;
2 )由线程本身显示调用 pthread_exit  函数退出;
pthread_exit (void * retval) ;
3 )被其他线程用 pthread_cance 函数终止:
pthread_cance (pthread_t thread) ;
在某线程中调用此函数,可以终止由参数 thread  指定的线程。
如果一个线程要等待另一个线程的终止,可以使用 pthread_join 函数,该函数的作用是调用 pthread_join 的线程将被挂起直到线程 ID 为参数 thread 的线程终止:
pthread_join (pthread_t thread, void** threadreturn);
3. 线程通信
        线程互斥
互斥意味着“排它”,即两个线程不能同时进入被互斥保护的代码。 Linux 下可以通过 pthread_mutex_t  定义互斥体机制完成多线程的互斥操作,该机制的作用是对某个需要互斥的部分,在进入时先得到互斥体,如果没有得到互斥体,表明互斥部分被其它线程拥有,此时欲获取互斥体的线程阻塞,直到拥有该互斥体的线程完成互斥部分的操作为止。
下面的代码实现了对共享全局变量 用互斥体 mutex  进行保护的目的:
int x; //  进程中的全局变量
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 按缺省的属性初始化互斥体变量 mutex
pthread_mutex_lock(&mutex); //  给互斥体变量加锁
… // 对变量 的操作
phtread_mutex_unlock(&mutex); //  给互斥体变量解除锁
线程同步
同步就是线程等待某个事件的发生。只有当等待的事件发生线程才继续执行,否则线程挂起并放弃处理器。当多个线程协作时,相互作用的任务必须在一定的条件下同步。
Linux 下的 C 语言编程有多种线程同步机制,最典型的是条件变量 (condition variable) pthread_cond_init 用来创建一个条件变量,其函数原型为:
pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *attr);
pthread_cond_wait pthread_cond_timedwait 用来等待条件变量被设置,值得注意的是这两个等待调用需要一个已经上锁的互斥体 mutex ,这是为了防止在真正进入等待状态之前别的线程有可能设置该条件变量而产生竞争。 pthread_cond_wait 的函数原型为:
pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_broadcast 用于设置条件变量,即使得事件发生,这样等待该事件的线程将不再阻塞:
pthread_cond_broadcast (pthread_cond_t *cond) ;
pthread_cond_signal 则用于解除某一个等待线程的阻塞状态:
pthread_cond_signal (pthread_cond_t *cond) ;
pthread_cond_destroy  则用于释放一个条件变量的资源。
在头文件 semaphore.h  中定义的信号量则完成了互斥体和条件变量的封装,按照多线程程序设计中访问控制机制,控制对资源的同步访问,提供程序设计人员更方便的调用接口。
sem_init(sem_t *sem, int pshared, unsigned int val);
这个函数初始化一个信号量 sem  的值为 val ,参数 pshared  是共享属性控制,表明是否在进程间共享。
sem_wait(sem_t *sem);
调用该函数时,若 sem 为无状态,调用线程阻塞,等待信号量 sem 值增加 (post ) 成为有信号状态;若 sem 为有状态,调用线程顺序执行,但信号量的值减一。
sem_post(sem_t *sem);
调用该函数,信号量 sem 的值增加,可以从无信号状态变为有信号状态。
4. 实例
下面我们还是以著名的生产者 / 消费者问题为例来阐述 Linux 线程的控制和通信。一组生产者线程与一组消费者线程通过缓冲区发生联系。生产者线程将生产的产品送入缓冲区,消费者线程则从中取出产品。缓冲区有 个,是一个环形的缓冲池。
#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 16 //  缓冲区数量
struct prodcons
{
  //  缓冲区相关数据结构
  int buffer[BUFFER_SIZE]; /*  实际数据存放的数组 */
  pthread_mutex_t lock; /*  互斥体 lock  用于对缓冲区的互斥操作  */
  int readpos, writepos; /*  读写指针 */
  pthread_cond_t notempty; /*  缓冲区非空的条件变量  */
  pthread_cond_t notfull; /*  缓冲区未满的条件变量  */
};
/*  初始化缓冲区结构  */
void init(struct prodcons *b)
{
  pthread_mutex_init(&b->lock, NULL);
  pthread_cond_init(&b->notempty, NULL);
  pthread_cond_init(&b->notfull, NULL);
  b->readpos = 0;
  b->writepos = 0;
}
/*  将产品放入缓冲区 , 这里是存入一个整数 */
void put(struct prodcons *b, int data)
{
  pthread_mutex_lock(&b->lock);
  /*  等待缓冲区未满 */
  if ((b->writepos + 1) % BUFFER_SIZE == b->readpos)
  {
    pthread_cond_wait(&b->notfull, &b->lock);
  }
  /*  写数据 , 并移动指针  */
  b->buffer[b->writepos] = data;
  b->writepos++;
  if (b->writepos >  = BUFFER_SIZE)
    b->writepos = 0;
  /*  设置缓冲区非空的条件变量 */
  pthread_cond_signal(&b->notempty);
  pthread_mutex_unlock(&b->lock);
}
 
/*  从缓冲区中取出整数 */
int get(struct prodcons *b)
{
  int data;
  pthread_mutex_lock(&b->lock);
  /*  等待缓冲区非空 */
  if (b->writepos == b->readpos)
  {
    pthread_cond_wait(&b->notempty, &b->lock);
  }
  /*  读数据 , 移动读指针 */
  data = b->buffer[b->readpos];
  b->readpos++;
  if (b->readpos >  = BUFFER_SIZE)
    b->readpos = 0;
  /*  设置缓冲区未满的条件变量 */
  pthread_cond_signal(&b->notfull);
  pthread_mutex_unlock(&b->lock);
  return data;
}
 
/*  测试 : 生产者线程将 10000  的整数送入缓冲区 , 消费者线
程从缓冲区中获取整数 , 两者都打印信息 */
#define OVER ( - 1)
struct prodcons buffer;
void *producer(void *data)
{
  int n;
  for (n = 0; n < 10000; n++)
  {
    printf("%d --->\n", n);
    put(&buffer, n);
  } put(&buffer, OVER);
  return NULL;
}
 
void *consumer(void *data)
{
  int d;
  while (1)
  {
    d = get(&buffer);
    if (d == OVER)
      break;
    printf("--->%d \n", d);
  }
  return NULL;
}
 
int main(void)
{
  pthread_t th_a, th_b;
  void *retval;
  init(&buffer);
  /*  创建生产者和消费者线程 */
  pthread_create(&th_a, NULL, producer, 0);
  pthread_create(&th_b, NULL, consumer, 0);
  /*  等待两个线程结束 */
  pthread_join(th_a, &retval);
  pthread_join(th_b, &retval);
  return 0;
}
5.WIN32 VxWorks Linux 线程类比
目前为止,笔者已经创作了《基于嵌入式操作系统 VxWorks 的多任务并发程序设计》(《软件报》 2006 5~12 期连载)、《深入浅出 Win32 多线程程序设计》(天极网技术专题)系列,我们来找出这两个系列文章与本文的共通点。
看待技术问题要瞄准其本质,不管是 Linux VxWorks 还是 WIN32 ,其涉及到多线程的部分都是那些内容,无非就是线程控制和线程通信,它们的许多函数只是名称不同,其实质含义是等价的,下面我们来列个三大操作系统共同点详细表单:
事项
WIN32
VxWorks
Linux
线程创建
CreateThread
taskSpawn
pthread_create
线程终止
执行完成后退出;线程自身调用 ExitThread  函数即终止自己;被其他线程调用函数 TerminateThread 函数
执行完成后退出;由线程本身调用 exit 退出;被其他线程调用函数 taskDelete 终止
执行完成后退出;由线程本身调用 pthread_exit  退出;被其他线程调用函数 pthread_cance 终止
获取线程 ID
GetCurrentThreadId
taskIdSelf
pthread_self
创建互斥
CreateMutex
semMCreate
pthread_mutex_init
获取互斥
WaitForSingleObject
WaitForMultipleObjects
semTake
pthread_mutex_lock
释放互斥
ReleaseMutex
semGive
phtread_mutex_unlock
创建信号量
CreateSemaphore
semBCreate semCCreate
sem_init
等待信号量
WaitForSingleObject
semTake
sem_wait
释放信号量
ReleaseSemaphore
semGive
sem_post
6. 小结
        本章讲述了 Linux 下多线程的控制及线程间通信编程方法,给出了一个生产者 / 消费者的实例,并将 Linux 的多线程与 WIN32 VxWorks 多线程进行了类比,总结了一般规律。鉴于多线程编程已成为开发并发应用程序的主流方法,学好本章的意义也便不言自明。


 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120039,如需转载请自行联系原作者



目录
打赏
0
0
0
0
348
分享
相关文章
Linux 手动安装快速部署 LNMP 环境实战
本文详细记录了在阿里云ECS上手动搭建LNMP环境的过程,系统选用Ubuntu 24.04。主要内容包括:1) 使用`apt`安装Nginx和MySQL,并更新软件源;2) 编译安装PHP 8.4.5,配置PHP-FPM及环境路径;3) 配置MySQL root用户密码;4) 调整Nginx支持PHP解析并测试整体环境。通过此过程,重现手动配置服务器的细节,帮助熟悉各组件的安装与协同工作。
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
147 19
Linux中的System V通信标准--共享内存、消息队列以及信号量
希望本文能帮助您更好地理解和应用System V IPC机制,构建高效的Linux应用程序。
143 48
如何在阿里云的linux上搭建Node.js编程环境?
本指南介绍如何在阿里云Linux服务器(Ubuntu/CentOS)上搭建Node.js环境,包含两种安装方式:包管理器快速安装和NVM多版本管理。同时覆盖全局npm工具配置、应用部署示例(如Express服务)、PM2持久化运行、阿里云安全组设置及外部访问验证等步骤,助你完成开发与生产环境的搭建。
|
2月前
|
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
59 17
|
2月前
|
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
64 26
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
72 16
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
169 13
|
4月前
|
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
344 2
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等