Linux 系统应用编程——线程基础

 传统多任务操作系统中一个可以独立调度的任务(或称之为顺序执行流)是一个进程。每个程序加载到内存后只可以唯一地对应创建一个顺序执行流,即传统意义的进程。每个进程的全部系统资源是私有的,如虚拟地址空间,文件描述符和信号处理等等。使用多进程实现多任务应用时存在如下问题:

1)任务切换,即进程间上下文切换,系统开销比较大。(虚拟地址空间以及task_struct 都需要切换)

2)多任务之间的协作比较麻烦,涉及进程间通讯。(因为不同的进程工作在不同的地址空间)

所以,为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程

 

一、线程基础

      通常线程指的是共享相同地址空间的多个任务。线程最大的特点就是在同一个进程中创建的线程共享该进程的地址空间;但一个线程仍用task_struct 来描述,线程和进程都参与统一的调度。所以,多线程的好处便体现出来:

1)大大提高了任务切换的效率;因为各线程共享进程的地址空间,任务切换时只要切换task_struct 即可;

2)线程间通信比较方便;因为在同一块地址空间,数据共享;

当然,共享地址空间也会成为线程的缺点,因为共享地址空间,如果其中一个线程出现错误(比如段错误),整个线程组都会崩掉!

     Linux之所以称呼其线程为LWP( Light Weight Process ),因为从内核实现的角度来说,它并没有为线程单独创建一个结构,而是继承了很多进程的设计:

1)继承了进程的结构体定义task_struct ;

2)没有专门定义线程ID,复用了PID;

3)更没有为线程定义特别的调度算法,而是沿用了原来对task_struct 的调度算法。

最新的Linux内核里线程已经替代原来的进程称为调度的实际最小单位

原来的进程概念可以看成是多个线程的容器,称之为线程组;即一个进程就是所有相关的线程构成的一个线程组。传统的进程等价于单线程进程

每个线程组都有自己的标识符 tgid (数据类型为 pid_t ),其值等于该进程(线程组)中的第一个线程(group_leader)的PID。

 

1、创建线程

 pthread_create()函数描述如下:

所需头文件 #include <pthread.h>
函数原型
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,

                               void *(* routine)(void *), void *arg)

函数参数
thread :创建的线程

attr :指定线程的属性,NULL表示使用缺省属性

routine :线程执行的函数

arg :传递给线程执行的函数的参数

函数返回值
成功: 0

出错: -1

1)这里routine 是回调函数(callback),其函数类型由内核来决定,这里我们将其地址传给内核;这个函数并不是线程创建了就会执行,而是只有当其被调度到cpu上时才会被执行;具体回调函数的讲解,移步Linux
C 函数指针应用---回调函数 .

2)arg 是线程执行函数的参数,这里我们将其地址穿进去,使用时需要先进行类型转换,才能使用;如果参数不止一个,我们可以将其放入到结构体中;

 

2、pthread_join () 函数

其函数描述如下:

所需头文件 #include <pthread.h>
函数原型 int thread_join(pthread_t thread, void  ** value_ptr)
函数参数
thread :要等待的线程

value_ptr :指针 *value_ptr 指向线程返回的参数

函数返回值
成功: 0

出错: -1

这里,我们可以看到 value_ptr 是个二级指针,其是出参,存放的是线程返回参数的地址;

 

3、pthread_exit 函数

其函数描述如下:

所需头文件 #include <pthread.h>
函数原型 int pthread_exit(void *value_ptr)
函数参数 value_ptr :线程退出时返回的值
函数返回值
成功:0

出错:-1

和进程中的exit() 、wait()一样,这里pthread_join 与 pthread_exit 是工作在两个线程之中;

 

下面看一个实例:

