POSIX 线程详解-程序员宅基地

技术标签: 线程  linux  linux 服务器  

转自http://www.360doc.com/content/14/1231/15/7534118_437137119.shtml

线程是有趣的

了解如何正确运用线程是每一个优秀程序员必备的素质。线程类似于进程。如同进程,线程由内核按时间分片进行管理。在单处理器系统中,内核使用时间分片来模拟线程的并发执行,这种方式和进程的相同。而在多处理器系统中,如同多个进程,线程实际上一样可以并发执行。

那么为什么对于大多数合作性任务,多线程比多个独立的进程更优越呢?这是因为,线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以,程序中的所有线程都可以读或写声明过的全局变量。如果曾用 fork() 编写过重要代码,就会认识到这个工具的重要性。为什么呢?虽然 fork() 允许创建多个进程,但它还会带来以下通信问题: 如何让多个进程相互通信,这里每个进程都有各自独立的内存空间。对这个问题没有一个简单的答案。虽然有许多不同种类的本地 IPC (进程间通信),但它们都遇到两个重要障碍:

  • 强加了某种形式的额外内核开销,从而降低性能。
  • 对于大多数情形,IPC 不是对于代码的“自然”扩展。通常极大地增加了程序的复杂性。

双重坏事: 开销和复杂性都非好事。如果曾经为了支持 IPC 而对程序大动干戈过,那么您就会真正欣赏线程提供的简单共享内存机制。由于所有的线程都驻留在同一内存空间,POSIX 线程无需进行开销大而复杂的长距离调用。只要利用简单的同步机制,程序中所有的线程都可以读取和修改已有的数据结构。而无需将数据经由文件描述符转储或挤入紧窄的共享内存空间。仅此一个原因,就足以让您考虑应该采用单进程/多线程模式而非多进程/单线程模式。

回页首

线程是快捷的

不仅如此。线程同样还是非常快捷的。与标准 fork() 相比,线程带来的开销很小。内核无需单独复制进程的内存空间或文件描述符等等。这就节省了大量的 CPU 时间,使得线程创建比新进程创建快上十到一百倍。因为这一点,可以大量使用线程而无需太过于担心带来的 CPU 或内存不足。使用 fork() 时导致的大量 CPU 占用也不复存在。这表示只要在程序中有意义,通常就可以创建线程。

当然,和进程一样,线程将利用多 CPU。如果软件是针对多处理器系统设计的,这就真的是一大特性(如果软件是开放源码,则最终可能在不少平台上运行)。特定类型线程程序(尤其是 CPU 密集型程序)的性能将随系统中处理器的数目几乎线性地提高。如果正在编写 CPU 非常密集型的程序,则绝对想设法在代码中使用多线程。一旦掌握了线程编码,无需使用繁琐的 IPC 和其它复杂的通信机制,就能够以全新和创造性的方法解决编码难题。所有这些特性配合在一起使得多线程编程更有趣、快速和灵活。

回页首

线程是可移植的

如果熟悉 Linux 编程,就有可能知道 __clone() 系统调用。__clone() 类似于 fork(),同时也有许多线程的特性。例如,使用 __clone(),新的子进程可以有选择地共享父进程的执行环境(内存空间,文件描述符等)。这是好的一面。但 __clone() 也有不足之处。正如__clone() 在线帮助指出:

“__clone 调用是特定于 Linux 平台的,不适用于实现可移植的程序。欲编写线程化应用程序(多线程控制同一内存空间),最好使用实现 POSIX 1003.1c 线程 API 的库,例如 Linux-Threads 库。参阅 pthread_create(3thr)。”

虽然 __clone() 有线程的许多特性,但它是不可移植的。当然这并不意味着代码中不能使用它。但在软件中考虑使用 __clone() 时应当权衡这一事实。值得庆幸的是,正如 __clone() 在线帮助指出,有一种更好的替代方案:POSIX 线程。如果想编写 可移植的 多线程代码,代码可运行于 Solaris、FreeBSD、Linux 和其它平台,POSIX 线程是一种当然之选。

回页首

第一个线程

下面是一个 POSIX 线程的简单示例程序:

thread1.c
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
 void *thread_function(void *arg) {
  int i;
  for ( i=0; i<20; i++) {
    printf("Thread says hi!\n");
    sleep(1);
  }
  return NULL;
}
int main(void) {
  pthread_t mythread;
  
  if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
    printf("error creating thread.");
    abort();
  }
  if ( pthread_join ( mythread, NULL ) ) {
    printf("error joining thread.");
    abort();
  }
  exit(0);
}

要编译这个程序,只需先将程序存为 thread1.c,然后输入:

$ gcc thread1.c -o thread1 -lpthread

运行则输入:

$ ./thread1

