rn_xtcxyczjh-6 并发[线程2 头文件重复包含 函数返回值的统一 结构体中的变长数组 线程锁接口]

2015.10.17
读xtcxyczjh(系统程序员成长计划)—- 学习程序设计方法。
练习源码保存地址:y15m10d17

y15m10d17/arrangement/

1. 整理

(1) 避免头文件重复包含

头文件重复包含时,可能会导致定义或声明的重复而导致编译错误。可以用条件编译让编译器在预编译阶段解决这个问题。

/* filename.h */
#ifndef FILENAME_H
#define FILENAME_H
//头文件内容
#endif

当在某文件中包含filname.h时,filname.h中的内容将会被复制到这个文件中替换#include “filename.h”语句。如果此文件是第一次包含filname.h,那么filname.h中的内容将将会生效(没有定义FILENAME_H);如果重复包含filname.h,则filname.h的包含内容将被忽略(第一次包含filename.h时已经define了FILENAME_H),就不会出现定义或声明的重复了。结合若头文件中已包含某头文件则源文件不载包含此头文件的原则可避免头文件重复包含。[使用跟头文件名一致的宏定义可避免在不同文件中使用相同宏名的情况。]

(2) 统一函数返回值

在dlist.c中即双向链表的函数中,有的函数具有返回值。可用一个联合将它们的返回值统一(每一个返回值情况都对应有一个特定的含义,方便给这样的函数进行错误包装)。不仅是dlist.c中的函数可以用,其它文件的函数也可以用,故而可以新建一个头文件typedef.h专门来作类似的定义。

/* typedef.h */
/* 定义大多文件都会用到的类型 */

#ifndef TYPEDEF_H
#define TYPEDEF_H

/* 统一函数的返回值 */
typedef enum _RetT {
    RET_FAILE = -1,
    RET_OK,
    RET_PARAMETER_INVALID
}RetT;
#endif

当函数还具有其它返回的情况时,可直接在enum _RetT中添加。
有了函数返回值的定义后,在有函数定义的文件中包含typedef.h(若需要),然后用联合中的值替换函数的返回值(可去掉tk.c中的PARA_INVALIED_RETURN_VALUE宏了)。

/* dlist.c */
/* 定义描述/管理(循环)双向链表的数据,定义(循环)双向链表的接口给 */
//……
#include "typedef.h"

//给双向链表中第i个节点中指向数据(即data)的指针赋值
RetT init_dlist_any_node_data(DlistManageT *pdl, unsigned int i, void *data)
{
    //……
    //判断参数是否无效
    return_val_if_p_invalid(pdl && data, RET_PARAMETER_INVALID);
    len = pdl->num;
    return_val_if_p_invalid(i > 0 && i <= len, RET_PARAMETER_INVALID);

    if (NULL != (pnd = get_dlist_any_node(pdl, i))) {
        //……
        return RET_OK;
    }
    return RET_FAIL;
}
//......

修改Makefile,往目前关联typedef.h文件的源文件的条件中添加typedef.h。

#Makefile
#make命令默认执行make all或者第一条规则
#......
dlist.o:    dlist.c dlist.h tk.h typedef.h
    $(CC) $(CFLAG_OBJ) $< $(CFLAGS_POSTFIX)
#......
cpthread.o: cpthread.c cpthread.h typedef.h
    $(CC) $(CFLAG_OBJ) $< $(CFLAGS_POSTFIX)
#......

在与各程序文件及Makefile同目录的linux终端下使用make命令编译程序,确保整理过程中没有错误。

y15m10d17/thread_interface/

2. 改进跟多线程关联的双向链表程序

需求简述。
对前面实现的双向链表做点改进:
- 支持多线程和单线程版本。对于多线程版本,由实现者(在链表)加锁/解锁,对于单线版本,其性能不受影响(或很小)。
- 区分单线程版本和多线程版本时,不需要链接不同的库,或者要宏来控制,完全可以在运行时切换。
- 保持双向链表的通用性,不依赖于特定的平台。

(1) (线程)锁的接口

对于不同平台的多线程程序,锁的实现方式不同[如linux下可使用sem_init/sem_wait/sem_post等函数(信号量的方式)实现锁;在windows上又是另外一些函数来实现锁]。用回调函数的方式实现加锁/解锁的程序(解决由调用者实现,同时解决双向链表的通用性,不依赖于特定平台。)为创建链表的函数加一个参数,由这个参数决定双向链表是否运行在多线程之下(即有无多线程加锁/解锁操作)。

