帆子
作者帆子·2020-12-27 22:14
售前技术支持·国内某服务器生产商

在IBM i 上用C实现多线程编程

字数 9149阅读 1717评论 0赞 1

在 IBM i 上使用 C 实现多线程编程,基本和用 Java 一样,并不复杂。本文希望以一个简单的程序为例,大致给出一个通用的基础框架。

在涉及程序代码之前,让我们先看一下它的流程图。

整个流程非常简单:主线程开始运行,它首先初始化一个 pthread_attr ,然后对该 pthread_attr 设置属性,接着就是最重要的一步——通过 pthread_create 函数 创建并开启若干子线程。这些子线程将开始独立、并发地运行。它们会根据 thread 函数,执行同样的 代码,但可以依据由主线程传过来的参数,处理不同的数据。而主程序则通过 pthread_join 函数,阻塞等待所有子线程的结束合入,然后作一番清理工作,最后结束。

下面我们就来看看程序:

#define _MULTI_THREADED
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>

#define  THRDNMB  5

#define checkResults(string, val) {                \\
  if (val) {                                                         \\
    printf("Failed with %d at %s", val, string);  \\
    exit(1);                                                        \\
  }                                                                    \\
}

char * getct(char * ct) {

  struct timeval now;
  int rc;

  rc = gettimeofday(&now, NULL);
  if (rc==0) {
    sprintf(ct, "%u.%06u", now.tv_sec, now.tv_usec);
  } else {
    sprintf(ct, "timeerror");
  }
  return(ct);
}

typedef struct {
  int   value;
} thread_parm_t;

void *threadfunc(void *parm) {

  int               i;
  char              ct[20];
  thread_parm_t     *p;
  pthread_id_np_t   thdid;

  thdid = pthread_getthreadid_np();
  p = (thread_parm_t *) parm;

  printf("线程%.8x%.8x - (%d)开始工作,时间是%s\\n",    \\
           thdid, p->value, getct(ct));

  for (i=0; i<10; i++)
  printf("线程%.8x%.8x - (%d)数到%d,时间是%s\\n",      \\
           thdid, p->value, i, getct(ct));


  printf("线程%.8x%.8x - (%d)结束工作,时间是%s\\n",     \\
           thdid, p->value, getct(ct));

  p->value = p->value * 2;

}

int main(int argc, char **argv) {

  int                   i=0;
  int                   rc=0;

  pthread_t             threadid[10];
  thread_parm_t         *(parm[THRDNMB]);
  pthread_attr_t        pta;

  for (i=0; i<THRDNMB; i++)
    parm[i] = malloc(sizeof(thread_parm_t));

  pthread_attr_init(&pta);
  pthread_attr_setdetachstate(&pta, PTHREAD_CREATE_JOINABLE);

  printf("Creating %d threads\\n", THRDNMB);
  for (i=0; i<THRDNMB; i++) {
    parm[i]->value = i;
    rc = pthread_create(&threadid[i], &pta,                            \\
                        threadfunc, (void *) parm[i]);
    checkResults("pthread_create()\\n", rc);
  }

  for (i=0; i<THRDNMB; i++) {
    rc = pthread_join(threadid[i], NULL);
    checkResults("pthread_join()\\n", rc);
  }

  for (i=0; i<THRDNMB; i++)
    printf("%d->%d\\n", i ,(parm[i])->value);

  printf("Cleanup\\n");
  for (i=0; i<THRDNMB; i++)
    free(parm[i]);
  pthread_attr_destroy(&pta);

  printf("Main completed\\n");
  return 0;
}

该程序开头定义了两个函数, getct() 用来提供当前的时间戳,而 checkResults() 则是为显示出错的通用模块。

我们还定义了一个结构类型,里面只有一个整型变量。我们打算将它用在主线程和子线程的参数传递上。

接下来就是 thread 函数的定义,也就是子线程所要执行的代码。注意,这个 thread 函数要以一个函数指针的形式来构建,我们给这个函数指针起名为 threadfunc 。除了运行属性方面的信息,这个 thread 函数还会从主线程那里获得相关的参数信息。该参数的类型就是我们开头所定义的那个结构类型。同样地,这里传的也是参数的指针。

这个 thread 函数做的事非常简单,就是从 0 数到 9 ,然后将从主线程那里拿到的结构参数进行修改,具体的就是将结构中的整型变量的值进行一个倍增,最后结束。

