多线程基础(Linux)

1.线程创建

1.函数声明

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

其中,thread用来接收线程标识符的地址,我们可以传入一个变量的地址,让它来接收这个值。

attr指向一个结构体,这个是需要我们传入,这个结构体存储了线程的属性,例如优先级等。

start_routine是线程运行的函数地址,通常填写这个函数的名字即可。

arg是输入线程运行函数的参数,这部分就可以好好说道说道了。

当线程正常创建的时候,这个函数会正常返回0。

我们想要简单地创建一个线程,这么写就可以了

pthread_create(&pthid,NULL,pthmain,NULL);

2.void*指针

在介绍传入参数之前,我先来简单介绍一下void*指针。

首先我们应该都知道,指针指向的就是一个地址。那么这个地址后有多少内存是属于它的呢?int指针告诉了后面的4个字节都是它的,double、long long指针告诉了后面有8个字节是属于它的,你自己定义了一个结构体Node,哪怕是Node的指针,编译器也能知道后面有多少内存是属于它的。

但是void指针就不一样了,它并不指明后面有多少内存属于它。这就要求了我们在使用一个void指针指向的地址时,需要先强制转化成我们所需的类型,或者说void所指的真正的数据类型。这样,对于不了解内部的外界,就没法轻易进行修改,起到了安全的作用。.

还可以参考这个文章万能指针void*指针

3.参数传入

如果传入单个值的话,比如我们要传入一个int类型的值,我们可以不用传入它的地址,而且将它的值作为地址传入

比如这样:

int main(){
	int yzz = 100,pid;
	void *ptr = (void *)(long)yzz;//int是4字节,指针是8字节,可以先转换一下成同为8字节的long
	pthread_create(&pid,NULL,pmain,ptr);
	return 0;
}
void *pmain(void *arg){
	cout<<(int)(long)ptr<<endl;
	return (void *)0;
}

这样直接传值的话,可以保证我们使用这个值不会被其他线程修改线程是共享一个进程下的内存空间的,而如果使用全局变量,或者是传入地址的话,如果不采取其他措施,就很可能被其他线程修改了。

当然,如果要传入多个值的话,我们可以使用一个结构体来保存,但是指针只有8字节,结构体都会比这个大,我们只能传入地址了,要想当前线程在使用这个结构体的时候,由于线程存在异步性,为了不被其他线程修改,我们就只能使用加锁这些额外措施了。


2.线程终止

线程要终止,不能使用exit(0),否则整个程序都会终止。主要有三种方法来终止一个线程。

1.线程的"主函数"结束

我们在使用pthread_create()创建线程的时候,有一个填写start_routine函数入口的位置,我暂且称为线程的"主函数",当这个函数start_routine函数运行完以后,这个线程也随之消亡。

2.线程内自行终止

这里使用到一个函数,通常我们都在线程里面这么写:

pthread_exit(0);

可以暂且理解成exit(0)的效果,区别就在于exit(0)会终止整个程序,而pthread_exit(0)只会终止当前线程。

3.主进程或其他线程终止

由其他线程,或者主进程来终止这个线程,这里暂不细说。

就比如当主进程结束了以后,整个程序的资源都会被操作系统回收,这时候哪怕你的其他线程还没运行完,也会被迫终止。

还有其他措施之后会说。


3.线程资源回收

线程有joinable和unjoinable两种状态,默认是joinable状态,此时线程不管是运行完自己的线程主函数还是调用pthread_exit(0)终止,都不会释放内存和其他占有资源类似于僵尸进程

有四种方法来避免系统中存在僵尸线程。

1.pthread_join()函数——阻塞

我们可以在主进程中使用如下函数来等待线程结束,回收资源

pthread_join(pid,NULL);

但是这是一个阻塞函数,如果pid线程号对应的线程没有结束,那么主进程就会阻塞在此,如果用于网络通信的客户端里面,负责监听的主进程要阻塞等待线程完成通信,那当然是不可以的。

2.修改线程属性——非阻塞

我们之前说过,pthread_create()函数的第二个参数是存储线程属性的结构体,我们可以传入一个结构体,将线程设置为detached,这样线程终止后资源也会被正常回收了。