回页首

理解 thread1.c

thread1.c 是一个非常简单的线程程序。虽然它没有实现什么有用的功能,但可以帮助理解线程的运行机制。下面,我们一步一步地了解这个程序是干什么的。main() 中声明了变量 mythread,类型是 pthread_t。pthread_t 类型在 pthread.h 中定义,通常称为“线程 id”(缩写为 "tid")。可以认为它是一种线程句柄。

mythread 声明后(记住 mythread 只是一个 "tid",或是将要创建的线程的句柄),调用 pthread_create 函数创建一个真实活动的线程。不要因为 pthread_create() 在 "if" 语句内而受其迷惑。由于 pthread_create() 执行成功时返回零而失败时则返回非零值,将 pthread_create() 函数调用放在 if() 语句中只是为了方便地检测失败的调用。让我们查看一下 pthread_create 参数。第一个参数 &mythread 是指向 mythread 的指针。第二个参数当前为 NULL,可用来定义线程的某些属性。由于缺省的线程属性是适用的,只需将该参数设为 NULL。

第三个参数是新线程启动时调用的函数名。本例中,函数名为 thread_function()。当 thread_function() 返回时,新线程将终止。本例中,线程函数没有实现大的功能。它仅将 "Thread says hi!" 输出 20 次然后退出。注意 thread_function() 接受 void * 作为参数,同时返回值的类型也是 void *。这表明可以用 void * 向新线程传递任意类型的数据,新线程完成时也可返回任意类型的数据。那如何向线程传递一个任意参数?很简单。只要利用 pthread_create() 中的第四个参数。本例中,因为没有必要将任何数据传给微不足道的 thread_function(),所以将第四个参数设为 NULL。

您也许已推测到,在 pthread_create() 成功返回之后,程序将包含两个线程。等一等, 两个 线程?我们不是只创建了一个线程吗?不错,我们只创建了一个进程。但是主程序同样也是一个线程。可以这样理解:如果编写的程序根本没有使用 POSIX 线程,则该程序是单线程的(这个单线程称为“主”线程)。创建一个新线程之后程序总共就有两个线程了。

我想此时您至少有两个重要问题。第一个问题,新线程创建之后主线程如何运行。答案,主线程按顺序继续执行下一行程序(本例中执行 "if (pthread_join(...))")。第二个问题,新线程结束时如何处理。答案,新线程先停止,然后作为其清理过程的一部分,等待与另一个线程合并或“连接”。

现在,来看一下 pthread_join()。正如 pthread_create() 将一个线程拆分为两个, pthread_join() 将两个线程合并为一个线程。pthread_join() 的第一个参数是 tid mythread。第二个参数是指向 void 指针的指针。如果 void 指针不为 NULL,pthread_join 将线程的 void * 返回值放置在指定的位置上。由于我们不必理会 thread_function() 的返回值,所以将其设为 NULL.

您会注意到 thread_function() 花了 20 秒才完成。在 thread_function() 结束很久之前,主线程就已经调用了 pthread_join()。如果发生这种情况,主线程将中断(转向睡眠)然后等待 thread_function() 完成。当 thread_function() 完成后, pthread_join() 将返回。这时程序又只有一个主线程。当程序退出时,所有新线程已经使用 pthread_join() 合并了。这就是应该如何处理在程序中创建的每个新线程的过程。如果没有合并一个新线程,则它仍然对系统的最大线程数限制不利。这意味着如果未对线程做正确的清理,最终会导致 pthread_create() 调用失败。

回页首

无父,无子

如果使用过 fork() 系统调用,可能熟悉父进程和子进程的概念。当用 fork() 创建另一个新进程时,新进程是子进程,原始进程是父进程。这创建了可能非常有用的层次关系,尤其是等待子进程终止时。例如,waitpid() 函数让当前进程等待所有子进程终止。waitpid() 用来在父进程中实现简单的清理过程。

而 POSIX 线程就更有意思。您可能已经注意到我一直有意避免使用“父线程”和“子线程”的说法。这是因为 POSIX 线程中不存在这种层次关系。虽然主线程可以创建一个新线程,新线程可以创建另一个新线程,POSIX 线程标准将它们视为等同的层次。所以等待子线程退出的概念在这里没有意义。POSIX 线程标准不记录任何“家族”信息。缺少家族信息有一个主要含意:如果要等待一个线程终止,就必须将线程的 tid 传递给 pthread_join()。线程库无法为您断定 tid。