来到主程序部分,这也是主线程所执行的代码。正如我们在先前流程介绍里所描述的,它首先初始化一个 pthread_attr ,然后对该 pthread_attr 设置属性,接着创建并开启若干子线程。这里,通过 malloc 函数,主线程在 Heap Storage 上获取到空间,为每个子线程准备了各自的结构参数,并通过 pthread_create 函数将这些结构参数的指针分别传送给对应的子线程。这些结构参数并不是共享资源,每个子线程都可获取到属于自己的结构参数,系统不会出现子线程之间相互竞争共享资源的情形。

创建和开启子线程后,主线程便开始依次执行 thread_join 函数,这使它处于阻塞等待状态。它将等待所有子线程的结束合入。一旦所有子线程结束,主线程就可以依次访问每个子线程所属的结构参数,并将每个子线程处理过后的结果显示出来。最后清理并结束。

接下去我们将这段程序中所涉及的用于多线程的主要函数介绍一下:

int pthread_attr_init(pthread_attr_t *attr);

其中, attr 是一个指向线程属性类型的指针。
该函数的作用就是初始化一个线程属性对象,并将其内容设置为缺省线程属性。

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

其中, attr 是一个指向线程属性类型的指针。 detachstate 是一个整型变量,代表着线程属性的分离状态属性,其取值有两个: PTHREAD_CREATE_JOINABLE ( 0 ),或是 PTHREAD_CREATE_DETACHED ( 1 )。前者是缺省值。
该函数的作用就是 设置线程属性对象的分离状态属性。线程的分离状态属性用来在创建线程时,设置它是否处于分离状态。这将决定当线程终止时,系统是否允许其立即释放该线程所占的资源 。 DETACHED 意味着立即释放,而 JOINABLE 意味着在主线程针对该线程运行 pthread_join 函数时加以释放。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                  void *(*start_routine)(void *), void *arg);

其中, thread 是一个指向线程类型的指针, attr 是一个指向线程属性类型的指针。
start_routine 是一个指向带有单一参数指针的函数的指针。在实际应用中,它就是 thread 函数的名字。我们可以注意以下 thread 函数是如何定义的。
arg 是一个指向参数的指针,用来为主线程向子线程传递参数。
该函数的作用就是根据所给的线程属性来创建一个线程,并启动它。启动后的线程会去执行 start_routine ,也就是具体的 thread 函数,并接收来自主线程的参数(如果有的话)。

int pthread_join(pthread_t thread, void **status);

其中, thread 是一个线程类型的指针, status 是指向线程的退出状态的一个指针。
该函数的作用就是 等待一个线程的结束合入,回收该线程的资源,然后返回线程的退出状态。

int pthread_attr_destroy(pthread_attr_t *attr);

其中, attr 是一个指向线程属性类型的指针。
该函数的作用就是清除一个线程属性对象, 并允许系统回收与该线程属性对象相关联的任何资源。不过,它对先前使用此线程属性对象创建并运行的线程没有影响。

好了,让我们来运行一下这个程序。一般多线程程序是提交给后台作业来运行的。在第一次提交时,需要让批处理作业允许运行多线程。

我们的做法是:

首先,参照 QGPL/QDFTJOBD 复制一个私人使用的 JOBD ,可以命名为 MULTIJOBD ;

CRTDUPOBJ OBJ(QDFTJOBD) FROMLIB(QGPL)
       OBJTYPE(*JOBD) TOLIB(QGPL) NEWOBJ(MULTIJOBD)

其次修改这个新建的 JOBD ,将其中的 ALWMLTTHD ( Allow Multiple Thread )参数设为 *YES ;

CHGJOBD JOBD(QGPL/MULTIJOBD) ALWMLTTHD(*YES)

最后,将程序提交给批处理作业运行。

SBMJOB CMD(CALL PGM(*LIBL/THRDSAMPLE))
        JOB(THRDSAMPLE) JOBD(QGPL/MULTIJOBD)

提交后,我们可以在 QBATCH 子系统下看到这个名为 THRDSAMPLE 的后台作业。

通过在该作业栏键入 12 选项,可以进一步了解该作业下开启的线程。

以下是另一次批处理作业运行结束以后输出的假脱机文件( Spool File )的内容:

Creating 5 threads                                           
线程000000000000006b - (0)开始工作,时间是1609085421.855640  
线程000000000000006c - (1)开始工作,时间是1609085421.855640  
线程000000000000006b - (0)数到0,时间是1609085421.855696     
线程000000000000006c - (1)数到0,时间是1609085421.855712     
线程000000000000006b - (0)数到1,时间是1609085421.855728     
线程000000000000006c - (1)数到1,时间是1609085421.855744     
线程000000000000006e - (3)开始工作,时间是1609085421.855768  
线程000000000000006c - (1)数到2,时间是1609085421.855776     
线程000000000000006d - (2)开始工作,时间是1609085421.855720  
线程000000000000006c - (1)数到3,时间是1609085421.855800     
线程000000000000006b - (0)数到2,时间是1609085421.855760     
线程000000000000006c - (1)数到4,时间是1609085421.855832     
线程000000000000006b - (0)数到3,时间是1609085421.855840     
线程000000000000006e - (3)数到0,时间是1609085421.855792     
线程000000000000006f - (4)开始工作,时间是1609085421.855808  
线程000000000000006e - (3)数到1,时间是1609085421.855872     
线程000000000000006f - (4)数到0,时间是1609085421.855888     
线程000000000000006d - (2)数到0,时间是1609085421.855816     
线程000000000000006f - (4)数到1,时间是1609085421.855912     
线程000000000000006d - (2)数到1,时间是1609085421.855920     
线程000000000000006f - (4)数到2,时间是1609085421.855928     
线程000000000000006b - (0)数到4,时间是1609085421.855864     
线程000000000000006e - (3)数到2,时间是1609085421.855896     
线程000000000000006b - (0)数到5,时间是1609085421.855968     
线程000000000000006e - (3)数到3,时间是1609085421.855976     
线程000000000000006b - (0)数到6,时间是1609085421.855984     
线程000000000000006c - (1)数到5,时间是1609085421.855856     
线程000000000000006b - (0)数到7,时间是1609085421.856008     
线程000000000000006d - (2)数到2,时间是1609085421.855944     
线程000000000000006b - (0)数到8,时间是1609085421.856032     
线程000000000000006f - (4)数到3,时间是1609085421.855952     
线程000000000000006b - (0)数到9,时间是1609085421.856048     
线程000000000000006e - (3)数到4,时间是1609085421.856000     
线程000000000000006b - (0)结束工作,时间是1609085421.856064  
线程000000000000006e - (3)数到5,时间是1609085421.856080     
线程000000000000006c - (1)数到6,时间是1609085421.856016     
线程000000000000006d - (2)数到3,时间是1609085421.856040     
线程000000000000006c - (1)数到7,时间是1609085421.856112     
线程000000000000006f - (4)数到4,时间是1609085421.856056     
线程000000000000006c - (1)数到8,时间是1609085421.856144     
线程000000000000006f - (4)数到5,时间是1609085421.856152     
线程000000000000006c - (1)数到9,时间是1609085421.856168     
线程000000000000006d - (2)数到4,时间是1609085421.856128     
线程000000000000006c - (1)结束工作,时间是1609085421.856192  
线程000000000000006e - (3)数到6,时间是1609085421.856096     
线程000000000000006f - (4)数到6,时间是1609085421.856184     
线程000000000000006e - (3)数到7,时间是1609085421.856232     
线程000000000000006d - (2)数到5,时间是1609085421.856208     
线程000000000000006e - (3)数到8,时间是1609085421.856264     
线程000000000000006d - (2)数到6,时间是1609085421.856280     
线程000000000000006e - (3)数到9,时间是1609085421.856296     
线程000000000000006d - (2)数到7,时间是1609085421.856304     
线程000000000000006f - (4)数到7,时间是1609085421.856256     
线程000000000000006e - (3)结束工作,时间是1609085421.856320  
线程000000000000006d - (2)数到8,时间是1609085421.856328     
线程000000000000006f - (4)数到8,时间是1609085421.856368     
线程000000000000006d - (2)数到9,时间是1609085421.856400     
线程000000000000006f - (4)数到9,时间是1609085421.856408     
线程000000000000006d - (2)结束工作,时间是1609085421.856424  
线程000000000000006f - (4)结束工作,时间是1609085421.856432  
0->0                                                         
1->2                                                         
2->4                                                         
3->6                                                         
4->8                                                         
Cleanup                                                      
Main completed                                                

显然,本文所涉及的多线程编程,是最简单最浅显的那一类,因为它并未涉及并发线程对共享资源的访问。有关这一部分,我将在后续的文章里谈及。

多线程的应用可以非常广泛,而且可以充分发挥多核服务器的效能。另外,由于线程启停的系统消耗要比作业少,所以将其应用于处理繁忙交易的场景非常合适。比如,我们可以用线程池来实现一个交易系统,这些线程有着不同的角色:总控主管、消息侦听、队列管理、任务分发、工作处理等等,通过这种池化的方式来应对繁多的交易请求。总之,多线程编程是提升性能的有力武器。

参考网站:

1,Knowledge Center – IBM i Programming - Multithreaded Applications

2,Knowledge Center – Pthread APIs

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

1

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广