[cpp] view
plain
 copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. #include <pthread.h>  
  5.   
  6. char message[32] = "Hello World!";  
  7. void *thread_function(void *arg);  
  8.   
  9. int main()  
  10. {  
  11.     pthread_t a_thread;  
  12.     void *thread_result;  
  13.   
  14.     if(pthread_create(&a_thread,NULL,thread_function,(void *)message) < 0)  
  15.     {  
  16.         perror("fail to pthread_create");  
  17.         exit(-1);  
  18.     }  
  19.   
  20.     printf("waiting for thread to finish\n");  
  21.     if(pthread_join(a_thread,&thread_result) < 0)  
  22.     {  
  23.         perror("fail to pthread_join");  
  24.         exit(-1);  
  25.     }  
  26.   
  27.     printf("Message is now %s\n",message);  
  28.     printf("thread_result is %s\n",(char *)thread_result);  
  29.     return 0;  
  30. }  
  31.   
  32. void *thread_function(void *arg)  
  33. {  
  34.     printf("thread_function is running,argument is %s\n",(char *)arg);  
  35.     strcpy(message,"marked by thread");  
  36.     pthread_exit("Thank you for the cpu time");  
  37. }  

编译

[cpp] view
plain
 copy

  1. fs@ubuntu:~/qiang/thread/0107$ gcc -o thread thread.c -lpthread  
  2. fs@ubuntu:~/qiang/thread/0107$   

线程通过第三方的线程库来实现,所以这里要 -lpthread ,-l 是链接一个库,这个库是pthread;

执行结果如下:

[cpp] view
plain
 copy

  1. fs@ubuntu:~/qiang/thread/0107$ ./thread   
  2. waiting for thread to finish  
  3. thread_function is running,argument is Hello World!  
  4. Message is now marked by thread  
  5. thread_result is Thank you for the cpu time  
  6. fs@ubuntu:~/qiang/thread/0107$   

从这个程序,我们可以看到线程之间是如何通信的,线程之间通过二级指针来传送参数的地址(这是进程所不具备的,因为他们的地址空间独立),但两个线程之间的通信,传递的数据的生命周期必须是静态的。可以使全局变量、static修饰的数据、堆里面的数据;这个程序中的message就是一个全局变量。其中一个线程可以修改它,另一个线程得到它修改过后的message。

 

二、线程的同步和互斥

先来了解同步和互斥的基本概念:

临界资源:某些资源来说,其在同一时间只能被一段机器指令序列所占用。这些一次只能被一段指令序列所占用的资源就是所谓的临界资源。

临界区:对于临界资源的访问,必须是互斥进行。也就是当临界资源被一个指令序列占用时,另一个需要访问相同临界资源的指令序列就不能被执行。指令序列不能执行的实际意思就是其所在的进程/线程会被阻塞。所以我们定义程序内访问临界资源的代码序列被称为临界区。

互斥:是指同事只允许一个访问者对临界资源进行访问,具有唯一性排它性。但互斥无法限制访问这个对资源的访问顺序,即访问时无序的。

同步:是指在互斥的基础上,通过其他机制实现访问者对资源的有序访问。

 

1、线程间互斥

引入互斥(mutual   exlusion)锁的目的是用来保证共享数据的完整性。

互斥锁主要用来保护临界资源。每个临界资源都有一个互斥锁来保护,任何时刻最多只能有一个线程能访问该资源;线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止;

通常,我们在临界区前上锁,临界区后解锁

1)初始化互斥锁函数

所需头文件 #include <pthread.h>
函数原型
int pthread_mutex_init (pthread_mutex_t  *mutex,  pthread_mutexattr_t  *attr )

//初始化互斥锁

函数参数
mutex:互斥锁

attr :互斥锁属性 // NULL表示缺省属性

函数返回值
成功:0

出错:-1

2)申请互斥锁函数

所需头文件 #include <pthread.h>
函数原型
int pthread_mutex_lock(pthread_mutex_t *mutex)

//申请互斥锁

函数参数
mutex:互斥锁

函数返回值
成功:0

出错:-1

3)释放互斥锁函数

所需头文件 #include <pthread.h>
函数原型
int pthread_mutex_unlock(pthread_mutex_t *mutex)

//释放互斥锁

函数参数
mutex:互斥锁

函数返回值
成功:0

出错:-1

下面是一个实例:

