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

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

编译

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

执行结果如下:

[cpp] view
plain
 copy

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

我们可以看到,数据是不断被打印的,说明 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. char buf[60];
  7. sem_t sem;
  8. void *function(void *arg);
  9. int main(int argc, char *argv[])
  10. {
  11. pthread_t a_thread;
  12. void *thread_result;
  13. if(sem_init(&sem,0,0) != 0)
  14. {
  15. perror("fail to sem_init");
  16. exit(-1);
  17. }
  18. if(pthread_create(&a_thread,NULL,function,NULL) != 0)
  19. {
  20. perror("fail to pthread_create");
  21. exit(-1);
  22. }
  23. printf("input 'quit' to exit\n");
  24. do
  25. {
  26. fgets(buf,60,stdin);
  27. sem_post(&sem);
  28. }
  29. while(strncmp(buf,"quit",4) != 0);
  30. return 0;
  31. }
  32. void *function(void *arg)
  33. {
  34. while(1)
  35. {
  36. sem_wait(&sem);
  37. printf("you enter %d characters\n",strlen(buf) - 1);
  38. }
  39. }

执行结果如下:

[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$

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

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

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

    一.Linux下多任务机制的介绍 Linux有一特性是多任务,多任务处理是指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务. 多任务操作系统使用某种调度(shedule)策 ...

  2. Linux系统常用升级的基础包

    Linux系统常用升级的基础包 yum -y install lrzsz gcc gcc-c++ make flex autoconf automake vixie-cron libjpeg libj ...

  3. java并发编程 线程基础

    java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...

  4. Linux系统shell编程自学_第一章基础

    第一章 基础shell的优势在于处理操作系统底层的业务,Python,php的优势在于开发运维工具,web界面的管理工具以及web业务开发.处理一键安装.优化.报警脚本shell又叫命令解释器,它能识 ...

  5. Linux系统文件系统及文件基础篇

    学习Linux,重难点在于掌握不同类别的文件系统及其作用.通过对Linux系统的安装,我们首先来了解下Linux系统里各个目录文件夹下的大致功能:主要的目录树的有/./root./home./usr. ...

  6. linux系统串口编程实例

    在嵌入式开发中一些设备如WiFi.蓝牙......都会通过串口进行主机与从机间通信,串口一般以每次1bit位进行传输,效率相对慢. 在linux系统下串口的编程有如下几个步骤,最主要的是串口初始化! ...

  7. windows核心编程 - 线程基础

    一.基本概念: 一个进程至少需要一个线程. 组成:一个线程包括仅包括一个线程堆栈和一个线程内核对象 线程堆栈:用于维护线程在执行代码时需要的所有函数参数和局部变量 线程内核对象:操作系统用它来对线程实 ...

  8. linux makefile: c++ 编程_基础入门_如何开始?

    学习android 终究还是需要研究一下其底层框架,所以,学习c++很有必要. 这篇博客,算是linux(ubuntu) 下学习 c++ 的一个入门. 刚开始学习编程语言的时候,最好还是使用命令行操作 ...

  9. 《linux系统及其编程》实验课记录(五)

    实验 5:权限的设置和更改 实验环境: 安装了 Red Hat Enterprise Linux 6.0 可运行系统,并且是成功验证系统.有另外一个无特权用户 student,密码 student 的 ...

随机推荐

  1. 二维码扫描&集合排序

    一.二维码扫描机制 二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的:在代码编制上巧妙地利用构 ...

  2. Android View框架总结(八)ViewGroup事件分发机制

    请尊重分享成果,转载请注明出处: http://blog.csdn.net/hejjunlin/article/details/52298780 上篇分析了View的事件分发流程,留了一个问题:如果上 ...

  3. Java虚拟机定义

    虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机有自己完善的硬体架构,如处理器.堆栈.寄存器等,还具有相应的指令系统.JVM屏蔽了与具体操作系统平台相关的 ...

  4. 【java集合框架源码剖析系列】java源码剖析之TreeSet

    本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...

  5. C语言--指针函数和函数指针

    指针函数和函数指针 指针函数其实是一个简称,是指带指针的函数,它本质上是一个函数,只是返回的是某种类型的指针.其定义的格式为: 类型标识符 *函数名(参数表)  函数指针,从本质上说是一个指针,只是它 ...

  6. 《java入门第一季》之TreeSet存储自定义对象并保证排序和唯一

    上一篇用一个简单的例子,介绍了treeset集合存储的内部过程,这里再完善其存储自定义对象保证唯一. 需求:A:  * 自然排序,按照年龄从小到大排序  *         B:  * 成员变量值都相 ...

  7. HTTP 消息结构

    HTTP 消息结构 HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议. 一个HTTP"客户端"是一个应用程序(Web浏览 ...

  8. Oracle WorkFlow(工作流)(二)

    2.4消息(Message) 消息主要是为通知服务的,可以把消息当作通知的内容和类型.消息也属于一个单据类型,通知只能和同一个单据类型里的消息相关联. 每个消息可以有一个或多个属性和自己相联系,消息的 ...

  9. Gradle 1.12翻译——第十九章. Gradle 守护进程

    有关其他已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或访问:http://gradledoc.qiniudn.com ...

  10. JAVA对象克隆可能会出现的问题

    首先,区分一下拷贝和克隆: 拷贝:当拷贝一个变量时,原始变量与拷贝变量引用的是同一个对象.当改变一个变量所引用的对象,则会对另一个变量造成影响. 克隆:当克隆一个对象时,是重新的创建了和该对象内容相同 ...