您的位置 首页 golang

十年老司机详解Linux多线程技术上篇(含实例源码,值得收藏)

Linux 线程 实现

Linux系统下的 多线程 遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。Linux下pthread是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似于fork()。

线程创建

函数说明:tidp参数是一个指向线程标识符的指针,当线程创建成功后,用来返回创建的线程ID;attr参数用于指定线程的属性,NULL表示使用默认属性;start_rtn参数为一个函数指针,指向线程创建后要调用的函数,这个函数也称为线程函数;arg参数指向传递给线程函数的参数。

返回值:线程创建成功则返回0,发生错误时返回错误码。

因为pthread不是Linux系统的库,所以在进行编译时要加上-lpthread,例如:

# gcc filename -lpthread

在代码中获得当前线程标识符的函数为:pthread_self()。

例子:

线程退出

void pthread_exit(void * rval_ptr);

函数说明:rval_ptr参数是线程结束时的返回值,可由其他函数如pthread_join()来获取。

如果进程中任何一个线程调用exit()或_exit(),那么整个进程都会终止。线程的正常退出方式有线程从线程函数中返回、线程可以被另一个线程终止以及线程自己调用pthread_exit()函数。

线程等待

在调用pthread_create()函数后,就会运行相关的线程函数了。pthread_join()是一个线程阻塞函数,调用后,则一直等待指定的线程结束才返回函数,被等待线程的资源就会被收回。

int pthread_join(pthread_t tid, void ** rval_ptr);

函数说明:阻塞调用函数,直到指定的线程终止。tid参数是等待退出的线程id;rval_ptr是用户定义的指针,用来存储被等待线程结束时的返回值(该参数不为NULL时)。

例子:

可以看出,pthread_exit(5)实际上就相当于return 5,也就是说,线程函数为run()函数,线程退出就是run()函数运行完。这时候就能明白pthread_join()的真正意义了。

线程函数运行结束是可以有返回值的,这个函数的返回值怎么返回呢?可以通过return语句进行返回,也可以通过pthread_exit()函数进行返回。函数的这个返回值怎么来接收呢?就通过pthread_join()函数来接受。

当然也可以选择不接受该线程的返回值,只阻塞该线程:

pthread_join(tid, NULL);

线程清除

线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;非正常终止是线程在其他线程的干预下,或者由于自身运行错误(比如访问非法地址)而退出,这种退出方式是不可预见的。

不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,如何保证线程终止时能顺利地释放自己所占用的资源,是一个必须考虑和解决的问题。

从pthread_cleanup_push的调用点到pthread_cleanip_pop之间的程序段中的终止动作(包括调用pthread_exit()和异常终止,不包括return)都将执行pthread_cleanup_push()所指定的清理函数。

void pthread_cleanup_push(void (* rtn)(void *), void * arg);

函数说明:将清除函数压入清除栈。rtn是清除函数,arg是清除函数的参数。

void pthread_cleanup_pop(int execute);

函数说明:将清除函数弹出清除栈。执行到pthread_cleanup_pop()时,参数execute决定是否在弹出清除函数的同时执行该函数,execute非0时,执行;execute为0时,不执行。

int pthread_cancel(pthread_t thread);

函数说明:取消线程,该函数在其他线程中调用,用来强行杀死指定的线程。

例子1:

程序运行结果为:

例子2:

也就是说,pthread_exit()用于本线程自己调用,pthread_cancel()用于本线程来终结其他线程。

同时这里也区分一下线程返回的return和pthread_exit:

pthread_exit()用于线程退出,可以指定返回值,以便其他线程通过pthread_join()函数获取该线程的返回值。return,是函数返回,不一定是线程函数哦! 只有线程函数中return,线程才会退出;

pthread_exit()、return都可以用pthread_join()来接收返回值的,也就是说,对于pthread_join()函数来说是没有区别的;

pthread_cleanup_push()所指定的清理函数支持调用pthread_exit()退出线程和异常终止,不支持return;