[cpp] view
plain
 copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. #include <pthread.h>  
  5. #include <unistd.h>  
  6. //#define _LOCK_  
  7.   
  8. unsigned int value1,value2,count;  
  9. pthread_mutex_t mutex;  
  10. void *function(void *arg);  
  11.   
  12. int main()  
  13. {  
  14.     pthread_t a_thread;  
  15.   
  16.     if(pthread_mutex_init(&mutex,NULL) < 0)  
  17.     {  
  18.         perror("fail to mutex_init");  
  19.         exit(-1);  
  20.     }  
  21.   
  22.     if(pthread_create(&a_thread,NULL,function,NULL) != 0)  
  23.     {  
  24.         perror("fail to pthread_create");  
  25.         exit(-1);  
  26.     }  
  27.   
  28.     while(1)  
  29.     {  
  30.         count++;  
  31. #ifdef _LOCK_  
  32.         pthread_mutex_lock(&mutex);  
  33. #endif  
  34.         value1 = count;  
  35.         value2 = count;  
  36. #ifdef _LOCK_  
  37.         pthread_mutex_unlock(&mutex);  
  38. #endif  
  39.     }  
  40.     return 0;  
  41. }  
  42.   
  43. void *function(void *arg)  
  44. {  
  45.     while(1)  
  46.     {  
  47. #ifdef _LOCK_  
  48.         pthread_mutex_lock(&mutex);  
  49. #endif  
  50.         if(value1 != value2)  
  51.         {  
  52.             printf("count = %d,value1 = %d,value2 = %d\n",count,value1,value2);  
  53.             usleep(100000);  
  54.         }  
  55. #ifdef _LOCK_  
  56.         pthread_mutex_unlock(&mutex);  
  57. #endif  
  58.     }  
  59.     return NULL;  
  60. }  

执行结果如下:

[cpp] view
plain
 copy

  1. fs@ubuntu:~/qiang/thread/0107$ ./mutex  
  2.   
  3. count = 3368408,value1 = 3368408,value2 = 3368407  
  4. count = 44174760,value1 = 44174760,value2 = 44174759  
  5. count = 69313865,value1 = 69313865,value2 = 69313864  
  6. count = 139035309,value1 = 139035309,value2 = 139035308  
  7. count = 168803956,value1 = 168803956,value2 = 168803955  
  8. count = 192992611,value1 = 192992611,value2 = 192992610  
  9. count = 224279903,value1 = 224279903,value2 = 224279902  
  10. count = 259586793,value1 = 259586793,value2 = 259586792  
  11. count = 282057307,value1 = 282057307,value2 = 282057306  
  12. count = 321607823,value1 = 321607823,value2 = 321607822  
  13. count = 351629940,value1 = 351629940,value2 = 351629939  
  14. count = 374130545,value1 = 374130545,value2 = 374130544  
  15. count = 400727525,value1 = 400727525,value2 = 400727524  
  16. count = 440219988,value1 = 440219988,value2 = 440219987  
  17. count = 466069865,value1 = 466069865,value2 = 466069864  
  18. count = 500581241,value1 = 500581241,value2 = 500581240  
  19. count = 522649671,value1 = 522649671,value2 = 522649670  
  20. count = 569234325,value1 = 569234325,value2 = 569234324  
  21. count = 608139152,value1 = 608139152,value2 = 608139151  
  22. count = 639493957,value1 = 639493957,value2 = 639493956  
  23. .....  

我们可以看到,数据是不断被打印的,说明 a 线程是可以访问临界资源的。

我们把#define  _LOCK_前面的注释去掉,这时就加上了互斥锁,执行结果如下:

[cpp] view
plain
 copy

  1. fs@ubuntu:~/qiang/thread/0107$ ./mutex  

此时,并没有数据被打印,说明此时a线程中 value1 与 value 2 一直是相等的,说明主线程执行是,a线程并无法访问临界资源的。

2、线程间同步

同步(synchronization) 指的是多个任务(线程)按照约定的顺序相互配合完成一件事情;

线程间同步——P / V 操作

信号量代表某一类资源,其值表示系统中该资源当前可用的数量。

信号量是一个受保护的变量,只能通过三种操作来访问:

1)初始化

2)P操作(申请资源)

3)V操作(释放资源)P(S)含义如下:

[cpp] view
plain
 copy

  1. if (信号量的值大于0)  
  2. {  
  3.     请资源的任务继续运行;  
  4.     信号量的值 减一;  
  5. }  
  6. else  
  7. {  
  8.     请资源的任务阻塞;  
  9. }  

V(S)含义如下:

[cpp] view
plain
 copy

  1. if (没有任务在等待该资源)  
  2. {  
  3.     信号量的值 加一;  
  4. }  
  5. else  
  6. {  
  7.     唤醒第一个等待的任务,让其继续运行;  
  8. }  

1)、信号量初始化函数:

所需头文件 #include <semaphore.h>
函数原型
int sem_int (sem_t *sem,int pshared,unsigned int value)

//初始化信号量

函数参数
sem:初始化的信号量

pshared:信号量共享的范围(0:线程间使用 非0 :进程间使用)

value :信号量初值

函数返回值
成功:0

出错:-1

2)P操作

所需头文件 #include <semaphore.h>
函数原型
int sem_wait (sem_t *sem) //P操作

函数参数
sem:信号量

函数返回值
成功:0

出错:-1

3)V操作

所需头文件 #include <semaphore.h>
函数原型
int sem_post(sem_t *sem) //V操作

函数参数
sem:信号量

函数返回值
成功:0

出错:-1

下面是个实例:

[cpp] view
plain
 copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. #include <pthread.h>  
  5. #include <semaphore.h>  
  6.   
  7. char buf[60];  
  8. sem_t sem;  
  9. void *function(void *arg);  
  10.   
  11. int main(int argc, char *argv[])  
  12. {  
  13.     pthread_t a_thread;  
  14.     void *thread_result;  
  15.   
  16.     if(sem_init(&sem,0,0) != 0)  
  17.     {  
  18.         perror("fail to sem_init");  
  19.         exit(-1);  
  20.     }  
  21.   
  22.     if(pthread_create(&a_thread,NULL,function,NULL) != 0)  
  23.     {  
  24.         perror("fail to pthread_create");  
  25.         exit(-1);  
  26.     }  
  27.   
  28.     printf("input 'quit' to exit\n");  
  29.     do    
  30.     {  
  31.         fgets(buf,60,stdin);  
  32.         sem_post(&sem);  
  33.     }  
  34.     while(strncmp(buf,"quit",4) != 0);  
  35.       
  36.     return 0;  
  37. }  
  38.   
  39. void *function(void *arg)  
  40. {  
  41.     while(1)  
  42.     {  
  43.         sem_wait(&sem);  
  44.         printf("you enter %d characters\n",strlen(buf) - 1);  
  45.     }  
  46. }  

执行结果如下:

[cpp] view
plain
 copy

  1. fs@ubuntu:~/qiang/thread/0107$ ./sem   
  2. input 'quit' to exit  
  3. xiao  
  4. you enter 4 characters  
  5. zhi  
  6. you enter 3 characters  
  7. qiang  
  8. you enter 5 characters  
  9. quit  
  10. fs@ubuntu:~/qiang/thread/0107$   

我们可以看到两个线程是同步的。

时间: 2024-10-26 10:50:22

Linux 系统应用编程——线程基础的相关文章

Linux 系统应用编程——进程基础

一.Linux下多任务机制的介绍          Linux有一特性是多任务,多任务处理是指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务.          多任务操作系统使用某种调度(shedule)策略(由内核来执行)支持多个任务并发执行.事实上,(单核)处理器在某一时刻只能执行一个任务.每个任务创建时被分配时间片(几十到上百毫秒),任务执行(占用CPU)时,时间片递减.操作系统会在当前任务的时间片用完时调度执行其他任务.由于任务会频繁地切换执行,因此给用户多

Linux 系统应用编程——标准I/O

标准I/O的由来         标准I/O指的是ANSI C 中定义的用于I/O操作的一系列函数.         只要操作系统安装了C库,标准I/O函数就可以调用.换句话说,如果程序中使用的是标准I/O函数,那么源代码不需要任何修改就可以在其他操作系统下编译运行,具有更好的可移植性.         除此之外,使用标准I/O可以减少系统调用的次数,提高系统效率.标准I/O函数在执行时也会用到系统调用.在执行系统调用时,Linux必须从用户态切换到内核态,处理相应的请求,然后再返回到用户态.如

