`
eriol
  • 浏览: 408862 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

请您先登录,才能继续操作

线程的数据处理

阅读更多

 

和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。

 

  1、线程数据

 

  在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。

 

  和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。

 

  创建键的函数原型为:

 

extern int pthread_key_create __P ((pthread_key_t *__key,void (*__destr_function) (void *)));

 

  第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。

 

  在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数 createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。

 

 

/* 声明一个键*/
pthread_key_t myWinKey;

/* 函数 createWindow */
void createWindow ( void ) {
 Fl_Window * win;
 static pthread_once_t once= PTHREAD_ONCE_INIT;
 /* 调用函数createMyKey,创建键*/
 pthread_once ( & once, createMyKey) ;
 /*win指向一个新建立的窗口*/
 win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
 /* 对此窗口作一些可能的设置工作,如大小、位置、名称等*/
 setWindow(win);
 /* 将窗口指针值绑定在键myWinKey上*/
 pthread_setpecific ( myWinKey, win);
}

/* 函数 createMyKey,创建一个键,并指定了destructor */
void createMyKey ( void ) {
 pthread_keycreate(&myWinKey, freeWinKey);
}

/* 函数 freeWinKey,释放空间*/
void freeWinKey ( Fl_Window * win){
 delete win;
}

 

 

  这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数 pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下:

 

  extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));

  extern void *pthread_getspecific __P ((pthread_key_t __key));

 

  这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。

 

  2、互斥锁

 

  互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。

 

  我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。

 

 

void reader_function ( void );
void writer_function ( void );

char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;

void main ( void ){
 pthread_t reader;
 /* 定义延迟时间*/
 delay.tv_sec = 2;
 delay.tv_nec = 0;
 /* 用默认属性初始化一个互斥锁对象*/
 pthread_mutex_init (&mutex,NULL);
 pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
 writer_function( );
}

void writer_function (void){
 while(1){
  /* 锁定互斥锁*/
  pthread_mutex_lock (&mutex);
  if (buffer_has_item==0){
   buffer=make_new_item( );
   buffer_has_item=1;
  }
  /* 打开互斥锁*/
  pthread_mutex_unlock(&mutex);
  pthread_delay_np(&delay);
 }
}

void reader_function(void){
 while(1){
  pthread_mutex_lock(&mutex);
  if(buffer_has_item==1){
   consume_item(buffer);
   buffer_has_item=0;
  }
  pthread_mutex_unlock(&mutex);
  pthread_delay_np(&delay);
 }
}
 

  这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数 pthread_mutex_init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数 pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数 pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值, PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上锁、解锁机制,一般情况下,选用最后一个默认属性。

 

   pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。

 

  需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数 pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。

 

 

  3、条件变量

 

    互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。

 

  条件变量的结构为pthread_cond_t,函数pthread_cond_init() 被用来初始化一个条件变量。它的原型为:

 

  extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));

 

  其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构 pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是 PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_ destroy(pthread_cond_t cond)。 

 

  函数pthread_cond_wait() 使线程阻塞在一个条件变量上。它的函数原型为:

 

  extern int pthread_cond_wait __P ((pthread_cond_t *__cond,pthread_mutex_t *__mutex));

 

  线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数 pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。

 

  另一个用来阻塞线程的函数是pthread_cond_timedwait() ,它的原型为:

 

  extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,pthread_mutex_t *__mutex, __const struct timespec *__abstime));

 

  它比函数pthread_cond_wait() 多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。

 

  函数pthread_cond_signal() 的原型为:

 

  extern int pthread_cond_signal __P ((pthread_cond_t *__cond));

 

  它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait() 和函数pthread_cond_signal() 的一个简单的例子。

 

 

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;

decrement_count () {
 pthread_mutex_lock (&count_lock);
 while(count==0)
  pthread_cond_wait( &count_nonzero, &count_lock);
  count=count -1;
 pthread_mutex_unlock (&count_lock);
}

increment_count(){
 pthread_mutex_lock(&count_lock);
 if(count==0)
  pthread_cond_signal(&count_nonzero);
  count=count+1;
 pthread_mutex_unlock(&count_lock);
}
 

 

  count值为0时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数 increment_count时,pthread_cond_signal() 函数改变条件变量,告知decrement_count() 停止阻塞。读者可以试着让两个线程分别运行这两个函数,看看会出现什么样的结果。

 

  函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。

 

  4、信号量

 

  信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post() 增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait() 减少信号量。函数sem_trywait() 和函数pthread_ mutex_trylock() 起同样的作用,它是函数sem_wait() 的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件 /usr/include/semaphore.h中定义。

 

  信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init() 用来初始化一个信号量。它的原型为:

 

  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));

 

  sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。

 

  函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。

 

  函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait() 的非阻塞版本,它直接将信号量sem的值减一。

 

  函数sem_destroy(sem_t *sem)用来释放信号量sem。

 

  下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。

 

/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;

/* 从文件1.dat读取数据,每读一次,信号量加一*/
void ReadData1(void){
 FILE *fp=fopen("1.dat","r");
 while(!feof(fp)){
  fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
  sem_post(&sem);
  ++size;
 }
 fclose(fp);
}

/*从文件2.dat读取数据*/
void ReadData2(void){
 FILE *fp=fopen("2.dat","r");
 while(!feof(fp)){
  fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
  sem_post(&sem);
  ++size;
 }
 fclose(fp);
}

/*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/
void HandleData1(void){
 while(1){
  sem_wait(&sem);
  printf("Plus:%d+%d=%dn",stack[size][0],stack[size][1],
  stack[size][0]+stack[size][1]);
  --size;
 }
}

void HandleData2(void){
 while(1){
  sem_wait(&sem);
  printf("Multiply:%d*%d=%dn",stack[size][0],stack[size][1],
  stack[size][0]*stack[size][1]);
  --size;
 }
}

int main(void){
 pthread_t t1,t2,t3,t4;
 sem_init(&sem,0,0);
 pthread_create(&t1,NULL,(void *)HandleData1,NULL);
 pthread_create(&t2,NULL,(void *)HandleData2,NULL);
 pthread_create(&t3,NULL,(void *)ReadData1,NULL);
 pthread_create(&t4,NULL,(void *)ReadData2,NULL);
 /* 防止程序过早退出,让它在此无限期等待*/
 pthread_join(t1,NULL);
}
 

  在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:

 

Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
 

  从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。

 

原文地址:http://blog.csdn.net/celineshi/archive/2007/06/28/1670219.aspx

分享到:
评论

相关推荐

    分批取数 多线程数据处理

    分批取数 多线程数据处理

    高性能多线程数据处理库

    一个高性能的多线程数据处理库,采用 Apache Arrow 作为底层技术,提供了低内存占用和高处理速度的优势。它设计了简单易用的 API,支持处理大数据集(例如50GB),使数据处理更加高效。Polars 支持包括 Rust 和 ...

    qt多线程实例-数据处理和UI界面显示

    在Qt框架中,多线程技术是实现高效并发处理的关键,尤其在数据处理和用户界面(UI)更新方面。这个实例“qt多线程实例-数据处理和UI界面显示”很可能是为了展示如何在不阻塞UI的情况下进行繁重的数据处理任务。 在...

    C++窗口+后台线程+多线程数据处理编程思想及源代码

    //文件介绍 1. computeThread 多线程处理数据 2. icpDialog 窗口类 3. icpThread 线程类 4. tb_basedata 线程间通信数据类型 5. Xmutex 临界区 以上所有功能的实现都使用C++类封装

    多线程同步数据处理方法研究.pdf

    多线程数据处理的一个挑战是如何平衡并发性和数据一致性。过多的并发可能导致系统资源的竞争,而过于严格的同步可能会降低系统性能。因此,合理地设计线程同步策略,如使用适当的锁粒度,以及适时使用异步编程模型,...

    winform 线程数据插槽源码

    Winform线程数据插槽源码是一个用于Windows Forms应用程序中的多线程数据处理技术的实现。在Windows桌面应用开发中,尤其是使用C#或Visual Basic .NET时,Winform是常用的用户界面框架。然而,由于UI(用户界面)...

    winform 多线程处理数据

    - 使用适当的数据结构和算法优化数据处理,减少后台任务的时间消耗。 - 为避免资源竞争,确保线程安全,特别是在多个线程访问共享数据时。 通过理解和应用这些概念,开发者可以构建出高效且用户体验良好的WinForm...

    java多线程处理数据库数据

    在Java编程中,多线程处理是提升程序性能和效率的重要手段,特别是在处理大量数据库数据时。本主题将深入探讨如何使用Java的并发包(java.util.concurrent)来实现多线程对数据库数据的批量处理,包括增、删、改等...

    多线程处理数据(工具)样例

    总之,这个工具利用多线程并发处理数据流,通过合理调度线程,有效利用多核处理器,提升了数据处理速度。对于任何需要处理大量数据或进行复杂计算的项目,这都是一个值得借鉴和学习的技术实践。

    java多线程实现大批量数据导入源码

    本项目以"java多线程实现大批量数据导入源码"为题,旨在通过多线程策略将大量数据切分,并进行并行处理,以提高数据处理速度。 首先,我们需要理解Java中的线程机制。Java通过`Thread`类来创建和管理线程。每个线程...

    一种基于线程安全队列的卫星测试数据处理方法.pdf

    在实时采集数据处理时,线程安全队列能够根据队列中数据帧的数量自动启动多线程处理,从而提高数据处理的效率,保证了航天器测试的有效性、完整性和可靠性。 知识点三:航天器测试流程 文档中提到航天器测试过程中...

    多线程处理数据

    在IT行业中,多线程处理数据是一项至关重要的技术,尤其在大数据处理、实时计算和并发性能优化等场景中。本文将深入探讨多线程的概念、原理以及如何在Java中实现多线程,同时结合提供的"TestThread.java"文件,解析...

    多线程的处理

    例如,在网络应用中,一个线程可以处理用户界面更新,另一个线程则负责后台数据的下载或计算,这样即使在进行耗时操作时,用户界面也能保持流畅。在游戏开发中,多线程可以用来分离图形渲染和物理模拟,提高游戏性能...

    易语言线程返回数据的方法

    3. **共享数据结构**:线程可以将结果存入一个共享数据结构(如数组或链表),然后通过同步机制(如锁)通知其他线程数据已准备好。 4. **使用线程句柄**:易语言允许通过线程句柄发送消息,线程执行完毕后,可以...

    C#处理大容量数据,及多线程简单应用

    以下将详细介绍“C#处理大容量数据,及多线程简单应用”这一主题。 首先,当我们面临大量数据时,一个关键的考虑点是避免阻塞主线程,尤其是对于UI(用户界面)应用。在C#中,长时间运行的任务会阻塞UI线程,导致...

    多线程并发处理数据的问题

    在IT行业中,多线程并发处理数据是一种常见的优化策略,特别是在大数据处理、网络服务和分布式系统中。通过并发,可以充分利用多核处理器的计算能力,提高程序执行效率,缩短任务的总体完成时间。本篇文章将深入探讨...

    相机采集、处理、显示多线程处理框架

    相机采集、处理、显示多线程处理是指使用多个线程来同时执行相机数据采集、处理和显示的操作。其中采集线程负责从相机中获取图像数据,处理线程负责对采集到的图像数据进行处理和算法分析,显示线程负责将处理后的...

    在Delphi中利用多线程实现数据采集的方法.doc

    多线程技术在处理并发任务时具有明显优势,尤其是在实时数据采集系统中,它可以显著提高数据处理的速度和效率。 #### 关键知识点 ##### 多线程基础 1. **概念**:多线程是指在一个进程中创建多个线程来执行不同的...

Global site tag (gtag.js) - Google Analytics