在双向链表程序中规定线程锁回调函数的格式 [接口,接口是一个抽象的概念,它只定义调用者和实现者之间的契约,而不规定实现的方法。(在C语言中,接口的朴素定义是:一组相关的回调函数及其共享的上下文)接口在软件设计中占有非常重要的地位,它是隔离变化和降低复杂度最有力的武器。]
lcoker.h

/* locker.h */
/* 定义线程锁的接口(即用户规定回调函数的格式) */
#include "typedef.h"

/* -------类型声明区-------- */
struct _Locker;

/* --------类型定义区-------- */
typedef struct  _Locker Locker;
typedef RetT    (*LockerLockFuncT)(Locker *locker);
typedef RetT    (*LockerUnlockFuncT)(Locker *locker);
struct _Locker {
    LockerLockFuncT     lock;
    LockerUnlockFuncT   unlock;
    char mt[0];
};

Locker结构体中的lock元素指向特定平台之下的线程加锁函数,此函数的为LockerLockFuncT类型;Locker结构体中的unlock指向特定平台之下的线程解锁函数,此函数的为LockerUnlockFuncT类型。这两个函数指针都需要指向调用者在特定平台下编写的线程加/解锁的程序。

在结构体末尾定义一个维数为0的数组可以实现C语言变长数组。在结构中,mt是一个数组名,但该数组没有元素。该数组的地址紧随结构体之后。如果为这个结构体分配的内存大于这个结构体的实际大小,后面多于的部分就是mt的内容。

将线程锁的接口放入管理双向链表的数据结构中:
dlist.c

/* dlist.c */
/* 定义描述/管理(循环)双向链表的数据,定义(循环)双向链表的接口给 */
//……
#include "locker.h"
//……
//管理双向链表的结构体
//管理双向链表的结构体
struct _DlistManage {
    DlistNodeT      *first; //指向双向链表中的第一个节点
    unsigned int    num;    //保存当前双向链表的节点个数
    unsigned int    wl;     //双向链表初始化的个数
    Locker          *locker;    //线程锁接口
};

//……
static RetT lock_dlist(DlistManageT *pdl)
{
    return_val_if_p_invalid(NULL != pdl && NULL != pdl->locker && NULL != pdl->locker->lock, RET_PARAMETER_INVALID);
    return pdl->locker->lock(pdl->locker);
}

static RetT unlock_dlistDlistManageT *pdl)
{
    return_val_if_p_invalid(NULL != pdl && NULL != pdl->locker && NULL != pdl->locker->unlock, RET_PARAMETER_INVALID);
    return pdl->locker->unlock(pdl->locker);
}
//……
//给双向链表中第i个节点中指向数据(即data)的指针赋值
RetT init_dlist_any_node_data(DlistManageT *pdl, unsigned int i, void *data)
{
    //……
    if (NULL != (pnd = get_dlist_any_node(pdl, i))) {
        lock_dlist(pdl);
        pnd->data   = data;
        pdl->wl++;
        unlock_dlist(pdl);
        return 0;
    }
    return -1;
}

//向双向链表中第i个位置插入一个节点,第i个位置指第i个节点之前, i = len + 1时表示插入到最后一个节点之后
RetT insert_dlist_node(DlistManageT *pdl, unsigned int i, void *data)
{
    //……
     lock_dlist(pdl);
    //寻找插入双向链表中某节点之前的节点
    pndc    = pdl->first;
    for (k = 1; k < i; ++k)
        pndc    = pndc->next;

    //插入节点
    pndi->prev          = pndc->prev;
    pndi->next          = pndc;
    pndc->prev->next    = pndi;
    pndc->prev          = pndi;
    pdl->num++;
    if (1 == k)
        pdl->first = pndi;
    unlock_dlist(pdl);
    return 0;
}

//删除双向链表中的第i个节点
RetT delete_dlist_node(DlistManageT *pdl, unsigned int i)
{
    //……
    lock_dlist(pdl);
    //查找删除的节点
    pnd = pdl->first;
    for (k = 1; k < i; ++k)
        pnd = pnd->next;

    if (1 == k) {
        pdl->first  = pnd->next;
    }
    //删除节点
    pnd->prev->next = pnd->next;
    pnd->next->prev = pnd->prev;
    pnd->prev       = NULL;
    pnd->next       = NULL;
    pnd->data       = NULL;
    pdl->num--;
    free(pnd);
    unlock_dlist(pdl);
    return 0;
}
//……
//获取双向链表中第i个节点的值
void *get_dlist_node_data(DlistManageT *pdl, unsigned int i)
{
    //……
    for (k = 1; k < i; ++k)
        pnd = pnd->next;
    lock_dlist(pdl);
    data    = pnd->data;
    unlock_dlist(pdl);
    return data;
}

