C与多线程
参考博客
https://blog.csdn.net/networkhunter/article/details/100218945
第一个实例
#include <stdio.h>
#include <pthread.h>
void* thread( void *arg )
{
//线程实现
printf( "This is a thread and arg = %d.\n", *(int*)arg);
*(int*)arg = 12;
return arg;
}
int main( int argc, char *argv[] )
{
//线程句柄
pthread_t th;
int ret;
int arg = 10;
int *thread_ret = NULL;
/*
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
函数参数:
1. 线程句柄 thread:当一个新的线程调用成功之后,就会通过这个参数将线程的句柄返回给调用者,以便对这个线程进行管理。
2. 线程属性 attr: pthread_create()接口的第二个参数用于设置线程的属性。这个参数是可选的,当不需要修改线程的默认属性时,
给它传递NULL就行。具体线程有那些属性,我们后面再做介绍。
3. 入口函数 start_routine(): 当你的程序调用了这个接口之后,就会产生一个线程,而这个线程的入口函数就是start_routine()。
如果线程创建成功,这个接口会返回0。
4. 入口函数参数 *arg : start_routine()函数有一个参数,这个参数就是pthread_create的最后一个参数arg。
这种设计可以在线程创建之前就帮它准备好一些专有数据,最典型的用法就是使用C++编程时的this指针。
start_routine()有一个返回值,这个返回值可以通过pthread_join()接口获得。
*/
// 线程句柄,设置属性,入口函数,参数
ret = pthread_create( &th, NULL, thread, &arg );
if( ret != 0 ){
printf( "Create thread error!\n");
return -1;
}
printf( "主函数执行.\n" );
//可通过pthread_join获取线程返回值
/*
因为函数参数是按值传递的,所以要想改变变量,必须传递地址
pthread_join会阻塞主进程的执行
*/
pthread_join( th, (void**)&thread_ret );
printf( "线程返回 = %d.\n", *thread_ret );
return 0;
}
执行结果
线程的分离与合并
跟内存分配类似,在动态分配了内存后须释放内存free或delete,若未进行释放,会导致内存泄漏
若创建的线程未进行回收,否则会产生资源泄漏
线程合并
线程的合并是一种主动回收线程资源的方案。
当一个进程或线程调用了针对其它线程的 pthread_join() 接口,就是线程合并了。
这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。
当被合并线程结束,pthread_join()接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。
上面第一个例子就是使用的线程合并
线程分离
与线程合并相对应的另外一种线程资源回收机制是线程分离,调用接口是pthread_detach()。
线程分离是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。
因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得pthread_detach()接口只要拥有一个参数就行了,那就是被分离线程句柄。
线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕。
线程的属性
线程的初始化与销毁函数
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destory(pthread_attr_t *attr);
线程拥有哪些属性呢?
一般地,Linux下的线程有:绑定属性、分离属性、调度属性、堆栈大小属性和满占警戒区大小属性。
绑定属性
轻进程和Linux系统的内核线程拥有相同的概念,属于内核的调度实体。一个轻进程可以控制一个或多个线程。
在计算机操作系统中,轻量级进程(LWP)是一种实现多任务的方法。与普通进程相比,LWP与其他进程共享所有(或大部分)它的逻辑地址空间和系统资源;与线程相比,LWP有它自己的进程标识符,并和其他进程有着父子关系;这是和类Unix操作系统的系统调用vfork()生成的进程一样的。另外,线程既可由应用程序管理,又可由内核管理,而LWP只能由内核管理并像普通进程一样被调度。Linux内核是支持LWP的典型例子。
绑定属性规定当前线程是否绑定轻进程,被绑定的线程具有较高的相应速度,因为操作系统的调度主体是轻进程,绑定线程可以保证在需要的时候它总有一个轻进程可用。绑定属性就是干这个用的。
函数定义如下:
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
参数解析
-
第一个就是线程属性对象的指针
-
第二个就是绑定类型
- PTHREAD_SCOPE_SYSTEM(绑定的)
- PTHREAD_SCOPE_PROCESS(非绑定的)
分离属性
前面说过线程能够被合并和分离,分离属性就是让线程在创建之前就决定它应该是分离的。
若设置了此属性,就没有必要使用pthread_join()或pthread_detach()来回收资源了
函数定义如下:
pthread_attr_setdetachstat(pthread_attr_t *attr, int detachstate);
参数解析
-
第一个就是线程属性对象的指针
-
第二个参数是否分离
- PTHREAD_CREATE_DETACHED(分离的)
- PTHREAD_CREATE_JOINABLE(可合并的,也是默认属性)
调度属性
线程的调度属性有三个,分别是:算法、优先级和继承权
Linux提供的线程调度算法有三个:时间片轮转、先到先服务和其它
函数定义如下:
pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
参数解析
-
第二个参数决定调度算法
- SCHED_RR(轮询)
- SCHED_FIFO(先进先出)
- SCHED_OTHER(其它)
堆栈大小属性
虽然同一个进程的线程之间是共享内存空间的,但是它的局部变量确并不共享。原因就是局部变量存储在堆栈中,而不同的线程拥有不同的堆栈。Linux系统为每个线程默认分配了8MB的堆栈空间,如果觉得这个空间不够用,可以通过修改线程的堆栈大小属性进行扩容。
函数定义如下:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
参数解析
它的第二个参数就是堆栈大小了,以字节为单位
需要注意的是,线程堆栈不能小于16KB,而且尽量按4KB(32位系统)或2MB(64位系统)的整数倍分配,也就是内存页面大小的整数倍。
满栈警戒区属性
既然线程是有堆栈的,而且还有大小限制,那么就一定会出现将堆栈用满的情况。线程的堆栈用满是非常危险的事情,因为这可能会导致对内核空间的破坏,一旦被有心人士所利用,后果也不堪设想。为了防治这类事情的发生,Linux为线程堆栈设置了一个满栈警戒区。这个区域一般就是一个页面,属于线程堆栈的一个扩展区域。一旦有代码访问了这个区域,就会发出SIGSEGV信号进行通知。
虽然满栈警戒区可以起到安全作用,但是也有弊病,就是会白白浪费掉内存空间,对于内存紧张的系统会使系统变得很慢。所有就有了关闭这个警戒区的需求。同时,如果我们修改了线程堆栈的大小,那么系统会认为我们会自己管理堆栈,也会将警戒区取消掉,如果有需要就要开启它。
函数定义:
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
参数分析
它的第二个参数就是警戒区大小了,以字节为单位。与设置线程堆栈大小属性相仿,应该尽量按照4KB或2MB的整数倍来分配。当设置警戒区大小为0时,就关闭了这个警戒区。
虽然栈满警戒区需要浪费掉一点内存,但是能够极大的提高安全性,所以这点损失是值得的。而且一旦修改了线程堆栈的大小,一定要记得同时设置这个警戒区。
线程的同步
Linux提供的线程同步机制主要有互斥锁和条件变量。
互斥锁
首先我们看一下互斥锁。
所谓的互斥就是线程之间互相排斥,获得资源的线程排斥其它没有获得资源的线程。
Linux使用互斥锁来实现这种机制。
既然叫锁,就有加锁和解锁的概念。当线程获得了加锁的资格,那么它将独享这个锁,其它线程一旦试图去碰触这个锁就立即被系统“拍晕”。
当加锁的线程解开并放弃了这个锁之后,那些被“拍晕”的线程会被系统唤醒,然后继续去争抢这个锁。至于谁能抢到,只有天知道。
但是总有一个能抢到。于是其它来凑热闹的线程又被系统给“拍晕”了……如此反复。感觉线程的“头”很痛:)
从互斥锁的这种行为看,线程加锁和解锁之间的代码相当于一个独木桥,同一时刻只有一个线程能执行。
从全局上看,在这个地方,所有并行运行的线程都变成了排队运行了。比较专业的叫法是同步执行,这段代码区域叫临界区。同步执行就破坏了线程并行性的初衷了,临界区越大破坏得越厉害。
所以在实际应用中,应该尽量避免有临界区出现。实在不行,临界区也要尽量的小。如果连缩小临界区都做不到,那还使用多线程干嘛?
函数定义
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); //初始化锁
int pthread_mutex_destory(pthread_mutex_t *mutex ); //销毁锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
phtread_mutex_trylock()比较特别,用它试图加锁的线程永远都不会被系统“拍晕”,只是通过返回EBUSY来告诉程序员这个锁已经有人用了。至于是否继续“强闯”临界区,则由程序员决定。系统提供这个接口的目的可不是让线程“强闯”临界区的。它的根本目的还是为了提高并行性,留着这个线程去干点其它有意义的事情。当然,如果很幸运恰巧这个时候还没有人拥有这把锁,那么自然也会取得临界区的使用权。
测试用例:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t g_mutex;
int g_lock_var = 0;
struct MyStruct
{
int age;
char name[256];
};
void* thread1( void *arg )
{
int i, ret;
time_t end_time;
char *test_str = (char*)arg;
//设置十秒
end_time = time(NULL) + 10;
printf("%s\n",test_str);
while( time(NULL) < end_time ) {
//加锁 ,进行下面的操作
ret = pthread_mutex_trylock( &g_mutex );
//EBUSY资源占用
if( EBUSY == ret ) {
printf( "thread1: the varible is locked by thread2.\n" );
} else {
printf( "thread1: lock the variable!\n" );
++g_lock_var;
//解锁
pthread_mutex_unlock( &g_mutex );
}
sleep(1);
}
return NULL;
}
void* thread2( void *arg )
{
struct MyStruct *ms = (struct MyStruct *)arg;
printf("MyStruct: age:%d name:%s \n",ms->age,ms->name);
int i;
time_t end_time;
end_time = time(NULL) + 10;
while( time(NULL) < end_time ) {
//加锁
pthread_mutex_lock( &g_mutex );
printf( "thread2: lock the variable!\n" );
++g_lock_var;
sleep(1);
pthread_mutex_unlock( &g_mutex );
}
return NULL;
}
int main( int argc, char *argv[] )
{
int i;
pthread_t pth1,pth2;
char test_str[256] = "Hello World";
struct MyStruct ms;
ms.age = 18;
snprintf(ms.name,255,"张三");
//初始化锁
pthread_mutex_init( &g_mutex, NULL );
pthread_create( &pth1, NULL, thread1, (void *)test_str );
pthread_create( &pth2, NULL, thread2, (void *)&ms );
pthread_join( pth1, NULL );
pthread_join( pth2, NULL );
//销毁锁
pthread_mutex_destroy( &g_mutex );
printf( "g_lock_var = %d\n", g_lock_var );
return 0;
}