pthread_exit()为直接杀死/退出当前进程,return则为退出当前函数,但是在g++编译器中,main中的return会被自动优化成exit(),所以在主函数中使用return会退出该进程所有线程的运行;

return会调用局部对象的析构函数,而pthread_exit()不会(线程本来就不建议用pthread_exit()这类方法自杀的,正确的方法是释放所申请的内存后return)。

线程函数传递及修改线程的属性

线程函数参数传递

在函数pthread_create()中,arg参数会被传递到start_rnt线程函数中。其中,线程函数的形参为void *类型,该类型为任意类型的指针。 所以任意一种类型都可以通过地址将数据传送给线程函数中。

例子:

数组作实参时,传入的是数组的首地址,即传入多个相同类型数据的首地址; 结构体 作实参时,传入的是结构体的地址,即传入多个不同数据类型的结构地址。

也就是说,如果线程函数中需要传入多个不同数据类型的参数,但是依照pthread_create()的定义,仅可以传入void *的类型的数据,参数数量为一个。这个时候就需要将不同数据类型的参数封装成一个结构体,将这个结构体的地址传入。

例子:

需要注意一下,线程函数和普通的函数一样,每调用一次,局部变量都会配分一次内存,并且各自之间互不干扰。

线程属性

之前线程创建函数pthread_create()函数的第二个参数都设置为了NULL,也就是说,都是采用的默认的线程属性。对于大多数的程序来说,使用默认属性就够了,但还是有必要来了解一下相关的属性。

属性结构为pthread_attr_t,属性值不能直接设置,必须使用相关的函数进行操作,初始化函数为pthread_attr_init(),这个函数必须在pthread_create()函数调用之前调用。

属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、默认1M的堆栈、与父进程同样级别的优先级。

线程绑定属性

关于绑定属性,涉及到另外一个概念:轻进程(Light Weight Process,LWP)。轻进程可以理解为内核进程,它位于用户层和内核层之间。系统对线程资源的分配和对线程的控制时通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认情况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定。绑定状况下,则顾名思义,即某个线程固定地绑在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

设置线程绑定状态的函数为pthread_attr_setscope,函数原型为:

int pthread_attr_setscope(pthread_attr_t * tattr, int scope);

函数说明:tattr参数为指向属性结构的指针,scope参数为绑定类型,通常有两个取值PTHREAD_SCOPE_SYSTEM(绑定)、PTHREAD_SCOPE_PROCESS(非绑定)。

返回值,pthread_sttr_setscope()成功完成后会返回0,其他任何返回值都表示出现了错误。

例子:

线程分离属性

线程的是否可结合状态决定线程以什么样的方式来终止自己。在任何一个时间点上,线程是可结合的(或非分离的,joinable)或者是分离的(detached)。

可结合属性:创建线程时,线程的默认属性是可结合的, 如果一个可结合线程结束运行但没有被pthread_join(),则它的状态类似于进程中的Zombie(僵死),即它的存储器资源(例如栈)是不释放的,所以创建线程者应该调用pthread_join()来等待线程运结束,并得到线程的退出码,回收其资源;

可分离属性:通过调用pthread_detach()函数该线程的可结合属性将被修改为可分离属性。一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

设置线程是否分离的函数为pthread_attr_setdatachstate(),其原型为:

int pthread_sttr_setdetachstate(pthread_sttr_t * tattr, int detachstate);

函数说明:tattr参数为指向属性结构的指针,detachstate参数为分离类型,通常有两个取值PTHREAD_CREATE_DETACHED(分离)、PTHREAD_CREATE_JOINABLE(非分离、结合)。

返回值,pthread_attr_setdatachstate()成功完成后会返回0,其他任何返回值都表示出现了错误。

例子:

注意,如果使用PTHREAD_CREATE_JOINABLE创建非分离线程(默认),则假设应用程序将等待线程完成。也就是说,在费线程终止后,必须要有一个线程用pthread_join()来等待它,否则就不会释放线程的资源,这将会导致内存泄漏。无论是创建的分离线程还是非分离线程,在所有线程都退出之前,进程都不会退出。