去掉原来跟sem_t类型相关的代码。

在双向链表中,所有涉及到线程加锁/解锁的操作都将通过lock_dlist和 unlock_dlist通过来实现(实际上是通过调用了用户在特定平台上的线程加/解锁的程序)。现在将调用者所需编写的函数列出来。

cpthread.c
站在调用者角度基于linux平台上的信号量实现锁接口所要求的回调函数。

/* cpthread.c */
/* 调用POSIX thread库中的函数定义线程线程相关函数 */
//……
#include <semaphore.h>
#include "typedef.h"
#include "cpthread.h"
//……
typedef struct _MtT {    //调用者基于特定的平台(linux)用来对线程加/解锁的方法
    //定义此平台上的某种线程锁 
    sem_t   mutex;
}MtT;

//……
//用户自定义实现的线程加锁函数
static RetT lock_thread(Locker *locker)
{
    //在这里定义你的线程加锁代码
    MtT *pmt = NULL;
    int ret = 0;

    pmt = (MtT*)locker->mt;
    ret = sem_wait(&pmt->mutex);

    return ret == 0 ? RET_OK : RET_FAIL;
}

//用户自定义实现的线程解锁函数
static RetT unlock_thread(Locker *locker)
{
    //在这里定义你的线程解锁代码
    MtT *pmt = NULL;
    int ret = 0;

    pmt = (MtT*)locker->mt;
    ret = sem_post(&pmt->mutex);

    return ret == 0 ? RET_OK : RET_FAIL;
}

//用户自定实现线程锁初始化函数
Locker *create_thread_locker(void)
{
    Locker *locker = NULL;

    locker  = (Locker *)malloc(sizeof(Locker) + sizeof(MtT));
    if (NULL != locker) {
        MtT *pmt = NULL;

        pmt = (MtT *)locker->mt;
        locker->lock    = lock_thread;
        locker->unlock  = unlock_thread;

        sem_init(&pmt->mutex, 0, 1);
    }

    return locker;
}

在cpthread.h中包含locker.h文件并声明create_thread_locker函数。在Makefile中添加locker.h这个条件。

#Makefile
#make命令默认执行make all或者第一条规则
//…...
dlist.o:    dlist.c dlist.h tk.h typedef.h locker.h
    $(CC) $(CFLAG_OBJ) $< $(CFLAGS_POSTFIX)

//......
cpthread.o: cpthread.c cpthread.h typedef.h locker.h
    $(CC) $(CFLAG_OBJ) $< $(CFLAGS_POSTFIX)

//......

双向链表程序通过调用create_thread_locker函数就可以通过用户定义的线程锁程序初始化线程锁,将create_thread_locker函数作为create_dlist函数的参数。

在dlist.h中声明初始化线程锁函数create_thread_locker的类型。

/* dlist.h */
/* 声明dlist.c中数据类型名及函数 */

#ifndef DLIST_H
#define DLIST_H
#include "locker.h"
//……
typedef Locker *    (*pCreateThreadLockerFuncT)(void);
//……
DlistManageT *create_dlist(unsigned int len, pCreateThreadLockerFuncT plocker_init_func);
#endif

dlist.c

/* dlist.c */
/* 定义描述/管理(循环)双向链表的数据,定义(循环)双向链表的接口给 */
//……

//创建含n个节点的双向链表
DlistManageT *create_dlist(unsigned int len, pCreateThreadLockerFuncT plocker_init_func)
{
    //……
   pdl->first->prev    = pndc;
    pndc->next          = pdl->first;

    //双向链表被初始化的元素个数为0
    pdl->wl = 0;

    //初始化双线链表的线程锁
    if (NULL != plocker_init_func)
            pdl->locker = plocker_init_func();

    return pdl;
}

//通过管理双向链表指针释放双向链表及管理双向链表的结构体指针
void free_dlist(DlistManageT *pdl)
{
    //……
    pdl->first = NULL;
    free(pdl->locker);
    free(pdl);
}

main.c

/* main.c */
/* 包含C程序入口 */
#include <stdio.h>
#include "cpthread.h"
#include <assert.h>