对大多数开发者来说这不是个好消息,因为这会使有多个线程的程序复杂化。不过不要为此担忧。POSIX 线程标准提供了有效地管理多个线程所需要的所有工具。实际上,没有父/子关系这一事实却为在程序中使用线程开辟了更创造性的方法。例如,如果有一个线程称为线程 1,线程 1 创建了称为线程 2 的线程,则线程 1 自己没有必要调用 pthread_join() 来合并线程 2,程序中其它任一线程都可以做到。当编写大量使用线程的代码时,这就可能允许发生有趣的事情。例如,可以创建一个包含所有已停止线程的全局“死线程列表”,然后让一个专门的清理线程专等停止的线程加到列表中。这个清理线程调用 pthread_join() 将刚停止的线程与自己合并。现在,仅用一个线程就巧妙和有效地处理了全部清理。

回页首

同步漫游

现在我们来看一些代码,这些代码做了一些意想不到的事情。thread2.c 的代码如下:

thread2.c
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal;
 void *thread_function(void *arg) {
  int i,j;
  for ( i=0; i<20; i++) {
    j=myglobal;
    j=j+1;
    printf(".");
    fflush(stdout);
    sleep(1);
    myglobal=j;
  }
  return NULL;
}
int main(void) {
  pthread_t mythread;
  int i;
  if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
    printf("error creating thread.");
    abort();
  }
  for ( i=0; i<20; i++) {
    myglobal=myglobal+1;
    printf("o");
    fflush(stdout);
    sleep(1);
  }
  if ( pthread_join ( mythread, NULL ) ) {
    printf("error joining thread.");
    abort();
  }
  printf("\nmyglobal equals %d\n",myglobal);
  exit(0);
}

回页首

理解 thread2.c

如同第一个程序,这个程序创建一个新线程。主线程和新线程都将全局变量 myglobal 加一 20 次。但是程序本身产生了某些意想不到的结果。编译代码请输入:

$ gcc thread2.c -o thread2 -lpthread

运行请输入:

$ ./thread2

输出:

$ ./thread2
..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o
myglobal equals 21

非常意外吧!因为 myglobal 从零开始,主线程和新线程各自对其进行了 20 次加一, 程序结束时 myglobal 值应当等于 40。由于 myglobal 输出结果为 21,这其中肯定有问题。但是究竟是什么呢?

放弃吗?好,让我来解释是怎么一回事。首先查看函数 thread_function()。注意如何将 myglobal 复制到局部变量 "j" 了吗? 接着将 j 加一, 再睡眠一秒,然后到这时才将新的 j 值复制到 myglobal?这就是关键所在。设想一下,如果主线程就在新线程将 myglobal 值复制给 j  立即将 myglobal 加一,会发生什么?当 thread_function() 将 j 的值写回 myglobal 时,就覆盖了主线程所做的修改。

当编写线程程序时,应避免产生这种无用的副作用,否则只会浪费时间(当然,除了编写关于 POSIX 线程的文章时有用)。那么,如何才能排除这种问题呢?

由于是将 myglobal 复制给 j 并且等了一秒之后才写回时产生问题,可以尝试避免使用临时局部变量并直接将 myglobal 加一。虽然这种解决方案对这个特定例子适用,但它还是不正确。如果我们对 myglobal 进行相对复杂的数学运算,而不是简单的加一,这种方法就会失效。但是为什么呢?

要理解这个问题,必须记住线程是并发运行的。即使在单处理器系统上运行(内核利用时间分片模拟多任务)也是可以的,从程序员的角度,想像两个线程是同时执行的。thread2.c 出现问题是因为 thread_function() 依赖以下论据:在 myglobal 加一之前的大约一秒钟期间不会修改 myglobal。需要有些途径让一个线程在对 myglobal 做更改时通知其它线程“不要靠近”。我将在下一篇文章中讲解如何做到这一点。到时候见。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zmbbb/article/details/50913778

智能推荐

Python入门基础书单——肯定有你想要的!_python编程从入门到精通 pan.baidu.com-程序员宅基地

​在Python基础图书中共收录了41本推荐图书,并附上了资源获取的方法。如果推荐的图书对你有帮助,为了更好的学习体验和知识产权的保护,请到正规渠道购买正版书籍。python学习手册(第4版)https://pan.baidu.com/s/1fBKy1pF3gEIH_XuqrvUSrw提取码:gbms流畅的pythonhttps://pan.baidu.com/s/1pQgwVKM-tf-hkxY9Mxt6DQ提取码:gpdjPython语言及其应.._python编程从入门到精通 pan.baidu.com

SparkStreaming+Kafka1.0.x多主题多分区偏移量维护-程序员宅基地

偏移量保存到数据库一、版本区别之前版本的kafka偏移量都是保存在kafka中的,而现在的kafka偏移量保存在了自己的一个特殊主题__consumer__offsets中二、维护思路根据传入的主题以及消费者组,先判断库中是否存在当前消费者组的消费记录,如果不存在,则证明为第一次消费,获取主题每分区当前的偏移量保存入库,如果存在,则读取库中各分区偏移量字段,封装为MAP,传入创建Dstre...

