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

时间:2021-09-14 15:55:27

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

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$

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