int main(void)
{
    DlistManageT    *pdl = NULL;
    unsigned int    dlen = 10;

    assert(NULL != (pdl = create_dlist(dlen, create_thread_locker)));
    create_test_thread(pdl);

    return 0;
}

autotest.c

/* autotest.c */
/* 自动测试程序:
   用于测试dlist.c中所编写的接口
*/
//……
//用正确的参数测试dlist.c中的接口
void init_dlist(DlistManageT **pdl, unsigned int len, pCreateThreadLockerFuncT plocker_init_func)
{
    //……
    assert(NULL != (*pdl = create_dlist(len, plocker_init_func)));
    //…...
}

//……
//自动测试双向链表接口程序
void autotest_dlist_interface(pCreateThreadLockerFuncT plocker_init_func)
{
    //……
    for (i = 1; i <= len; ++i) {
        init_dlist(&pdl, i, plocker_init_func);
        //…...
    }
}

重新声明autotest.c中的函数。

/* autotest.h */
/* 什么autotest.c中的函数 */

#ifndef AUTOTEST_H
#define AUTOTEST_H
#include "dlist.h"

void init_dlist(DlistManageT **pdl, unsigned int len, pCreateThreadLockerFuncT plocker_init_func);
void test_dlist_interface_with_para(DlistManageT **pdl);
void autotest_dlist_interface(pCreateThreadLockerFuncT plocker_init_func);
#endif

去掉cpthread.c中所调用的错误包装函数P()和V()。
在与程序及Makefile同目录的linux终端使用make命令编译程序,运行./all得跟以前一样的结果。不过,设计程序的方法却是大大转变。

(2) 支持单/多线程

如果create_dlist的pCreateThreadLockerFuncT参数为NULL,那么就可以说明双向链表工作于单线程模式。可以根据create_dlist的pCreateThreadLockerFuncT参数来决定是否要调用线程锁接口中的函数指针所指向的用户编写的函数。

/* dlist.c */
/* 定义描述/管理(循环)双向链表的数据,定义(循环)双向链表的接口给 */
//……
//线程加锁
static RetT lock_dlist(DlistManageT *pdl)
{
    return_val_if_p_invalid(NULL != pdl, RET_PARAMETER_INVALID);
    if (NULL != pdl->locker && NULL != pdl->locker->lock)
        return pdl->locker->lock(pdl->locker);
    return RET_FAIL;
}

//线程解锁
static RetT unlock_dlist(DlistManageT *pdl)
{
    return_val_if_p_invalid(NULL != pdl, RET_PARAMETER_INVALID);
    if (NULL != pdl->locker && NULL != pdl->locker->unlock)
        return pdl->locker->unlock(pdl->locker);
    return RET_FAIL;
}
//......

这样,对于单线程的程序来说,在调用线程加/解锁的函数中会多运行几个无效的判断语句。

cpthread.c

/* cpthread.c */
/* 调用POSIX thread库中的函数定义线程线程相关函数 */
//……
#include "autotest.h"
//…...
//双向链表的单线程模式测试 plocker_init_func = NULL
void single_thread_test(DlistManageT **pdl, unsigned int len, pCreateThreadLockerFuncT plocker_init_func)
{
        init_dlist(pdl, len, plocker_init_func);
        test_dlist_interface_with_para(pdl);
        autotest_dlist_interface(plocker_init_func);
}

//双向链表的多线程模式测试
void multi_thread_test(DlistManageT **pdl, unsigned int len, pCreateThreadLockerFuncT plocker_init_func)
{
    assert(NULL != (*pdl = create_dlist(len, plocker_init_func)));
    create_test_thread(*pdl);
}

main.c

/* main.c */
/* 包含C程序入口 */
#include <stdio.h>
#include "cpthread.h"
#include <assert.h>
#include "autotest.h"

int main(void)
{
    DlistManageT    *pdl = NULL;
    unsigned int    dlen = 10;

    single_thread_test(&pdl, dlen, NULL);
    free_dlist(pdl);

    pdl = NULL;
    multi_thread_test(&pdl, dlen, create_thread_locker);
    free_dlist(pdl);
    return 0;
}

(3) 单/多线程不需要链接不同的库,在运行时切换

wt……

(4) 不依赖于特定的平台

这在使用接口的思想来为双向链表实现线程锁时已经实现了这一点(所有跟平台相关的代码都由用户编写)。

读《xtcxyczjh》-Part-VI pnote over.
[2015.10.17-23:03]

你可能感兴趣的:(c,程序设计方法)