这部分代码有一点点多,直接上代码了

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);  // 设置线程的属性。
pthread_create(&pthid,&attr,pth_main,(void*)((long)TcpServer.m_clientfd);

3.在"主进程"中设置——非阻塞

这个小标题可能带有一点误导性,我要在此声明,通常情况我是在主进程里面创建的线程,所以也在主进程中将线程设置为detached状态。

当然如果你是在其他的函数中创建的话,那么就在创建它的函数那里设置。

简言之,在哪创建的新线程,就在哪设置

在创建之后调用:

pthread_detach(pid);

即可对pid对应的线程完成设置。

4.在线程内部设置——非阻塞

在线程主函数中调用如下函数,线程即可完成对自身的设置:

pthread_detach(pthread_self());

5.小总结

第一个措施需要主进程阻塞,这在网络通信里面多半是不用的,当然肯定也有适用它的地方。

而第二个,要传入结构体,代码量比较多。结构体里面其实可以有多个属性可以设置,我认为单单设置一个属性大可不必特地传入一个结构体。

第三第四个措施都比较简洁,对我这种入门的而言应该最好用了。


4.线程查看

在Linux命令行用如下两个命令其一即可。

top -H
ps -xH

5.线程的返回值

平常,我们的主函数有return 0,而线程主函数同样可以有返回值,我们用pthread_join()函数去接收

pthread_join()第一个参数是传入线程号,第二个参数是一个二级指针,用于接收返回值,不管是几级指针,始终都还是指针,我们把它当成一个普通指针去进行类型转化即可。

pthread_exit()和return都可以返回值,这里也比较容易,我直接上两个代码进行演示,演示一下返回普通整数即可。

int main(){     
        int ans;
        pthread_t pid;
        pthread_create(&pid,NULL,pth_main,NULL);
        pthread_join(pid,(void **)&ans);
        cout<<"ans="<<ans<<endl;
        return 0;
}
void *pth_main(void *arg){
    for(int i=0;i<5;i++)printf("%d\n",i*10);                                                                                            
    pthread_exit((void *)99);
    //return (void *)99;两种都可以
}

结果都是一样的:
在这里插入图片描述


6.线程取消

1.pthread_cancel()函数

pthread_cancel(pid);

pid即是你需要取消的线程号,在主进程或者其他进程里面调用都可以,调用后取消对应线程。

取消线程后,如果用pthread_join()接收线程返回值,(第二个参数)收到的线程返回值是PTHREAD_CANCELED,实际值为-1。

2.pthread_setcancelstate() 函数

pthread_setcancelstate(int state,int *oldstate);

这个函数在子线程内调用,可以设置该线程接收到取消信号的时候作出的响应,只有两种,取消PTHREAD_CANCEL_ENABLE和不取消和PTHREAD_CANCEL_DISABLE

不设置的话,线程默认是收到信号就会取消执行,如果设置了不取消的话,那么它收到信号也不会取消。

这个函数的第二个参数是用于传参接收设置前的状态的,如果你不需要知道的话,填NULL即可。

3.pthread_setcanceltype()函数

pthread_setcanceltype(int type,int *oldtype);

这个参数可以设置如果线程执行取消,是立即取消PTHREAD_CANCEL_DEFFERED还是延迟取消PTHREAD_CANCEL_ASYCHRONOUS。

关于延迟取消,有取消点,基本IO函数等或者其他会阻塞的函数,都是取消点,例如recv()函数什么的,当走到最近一个取消点的时候,线程就会取消。

使用pthread_testcancel()函数,可以把调用这个函数的地方设置为取消点。

不过延迟取消这里我并没有去了解什么,想了解的另外去搜吧。


7.线程清理

一般线程结束以后,需要释放资源、释放锁、回滚事务(这个我暂时不知道是什么)等操作,一般不写在线程的主函数里,而是写在线程清理函数里

清理函数必须成对出现

void pthread_cleanup_push(void(*rountine)(void *),void *arg);///一般写在线程主函数开头
void pthread_cleanup_pop(int execute);///一般写在线程主函数结尾

pthread_cleanup_push的两个参数分别是需要传入的执行清理函数,第二个参数是传给执行清理函数的值。这里其实和pthread_create()传参是非常相似的,大家类比一下即可,区别就在于线程主函数有void*返回值而执行清理函数并没有返回值。

当线程被取消或者调用pthread_exit()的时候,就会调用清理函数。调用顺序与栈一致。


8.向线程发送信号

1.外部使用kill/killall命令向进程发送信号(比如SIGINT)的时候,会遍历所有线程(一般从主线程开始找),找到第一个不会阻塞该信号的线程,然后由这个线程来处理

2.在任何一个线程中使用signal()函数,会改变整个进程中所有线程对这个信号的处理方式

int pthread_kill(thread_t tid, int sig)

可以通过这个函数向指定tid的线程发送信号,tid对应的线程收到了信号后去执行其他函数(并非终止)后,会等其他线程都执行完,最后再来执行tid对应线程的剩下语句

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值