(转载)Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)

介绍:什么是线程,线程的优点是什么

线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。 一个进程可以有很多线程,每条线程并行执行不同的任务。

线程可以提高应用程序在多核环境下处理诸如文件I/O或者socket I/O等会产生堵塞的情况的表现性能。在Unix系统中,一个进程包含很多东西,包括可执行程序以及一大堆的诸如文件描述符地址空间等资源。在很多情况下,完成相关任务的不同代码间需要交换数据。如果采用多进程的方式,那么通信就需要在用户空间和内核空间进行频繁的切换,开销很大。但是如果使用多线程的方式,因为可以使用共享的全局变量,所以线程间的通信(数据交换)变得非常高效。

Hello World(线程创建、结束、等待)

创建线程 pthread_create

线程创建函数包含四个变量,分别为: 1. 一个线程变量名,被创建线程的标识 2. 线程的属性指针,缺省为NULL即可 3. 被创建线程的程序代码 4. 程序代码的参数 For example: - pthread_t thrd1; - pthread_attr_t attr; - void thread_function(void argument); - char *some_argument;

pthread_create(&thrd1, NULL, (void *)&thread_function, (void *) &some_argument);

结束线程 pthread_exit

线程结束调用实例:pthread_exit(void *retval); //retval用于存放线程结束的退出状态

线程等待 pthread_join

pthread_create调用成功以后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似与多进程编程中的waitpid。 举个例子,以下假设 A 线程调用 pthread_join 试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。 该函数包含两个参数:

  • pthread_t th //th是要等待结束的线程的标识
  • void **thread_return //指针thread_return指向的位置存放的是终止线程的返回状态。

调用实例:pthread_join(thrd1, NULL);

example1:

 1 /*************************************************************************
 2     > File Name: thread_hello_world.c
 3     > Author: couldtt(fyby)
 4     > Mail:  [email protected]
 5     > Created Time: 2013年12月14日 星期六 11时48分50秒
 6  ************************************************************************/
 7
 8 #include <stdio.h>
 9 #include <stdlib.h>
10 #include <pthread.h>
11
12 void print_message_function (void *ptr);
13
14 int main()
15 {
16     int tmp1, tmp2;
17     void *retval;
18     pthread_t thread1, thread2;
19     char *message1 = "thread1";
20     char *message2 = "thread2";
21
22     int ret_thrd1, ret_thrd2;
23
24     ret_thrd1 = pthread_create(&thread1, NULL, (void *)&print_message_function, (void *) message1);
25     ret_thrd2 = pthread_create(&thread2, NULL, (void *)&print_message_function, (void *) message2);
26
27     // 线程创建成功,返回0,失败返回失败号
28     if (ret_thrd1 != 0) {
29         printf("线程1创建失败\n");
30     } else {
31         printf("线程1创建成功\n");
32     }
33
34     if (ret_thrd2 != 0) {
35         printf("线程2创建失败\n");
36     } else {
37         printf("线程2创建成功\n");
38     }
39
40     //同样,pthread_join的返回值成功为0
41     tmp1 = pthread_join(thread1, &retval);
42     printf("thread1 return value(retval) is %d\n", (int)retval);
43     printf("thread1 return value(tmp) is %d\n", tmp1);
44     if (tmp1 != 0) {
45         printf("cannot join with thread1\n");
46     }
47     printf("thread1 end\n");
48
49     tmp2 = pthread_join(thread1, &retval);
50     printf("thread2 return value(retval) is %d\n", (int)retval);
51     printf("thread2 return value(tmp) is %d\n", tmp1);
52     if (tmp2 != 0) {
53         printf("cannot join with thread2\n");
54     }
55     printf("thread2 end\n");
56
57 }
58
59 void print_message_function( void *ptr ) {
60     int i = 0;
61     for (i; i<5; i++) {
62         printf("%s:%d\n", (char *)ptr, i);
63     }
64 }

编译

gcc thread_hello_world.c -otest -lpthread 一定要加上-lpthread,要不然会报错,因为源代码里引用了pthread.h里的东西,所以在gcc进行链接的时候,必须要找到这些库的二进制实现代码。

运行结果

结果分析: 1.这段程序我运行了两次,可以看到,两次的运行结果是不一样的,从而说明,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度。 2.另外,我们看到,在thread2的join结果出现了错误,打印出cannot join with thread2其实这个是个小错误,因为,我pthread_join传进去的th是thread1,在上面的结果中,thread1早已经结束了,所以我们再次等待thread1结束肯定会出现无法取到状态的错误的。 3.pthread_join(thread1, &retval)确实等待了thread1的结束,我们看到,在print_message_function函数循环了5遍结束以后,才打印出thread1 end