gcc用于linux系统下编程的编译器选项说明

GC++(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器.它是一套&http://www.aliyun.com/zixun/aggregation/37954.html">nbsp; GNU编译器套装以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU计划的关键部分,亦是自由的类Unix及苹果电脑 Mac OS X 操作系统的标准编译器. GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言.GCC

Linux下多线程编程遇到的一些问题

今天在学习了Linux的多线程编程的基础的知识点.于是就试着做了一个简单的Demo.本以为会得到预期的结果.不成想却遇到了意想不到的问题. 代码展示 我的C 代码很简单,就是一个简单的示例程序,如下: #include <stdio.h> #include <stdlib.h> #include<pthread.h> int sum ; void* runner(void *param); int main( int argc, char*argv[]) { pthre

总结六条对我们学习Linux系统有用的忠告

接触linux需要的是端正自己的态度,这个玩意可不是一天两天就能拿得下的.学习个基础,能装系统.能装常见服务.能编译.能配置存储空间.能配置系统参数.能简单查看系统负载等基本够用.但这些只保证能做机房运维,真正和进阶的运维工作不在机房,真正的运维工作也不仅仅只是Linux.Linux只是基于Linux系统运行环境的基础知识,衡量一个好的Linux系统下运维工程师也不一定非得用Linux知识的深浅,当然Linux钻研得越深越好. 还要看工作内容,就拿我来说作为一个机房运维维护人员,机房运维分很多种

GCC 4.6.1发布 linux系统下的编译器

GCC是一个用于linux系统下编程的编译器.是一套由 GNU 开发的编程语言编译器.它是一套  GNU编译器套装以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU计划的关键部分,亦是自由的类Unix及苹果电脑 Mac OS X 操作系统的标准编译器. GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言.GCC 很快地扩展,变得可处理 C++.之后也变得可处理 Fortran.Pascal.Objective-C.Java, 以及 Ada与其他语言. GCC 4.6.

Linux系统中bash shell编程的10个基础问题讲解_linux shell

第1问:为何叫做shell?在介绍 shell 是什么东西之前,不妨让我们重新审视使用者与电脑的关系.我们知道电脑的运作不能离开硬件,但使用者却无法直接对硬件作驱动,硬件的驱动只能透过一个称为"操作系统(Operating System)"的软件来控管,事实上,我们每天所谈的linux,严格来说只是一个操作系统,我们称之为"核心(kernel)".然而,从使用者的角度来说,使用者也没办法直接操作kernel,而是透过kernel的"外壳"程序,也

linux系统编程基础(一) 计算机体系结构一点基础知识

无论是在CPU外部接总线的设备还是在CPU内部接总线的设备都有各自的地址范围,都可以像访问内存一样访问,很多体系结构(比如ARM)采用这种方式操作设备,称为内存映射I/O(Memory-mappedI/O).但是x86比较特殊,x86对于设备有独立的端口地址空间,CPU核需要引出额外的地址线来连接片内设备(和访问内存所用的地址线不同),访问设备寄存器时用特殊的in/out指令(汇编),而不是和访问内存用同样的指令,这种方式称为端口I/O(PortI/O). 在x86平台上,硬盘是挂在IDE.SA

《Linux系统编程(第2版)》——第1章 入门和基本概念 1.1 系统编程

第1章 入门和基本概念 摆在你面前的是一本关于系统编程的书,你将在本书中学习到编写系统软件的相关技术和技巧.系统软件运行在系统的底层,与内核和系统核心库进行交互.常见的系统软件包括Shell.文本编辑器.编译器.调试器.核心工具(GNU Core Utilities)以及系统守护进程.此外,网络服务.Web服务和数据库也属于系统软件的范畴.这些程序都是基于内核和C库实现的,可以称为"纯"系统软件.相对地,其他软件(如高级GUI应用),很少和底层直接交互.有些程序员一直在编写系统软件,而