这与进程的wait()函数类似。

线程优先级属性

线程优先级存放在结构sched_param中,设置线程优先级的接口是pthread_attr_setschedparam(),它的完整定义是:

struct sched_param {

int sched_priority;

}

int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param);

例子:

线程的互斥

线程间的互斥是为了避免对共享资源或临界资源的同时使用,从而避免因此而产生的不可预料的后果。临界资源一次只能被一个线程使用。线程互斥关系是由于对共享资源的竞争而产生的间接制约。

互斥锁

假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。互斥锁用来保证一段时间内只有一个线程在执行一段代码,实现了对一个共享资源的访问进行排队等候。互斥锁是通过互斥锁变量来对访问共享资源排队访问。

互斥量

互斥量是pthread_mutex_t类型的变量。互斥量有两种状态:lock(上锁)、unlock(解锁)。

当对一个互斥量加锁后,其他任何试图访问互斥量的线程都会被堵塞,直到当前线程释放互斥锁上的锁。如果释放互斥量上的锁后,有多个堵塞线程,这些线程只能按一定的顺序得到互斥量的访问权限,完成对共享资源的访问后,要对互斥量进行解锁,否则其他线程将一直处于阻塞状态。

操作函数

pthread_mutex_t是锁类型,用来定义互斥锁。

互斥锁的初始化

int pthread_mutex_init(pthread_mutex_t * restrict mutex, const pthread_mutexattr_t * restrict attr);

restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

函数说明:mutex为互斥量,由pthread_mutex_init调用后填写默认值;attr属性通常默认为NULL。

上锁

int pthread_mutex_lock(pthread_mutex_t * mutex);

函数说明:mutex为互斥量。

解锁

int pthread_mutex_unlock(pthread_mutex_t * mutex);

函数说明:mutex为互斥量。

判断是否上锁

int pthread_mutex_trylock(pthread_mutex_t * mutex);

返回值:0表示已上锁,非0表示未上锁。

销毁互斥锁

int pthread_mutex_destory(pthread_mutex_t * mutex);

例子:

这里的互斥量的用处就是在sleep(5)之间的时间内,不会切换到另一个线程的线程函数中,因为已经用互斥量锁定了。

自旋锁

自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁不同之处在于:当自旋锁尝试获取锁时以忙等待的形式不断地循环检查锁是否可用。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

自旋锁和互斥锁的区别

从实现原理上来讲,互斥锁属于sleep-waiting类型的锁,而自旋锁属于busy-waiting类型的锁。也就是说:pthread_mutex_lock()操作,如果没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该互斥锁的等待队列里;而pthread_spin_lock()则可以理解为,在一个while(1)循环中用内嵌的汇编代码实现的锁操作(在linux内核中pthread_spin_lock()操作只需要两条CPU指令,unlock()解锁操作只用一条指令就可以完成)。

对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间;

对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。

因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

操作函数

pthread_spinlock_t是锁类型,用来定义自旋锁。

自旋锁的初始化

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

自旋锁的销毁

int pthread_spin_destroy(pthread_spinlock_t *lock);

上锁

int pthread_spin_lock(pthread_spinlock_t *lock);

判断是否上锁

int pthread_spin_trylock(pthread_spinlock_t *lock);

解锁

int pthread_spin_unlock(pthread_spinlock_t *lock);

C++实现自旋锁

C++11提供了对 原子操作 的支持,其中std::atomic是标准库提供的一个原子类模板。

对于lock函数,需要CAS的原子操作,可以使用std::atomic类模板的成员函数compare_exchange_strong();

对于unlock函数,可以使用std::atomic类模板的成员函数store来以原子操作的方式将flag置false。

未完待续……

需要C/C++ Linux服务器开发学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

文章来源:智云一二三科技

文章标题:十年老司机详解Linux多线程技术上篇(含实例源码,值得收藏)

文章地址:https://www.zhihuclub.com/98415.shtml

关于作者: 智云科技

热门文章

网站地图