这是一个非常简单的例子,hello world级别的,只是用来演示Linux下C多线程的使用,在实际应用中,由于多个线程往往会访问共享的资源(典型的是访问同一个全局变量),因此多个县城间存在着竞争的关系,这就需要对多个线程进行同步,对其访问的数据予以保护。

多线程的同步与互斥

方式一:锁

  • 在主线程中初始化锁为解锁状态

    • pthread_mutex_t mutex;
    • pthread_mutex_init(&mutex, NULL);
  • 在编译时初始化锁为解锁状态
    • 锁初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 访问对象时的加锁操作与解锁操作
    • 加锁 pthread_mutex_lock(&mutex)
    • 释放锁 pthread_mutex_unlock(&mutex)
不加锁,数据不同步

我们先来看一个不加锁,多个线程访问同一段数据的程序。

 1 /*************************************************************************
 2     > File Name: no_mutex.c
 3     > Author: couldtt(fyby)
 4     > Mail: [email protected]
 5     > Created Time: 2013年12月15日 星期日 17时52分24秒
 6  ************************************************************************/
 7
 8 #include <stdio.h>
 9 #include <stdlib.h>
10 #include <pthread.h>
11
12 int sharedi = 0;
13 void increse_num(void);
14
15 int main(){
16     int ret;
17     pthread_t thrd1, thrd2, thrd3;
18
19     ret = pthread_create(&thrd1, NULL, (void *)increse_num, NULL);
20     ret = pthread_create(&thrd2, NULL, (void *)increse_num, NULL);
21     ret = pthread_create(&thrd3, NULL, (void *)increse_num, NULL);
22
23     pthread_join(thrd1, NULL);
24     pthread_join(thrd2, NULL);
25     pthread_join(thrd3, NULL);
26
27     printf("sharedi = %d\n", sharedi);
28
29     return 0;
30
31 }
32
33 void increse_num(void) {
34     long i,tmp;
35     for(i=0; i<=100000; i++) {
36         tmp = sharedi;
37         tmp = tmp + 1;
38         sharedi = tmp;
39     }
40 }

编译

gcc no_mutex.c -onomutex -lpthread

运行分析

从上图可知,我们no_mutex每次的运行结果都不一致,而且,运行结果也不符合我们的预期,出现了错误的结果。 原因就是三个线程竞争访问全局变量sharedi,并且都没有进行相应的同步。

举个例子,当线程thrd1访问到sharedi的时候,sharedi的值是1000,然后线程thrd1将sharedi的值累加到了1001,可是线程thrd2取到sharedi的时候,sharedi的值是1000,这时候线程thrd2对sharedi的值进行加1操作,使其变成了1001,可是这个时候,sharedi的值已经被线程thrd1加到1001了,然而,thrd2并不知道,所以又将sharedi的值赋为了1001,从而导致了结果的错误。

这样,我们就需要一个线程互斥的机制,来保护sharedi这个变量,让同一时刻,只有一个线程能够访问到这个变量,从而使它的值能够保证正确的变化。

加锁,数据同步

通过加锁,保证sharedi变量在进行变更的时候,只有一个线程能够取到,并在在该线程对其进行操作的时候,其它线程无法对其进行访问。

 1 /*************************************************************************
 2     > File Name: mutex.c
 3     > Author: couldtt(fyby)
 4     > Mail: [email protected]
 5     > Created Time: 2013年12月15日 星期日 17时52分24秒
 6  ************************************************************************/
 7
 8 #include <stdio.h>
 9 #include <stdlib.h>
10 #include <pthread.h>
11
12 int sharedi = 0;
13 void increse_num(void);
14
15 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
16
17 int main(){
18     int ret;
19     pthread_t thrd1, thrd2, thrd3;
20
21     ret = pthread_create(&thrd1, NULL, (void *)increse_num, NULL);
22     ret = pthread_create(&thrd2, NULL, (void *)increse_num, NULL);
23     ret = pthread_create(&thrd3, NULL, (void *)increse_num, NULL);
24
25     pthread_join(thrd1, NULL);
26     pthread_join(thrd2, NULL);
27     pthread_join(thrd3, NULL);
28
29     printf("sharedi = %d\n", sharedi);
30
31     return 0;
32
33 }
34
35 void increse_num(void) {
36     long i,tmp;
37     for(i=0; i<=100000; i++) {
38     /*加锁*/
39         if (pthread_mutex_lock(&mutex) != 0) {
40            perror("pthread_mutex_lock");
41            exit(EXIT_FAILURE);
42         }
43         tmp = sharedi;
44         tmp = tmp + 1;
45         sharedi = tmp;
46     /*解锁锁*/
47         if (pthread_mutex_unlock(&mutex) != 0) {
48             perror("pthread_mutex_unlock");
49             exit(EXIT_FAILURE);
50         }
51     }
52 }