oracle 函数 和 优化-程序员宅基地

sql语句中,如果where条件里面含有not, !=, &lt;&gt; ,null ,则即使该字段建有索引,也不会走索引拷贝表(拷贝数据,源表名:a 目标表名:b) insert into b1(a, b, c) select d,e,f from b2; // 表结构已存在的情况下,复制另外的表的数据create table test as select * from..._提高oracle函数

PHP中isset()、is_null()、empty()的比较-程序员宅基地

在项目中经常会看到在判断中用is_set(),is_null(),empty()作为判断校验条件的,但是你知道在什么情况下用哪一种最合适吗?今天来做一下归纳总结!首先不管三七二十一,翻一翻官方手册才是王道 ‘u’ 。issetbool isset ( mixed $var [, mixed $... ] )检测变量是否已设置并且非 NULL如果已经使用 unset() 释放了一个变...

损失函数:交叉熵_交叉熵是凸函数吗-程序员宅基地

给定两个概率分布p和q,通过q来表示p的交叉熵为:H(p,q)=−∑xp(x)logq(x)\LARGE H(p,q)=-\sum_{x}p(x)logq(x)H(p,q)=−∑x​p(x)logq(x)交叉熵刻画的是两个概率分布之间的距离,或可以说它刻画的是通过概率分布q来表达概率分布p的困难程度,p代表正确答案,q代表的是预测值,交叉熵越小,两个概率的分布约接近。在神经网络中通常使用 S..._交叉熵是凸函数吗

Java网络编程——基于socket的HTTP服务器-程序员宅基地

简介本文通过最底层的socket来实现HTTP服务器的功能——并发处理GET、POST、HEAD等请求。HTTP协议的概览GET请求GET请求的格式GET <请求路径> HTTP/1.1key:value对key:value对key:value对key:value对空行则结束GET请求的例子在google的开发者工具的network里就可以看到GET / HTTP/1.1accept-encoding: gzip, deflate, braccep

随便推点

黑马程序员_Java语言_异常,File类-程序员宅基地

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-19.01_异常(异常的概述和分类)A:异常的概述 异常就是Java程序在运行过程中出现的错误。B:异常的分类 通过API查看Throwable//Throwable 类是 Java 语言中所有错误或异常的超类Error 服务器宕机,数据库崩溃等ExceptionC:异常的继承体系 Throwab

电子学会(Scratch)等级考试(四级)选择题!-程序员宅基地

点击上面微信号关注我关注我哟每天中午12:00-2:00定期推送文章,喜欢的可以设置星标,并分享点赞我们的文章,非常感谢大家的支持,您的点击的在看就是我们的动力!今天我们分享一下四级等级考..._scratch四级题库选择题a

matlab 数组元素去重_matlab检查向量中有没有重复元素-程序员宅基地

1. 前言今天老板突然在群里问了一个问题, 如何使用matlab 将一个数组中重复出现的所有元素剔除。 ie: [1,1,2,3,4] ⇒ [2,3,4]2. 实现思路基本思路就是统计每个元素出现的次数, 然后选出出现次数为 1 次的进行输出。 使用C++ 我们可以借助一个 map 非常容易的实现, 但是要求用matlab 确实有些费劲。。。。3. 实现方法其中, histc 是_matlab检查向量中有没有重复元素

Java判断平年闰年_java平年闰年的判断代码-程序员宅基地

定义:非整百年份除以4,整百年份除以400,如果不能被整除的是平年,可以被整除的就是闰年。_java平年闰年的判断代码

基于单片机的加热炉炉温控制系统设计_计算机控制加热炉系统设计_biyezuopinvip的博客-程序员宅基地

但由于输出控制量只有两种状态,使被控参数在两个方向上变化的速率均为最大,因此容易引起反馈回路产生振荡,对自动控制加热炉炉温控制系统会产生十分不利的影响,甚至会因为输出开关的频繁动作而不能满足加热炉炉温控制系统对控制精度的要求。但随着计算机与超大规模集成电路的迅速发展,以现代控制理论和计算机为基础,采用数字控制、显示、A/D与D/A转换,配合执行器与控制阀构成的计算机控制加热炉炉温控制系统,在过程控制过程中得到越来越广泛的应用。由于炉温控制加热炉炉温控制系统的控制对象具有惯性大,连续性的特点。_计算机控制加热炉系统设计

@PostConstruct_postconstruct 在数据库连接后执行-程序员宅基地

不要在@PostConstruct注解的方法中写过为复杂的东西,会导致程序启动很慢。特别是while(true)这种,甚至导致程序启动不起来_postconstruct 在数据库连接后执行