1.前言2.glib介绍3.线程库设计4.总结1.前言在Linux和Windows平台上,如何设计线程库。Linux平台:Windows平台:最近写了几篇关于跨平台应用设计思路的文章。有小伙伴在后台留言询问一些通用的跨平台库。看来这方面的需求还是很多的。所谓跨平台,无非是希望可以用相同的应用代码编译出运行在多个平台上的可执行程序。那么我们如何才能使应用程序代码平台无关呢?很显然,我们中间需要一个桥接层,把那些我们不想处理的平台相关的、烦人的代码丢给这个中间层去处理。简单的说:那些和平台相关的又脏又累的事情,都是这个中间层给你做的。我们在写应用的时候,只需要关心自己业务层的事务。如果没有这个中间层,您的代码可能会充满大量#if...#else代码。而glib就是这样一个中级跨平台库,它提供了很多常用的包,thread库只是其中的一个包。在这篇文章中,我们主要学习glib是如何设计一个跨平台线程库的。2.glib简介乍一看,很容易混淆glib和glibc。它们都是基于GPL的开源软件,但却是完全不同的概念。glibc是GNU实现的一套标准C函数库,glib是gtk+的一套函数库。那么什么是gtk+?使用Linux的朋友一定知道gnome的桌面环境。Gnome是基于gtk+开发的桌面系统,glib是gtk背后的幕后英雄。glib可以在多种平台上使用,例如Linux、Unix、Windows等。glib提供了许多标准的、常用的C语言结构的替代品。作为C语言开发者,有时候我们很羡慕C++开发者。标准库(SDL)有很多可用的工具:链表、向量和字符串处理。..但是C语言呢?你必须在任何地方自己实现这些轮子。但另一方面,如果我们在日常的开发过程中把自己写的和从其他地方借来的有用的轮子都积累起来,形成自己的“宝库”,这也是一种经验的体现,也是一种竞争力。今天github上也有很多雷锋实现的优质C库:有的专注于跨平台,有的专注于某个领域(比如:网络处理,格式化文本解析)。glib在解决跨平台的同时,还提供了很多其他有用的工具箱,比如:事件循环、线程池、同步队列、内存管理等,既然提供了很多功能,势必会导致体积比较大。这就是为什么很多开发者在面对不同的选择时放弃glib的原因。不管怎样,glib这么强大,我们可以学习一下它的设计思想,这比盲目敲几千行代码更能提升一个人的元技能!三、线程库设计1、线程相关的文件在Linux系统中,一般通过POSIX接口(可移植操作系统接口)创建线程,例如:线程创建API函数为pthread_create(...)。在Windows系统中,线程的创建有以下几种方式:CreateThread()_beginthread()由于glib库是专门用来解决跨平台问题的,所以面对应用层程序时必须提供统一的接口;当面对不同的操作系统时,调用不同系统中的线程函数。glib将这些线程相关的操作封装在平台相关的代码中,具体如下图所示:Linux系统:gthread.c、gthread_posix.c参与编译生成glib库;Windows系统:gthread.c、gthread_win32.c参与编译,生成glib库;关于这种跨平台的文件构造方式(也就是编译),建议你看一下这篇短文:跨平台代码的3种组织方式2.数据结构你一定听说过这个公式:program=datastructure+算法。对于一个C语言的项目来说,理解数据结构的设计对于理解整个程序的思路是非常重要的,在glib中也是一样的。在设计线程库时,glib分为两个层次:平台无关部分和平台相关部分。有平台无关的数据结构(删除一些不影响理解的代码):struct_GThread{GThreadFuncfunc;gpointerdata;gbooleanjoinable;};typedefstruct_GThreadGThread;struct_GRealThread{GThreadthread;gintref_count;gchar*name;};typedefstruct_GRealThreadGRealThread;平台相关数据结构是:Linux系统:typedefstruct{GRealThreadthread;pthread_tsystem_thread;gbooleanjoined;GMutexlock;void*(*proxy)(void*);constGThreadSchedulerSettings*scheduler_settings;}GThreadPosix;Windows系统:typedefstruct{GRealThreadthread;GThreadFuncproxy;HANDLEhandle3;你在每个结构体的第一个成员变量中找到了什么吗?从层级关系来看,这些结构之间的关系是:Linux平台:Windows平台:该结构在内存模型中是什么意思?占用一块内存空间。而这些数据结构都是把“子”结构放在“父”结构的第一个位置,这样就可以方便的进行强制类型转换。在上面的内存模型中,GRealThread结构体的开头部分是GThread,所以可以将GRealThread所在内存的起始部分作为GThread结构体变量进行操作。在C++中用面向对象的术语来描述它更准确:基类指针可以指向派生类对象。在下面的代码中,您可以看到实际效果。3、线程创建(1)函数原型平台无关函数(在gthread.c中实现)GThread*g_thread_new(constgchar*name,GThreadFuncfunc,gpointerdata);GThread*g_thread_new_internal(constgchar*name,GThreadFuncproxy,GThreadFuncfunc,gpointerdata,gsizestack_size,constGThreadSchedulerSettings*scheduler_settings,GError**error);平台相关函数(在gthread_posix.c或ghread_win32.c中实现)GRealThread*g_system_thread_new(GThreadFuncproxy,gulongstack_size,constGThreadSchedulerSettings*scheduler_settings,constchar*name,GThreadFuncfunc,gpointerdata,GError);(**error2)Linux平台函数调用链看看Linux平台上的函数调用关系:如果手边有源码,请注意g_thread_new()函数中的两个参数func和data。func是一开始从用户层传入的线程执行函数,也就是用户创建这个线程后想要执行的函数。data是func函数接收的函数参数。如果是直接在Linux操作系统上编程,在调用POSIX接口函数pthread_create()时,通常直接传入用户要执行的函数和参数。但是glib层并没有直接将用户层的功能交给Linux操作系统,而是自己提供了两个线程代理功能。调用pthread_create()时,根据不同情况,传递两个代理函数之一。对于操作系统:第一个线程代理函数:g_thread_proxy();第二个线程代理函数:linux_pthread_proxy();至于传递给哪个代理函数,要看宏定义HAVE_SYS_SCHED_GETATTR是否有效。下面是g_system_thread_new()函数的简化代码:g_system_thread_new(proxy,stack_size,scheduler_settings,name,func,data,error);GThreadPosix*thread;GRealThread*base_thread;//填充base_thread字段,重点关注下面两个语句base_thread->thread.func=func;base_thread->thread.data=data;thread->scheduler_settings=scheduler_settings;thread->proxy=proxy;#ifdefined(HAVE_SYS_SCHED_GETATTR)ret=pthread_create(&thread->system_thread,&attr,linux_pthread_proxy,线);#elseret=pthread_create(&thread->system_thread,&attr,(void*(*)(void*))proxy,thread);#endif4.对于线程执行,我们假设宏定义HAVE_SYS_SCHED_GETATTR已定义且有效。Linux系统中的pthread_create()接收到linux_pthread_proxy()函数。当新创建的线程被调度执行时,会调用linux_pthread_proxy()函数并执行:简化的linux_pthread_proxy()函数:staticvoid*linux_pthread_proxy(void*data){//data是g_system_thread_new中的GThreadPosix类型指针,是platform-依赖。GThreadPosix*thread=data;if(thread->scheduler_settings){//设置线程属性tid=(pid_t)syscall(SYS_gettid);res=syscall(SYS_sched_setattr,tid,thread->scheduler_settings->attr,flags);}//调用glib中的线程代理函数,其实就是g_thread_proxy()returnthread->proxy(data);}这个函数主要关注三点:data参数:是g_system_thread_new函数中的GThreadPosix类型指针,与平台相关.中间部分是设置线程属性;最后一个return语句调用了glib中的第一个线程代理函数g_thread_proxy。继续贴这个函数的简化代码:gpointerg_thread_proxy(gpointerdata){//data是g_system_thread_new中的GThreadPosix类型指针,是平台相关的。//这里强制为平台无关的GRealThread类型。GRealThread*thread=data;if(thread->name){//设置线程属性:nameg_system_thread_set_name(thread->name);}//调用应用层的线程入口函数thread->retval=thread->thread.func(thread->thread.data);returnNULL;}这个函数只需要注意三点:data参数:linux_pthread_proxy函数传递的是一个GThreadPosix类型的指针,但是这里直接赋值给了一个GRealThread类型的指针,因为他们的内存模型是Included关系;中间部分是设置线程名称;最后的thread->thread.func(thread->thread.data)语句调用的是一开始用户传入的函数,并传递了用户的data参数。至此可以执行用户层定义的线程函数user_thread_func(data)。那么,如果glib层没有定义宏HAVE_SYS_SCHED_GETATTR,那么Linux系统中的pthread_create()接收glib中的第一个线程代理函数g_thread_proxy。线程执行的调用关系为:5.Windows平台函数调用链首先看Windows平台创建线程时的函数调用关系:在Windows平台,glib的线程代理函数为g_thread_win32_proxy()。当新创建的线程被调度执行时,函数调用关系为:4.总结实现这样的线程函数代理设计,关键是利用C语言中的结构类型,将“父”结构类型变量强行写入使用“子”结构类型变量是因为它们在内存模型开始的空间中完全相同。最后,我将文章中的这些图片合并,绘制了如下两张图,充分体现了glib中的线程设计思想:Linux平台:Windows平台:本文转载自微信公众号《IOT物联网小镇》,你可以通过以下二维码关注。转载本文请联系物联小镇公众号。