结果分析

这一次,我们的结果是正确的,锁有效得保护了我们的数据安全。然而:

  1. 锁保护的并不是我们的共享变量(或者说是共享内存),对于共享的内存而言,用户是无法直接对其保护的,因为那是物理内存,无法阻止其他程序的代码访问。事实上,锁之所以对关键区域进行了保护,在本例中,是因为所有线程都遵循了一个规则,那就是在进入关键区域钱加同一把锁,在退出关键区域钱释放同一把
  2. 我们从上述运行结果中可以看到,加锁是会带来额外的开销的,加锁的代码其运行速度,明显比不加锁的要慢一些,所以,在使用锁的时候,要合理,在不需要对关键区域进行保护的场景下,我们便不要画蛇添足,为其加锁了

方式二:信号量

锁有一个很明显的缺点,那就是它只有两种状态:锁定与不锁定。

信号量本质上是一个非负数的整数计数器,它也被用来控制对公共资源的访问。当公共资源增加的时候,调用信号量增加函数sem_post()对其进行增加,当公共资源减少的时候,调用函数sem_wait()来减少信号量。其实,我们是可以把锁当作一个0-1信号量的。

它们是在/usr/include/semaphore.h中进行定义的,信号量的数据结构为sem_t, 本质上,它是一个long型整数

相关函数

在使用semaphore之前,我们需要先引入头文件#include <semaphore.h>

  • 初始化信号量: int sem_init(sem_t *sem, int pshared, unsigned int value);

    • 成功返回0,失败返回-1
    • 参数
    • sem:指向信号量结构的一个指针
    • pshared: 不是0的时候,该信号量在进程间共享,否则只能为当前进程的所有线程们共享
    • value:信号量的初始值
  • 信号量减1操作,当sem=0的时候该函数会堵塞 int sem_wait(sem_t *sem);
    • 成功返回0,失败返回-1
    • 参数
    • sem:指向信号量的一个指针
  • 信号量加1操作 int sem_post(sem_t *sem);
    • 参数与返回同上
  • 销毁信号量 int sem_destroy(sem_t *sem);
    • 参数与返回同上
代码示例

 1 /*************************************************************************
 2     > File Name: sem.c
 3     > Author: couldtt(fyby)
 4     > Mail: [email protected]
 5     > Created Time: 2013年12月15日 星期日 19时25分08秒
 6  ************************************************************************/
 7
 8 #include <stdio.h>
 9 #include <unistd.h>
10 #include <pthread.h>
11 #include <semaphore.h>
12
13 #define MAXSIZE 10
14
15 int stack[MAXSIZE];
16 int size = 0;
17 sem_t sem;
18
19 // 生产者
20 void provide_data(void) {
21     int i;
22     for (i=0; i< MAXSIZE; i++) {
23         stack[i] = i;
24         sem_post(&sem); //为信号量加1
25     }
26 }
27
28 // 消费者
29 void handle_data(void) {
30     int i;
31     while((i = size++) < MAXSIZE) {
32         sem_wait(&sem);
33         printf("乘法: %d X %d = %d\n", stack[i], stack[i], stack[i]*stack[i]);
34         sleep(1);
35     }
36 }
37
38 int main(void) {
39
40     pthread_t provider, handler;
41
42     sem_init(&sem, 0, 0); //信号量初始化
43     pthread_create(&provider, NULL, (void *)handle_data, NULL);
44     pthread_create(&handler, NULL, (void *)provide_data, NULL);
45     pthread_join(provider, NULL);
46     pthread_join(handler, NULL);
47     sem_destroy(&sem); //销毁信号量
48
49     return 0;
50 }

运行结果:

因为信号量机制的存在,所以代码在handle_data的时候,如果sem_wait(&sem)时,sem为0,那么代码会堵塞在sem_wait上面,从而避免了在stack中访问错误的index而使整个程序崩溃。

参考资料

时间: 2024-08-04 20:43:12

(转载)Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)的相关文章

转载自~浮云比翼:Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)

Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥) 介绍:什么是线程,线程的优点是什么 线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等.但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage). 一

Unix/Linux环境C编程入门教程(24) MySQL 5.7.4 for Red Hat Enterprise 7(RHEL7)的安装

远观历史, MySQL的主要目的是为了能够在单处理器核心的商业服务器上运行.如今MySQL的一个变化用户可能不会注意到,那就是甲骨文已经开始重新架构MySQL的代码,使它大量的模块化.如软件解析器,优化和复制功能以模块化的形式被重写.该版本的查询性能得以大幅提升,比 MySQL 5.6 提升 1 倍,新版本的 MySQL 在测试平台上可达到每秒 512000 只读 QPS,而 MySQL 5.6 最高只能到 250000 QPS.该性能的提升是通过其 Memcached 插件实现的.同时该版本在

Unix/Linux环境C编程入门教程(12) openSUSECCPP以及Linux内核驱动开发环境搭建

1. openSUSE是一款优秀的linux. 2.选择默认虚拟机 3.选择稍后安装操作系统 4.选择linux  opensuse 5. 选择默认虚拟机名称 6.设置处理器为双核. 7.内存设置为2G 8. 选择网络地址转换 9.设置IO控制器 10. 选择默认磁盘类型 11.创建一个新的虚拟磁盘 12.设置磁盘大小 13.选择路径保存虚拟磁盘 14. 完成虚拟机创建 15.设置虚拟机 16.选择opensuse镜像 17.开启虚拟机 18.虚拟机启动 19.安装opensuse 20.安装程

Unix/Linux环境C编程入门教程(41) C语言库函数的文件操作详解

?? 上一篇博客我们讲解了如何使用Linux提供的文件操作函数,本文主要讲解使用C语言提供的文件操作的库函数. 1.函数介绍 fopen(打开文件) 相关函数 open,fclose 表头文件 #include<stdio.h> 定义函数 FILE * fopen(const char * path,const char * mode); 函数说明 参数path字符串包含欲打开的文件路径及文件名,参数mode字符串则代表着流形态. mode有下列几种形态字符串: r 打开只读文件,该文件必须存

Linux下C编程入门

1.首先,以Ubuntu 64-bit操作系统下为例 图中的sudo su表示的是快速进入到root管理员下.这里的密码默认的是安装后的Ubunto操作系统中自定义的用户名的密码,此处以用户toto,密码:123456为例 2.在/demo目录下创建以下三个文件(看截图): 注意:其中add.c的内容如下(通过vi编辑器编辑add.c): 其中的add.h(用于声明同级目录下的add.c文件中编写的函数).截图如下: 3.关于自定义add.h函数的引用,在main.c中的写法如下: 4.编译ad

Unix/Linux环境C编程入门教程(1) Solaris 11 64bit环境搭建

Unix/Linux版本众多,我们推荐Unix/Linux初学者选用几款典型的Unix/Linux操作系统进行学习. 本文就带大家来安装Solaris 11 64位并且配置好C/C++开发环境 本文所需软件下载地址 33.  F2 开始安装 34.待安装完成 按F8重启 35.重启后的界面  直接回车  启动 36. 登陆切换到我们刚刚创建的用户 待切成功的时候我们就加载合成的镜像文件sol-11_1-repo-full.iso 然后我们切换到root用户 37.先取消掉已连接的状态 38.重新

Unix/Linux环境C编程入门教程(5) Red Hat Enterprise Linux(RHEL)环境搭建

Unix/Linux版本众多,我们推荐Unix/Linux初学者选用几款典型的Unix/Linux操作系统进行学习. Red Hat Enterprise Linux是Red Hat公司的Linux发行版,面向商业市场,包括大型机.红帽公司从Red Hat Enterprise Linux 5开始对企业版LINUX的每个版本提供10年的支持[1].而Red Hat Enterprise Linux常简作RHEL. 1. 启动Vmware,如果没有安装的话,请看前面VMware安装的视频 2.选中

Unix/Linux环境C编程入门教程(40) 初识文件操作

?? 1.函数介绍 close(关闭文件) 相关函数 open,fcntl,shutdown,unlink,fclose 表头文件 #include<unistd.h> 定义函数 int close(int fd); 函数说明 当使用完文件后若已不再需要则可使用close()关闭该文件,二close()会让数据写回磁盘,并释放该文件所占用的资源.参数fd为先前由open()或creat()所返回的文件描述词. 返回值 若文件顺利关闭则返回0,发生错误时返回-1. 错误代码 EBADF 参数fd

C++多线程编程入门之经典实例

多线程在编程中有相当重要的地位,我们在实际开发时或者找工作面试时总能遇到多线程的问题,对多线程的理解程度从一个侧面反映了程序员的编程水平. 其实C++语言本身并没有提供多线程机制,但Windows系统为我们提供了相关API,我们可以使用它们来进行多线程编程.本文就以实例的形式讲解多线程编程的知识. 创建线程的API函数 C++代码 HANDLE CreateThread( __in   SEC_ATTRS SecurityAttributes, __in   ULONG StackSize,