【Linux网络编程-4】线程

  • 开辟进程会分配新的地址空间,系统开销高。

    每个进程可以有很多线程,同个进程的线程共享地址空间,共享全局变量和对象,系统开销较低。

  • 头文件

#include <pthread.h>
  • pid类型

    pid类型pthread_t,实质是unsigned long int,一串长长的无符号整数

  • 链接

    要指定pthread共享库

    g++ -o demo demo.cpp -lpthread
    
  • 查看线程

    ps -xH|grep demo
    

(1)线程创建

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

thread: 指向 pthread_t 类型的指针,用于存储新创建的线程的标识符。
attr: 指向 pthread_attr_t 结构的指针,用于指定线程的属性。如果不需要特定属性,可以传递 NULL。(可设置detach)
start_routine: 线程将要执行的主函数。void* func(void*)
arg: 传递给 start_routine 函数的参数。

返回值:
成功时,返回 0。
失败时,返回错误代码。
//开发用的少,一般用于测试,因为主线程有其他事要做不可能等待子线程
int pthread_join(pthread_t thread, void **retval);

thread: 要等待的线程的标识符。
retval(returnvalue): 一个指向 void* 类型的指针的指针。如果线程函数返回了一个值,并且你希望获取这个返回值,可以传递一个非 NULL 的指针给 retval。如果不需要获取返回值,可以传递 NULL。
返回值:

成功时,返回 0。
失败时,返回错误代码。

(2)线程终止

因为在进程的任一线程中exit(),整个进程会结束,所以线程的start_routine函数中不能调用exit()

  • 线程的start_routine函数执行完,自然终止

  • start_routine函数中调用pthread_exit

    void pthread_exit(void *retval);
    //retval通常是一个整数
    
  • 被主进程或其他线程终止

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

void* mainfunc1(void* arg);
void* mainfunc2(void* arg);

int val=0;
int main()
{
    pthread_t pthid1,pthid2;
    if(pthread_create(&pthid1,NULL,mainfunc1,NULL)!=0)
        printf("create thread failed..");
    if(pthread_create(&pthid2,NULL,mainfunc2,NULL)!=0)
        printf("create thread failed..");
    
    printf("pthid1=%lu,pthid2=%lu\n",pthid1,pthid2);
    
    printf("等待子线程退出\n");
    
    pthread_join(pthid1,NULL);
    printf("子线程1退出\n");
    pthread_join(pthid2,NULL);
    printf("子线程2退出\n");
    
    return 0;
}

void* mainfunc1(void* arg)
{
    for(int i=0;i<5;i++)
    {
        val++;
        sleep(1);
        printf("子线程1(%d)\n",val);
    }
    pthread_exit(0);
}

void* mainfunc2(void* arg)
{
    for(int i=0;i<30;i++)
    {
        val++;
        sleep(1);
        printf("子线程2(%d)\n",val);
    }
}

(3)参数传递

​ 不传参数而直接用全局变量时,全局变量被主线程改变,线程也不是一创建就立即运行,运行时可能用到的变量已经被其他线程或主线程改了。

pthread_create的第4个参数。

  • 强转

    指针8字节,int4字节,long8字节。

    只要字节长度保持一致,就可以转。

    //指针可以存数值
    int i=10;
    void* ptr=(void*)(long)i;	//int是4字节,先转long是8字节,再转指针8字节
    int j=(int)(long)ptr;
    
    long i=10;
    void* ptr=(void*)i;
    int j=(long)ptr;
    
    long i=-10;	//可以存负值
    void* ptr=(void*)i;
    int j=(long)ptr;
    
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>


void* mainfunc1(void* arg)
{
    printf("线程%d\n",(int)(long)arg);
    for(int i=0;i<5;i++)
    {
        sleep(1);
    }
    pthread_exit(0);
}

void* mainfunc2(void* arg)
{
    printf("线程%d\n",*((int*)arg));
    for(int i=0;i<5;i++)
    {
        sleep(1);
    }
    pthread_exit(0);
}

void* mainfunc3(void* arg)
{
    printf("线程%d\n",(int)(long)arg);
    for(int i=0;i<5;i++)
    {
        sleep(1);
    }
    pthread_exit(0);
}

void* mainfunc4(void* arg)
{
    printf("线程%d\n",(int)(long)arg);
    for(int i=0;i<5;i++)
    {
        sleep(1);
    }
    pthread_exit(0);
}

void* mainfunc5(void* arg)
{
    printf("线程%d\n",(int)(long)arg);
    for(int i=0;i<5;i++)
    {
        sleep(1);
    }
    pthread_exit(0);
}

int val=1;
int main()
{
    pthread_t pthid1,pthid2,pthid3,pthid4,pthid5;

	if(pthread_create(&pthid1,NULL,mainfunc1,(void*)(long)val)!=0)
        printf("thread create failed..");
    val++;
    
    if(pthread_create(&pthid2,NULL,mainfunc2,&val)!=0)
        printf("thread create failed..");
    val++;
    
    if(pthread_create(&pthid3,NULL,mainfunc3,(void*)(long)val)!=0)
        printf("thread create failed..");
    val++;
    
    if(pthread_create(&pthid4,NULL,mainfunc4,(void*)(long)val)!=0)
        printf("thread create failed..");
    val++;
    
    if(pthread_create(&pthid5,NULL,mainfunc5,(void*)(long)val)!=0)
        printf("thread create failed..");


    pthread_join(pthid1,NULL);
    printf("子线程1退出\n");
    pthread_join(pthid2,NULL);
    printf("子线程2退出\n");
    pthread_join(pthid3,NULL);
    printf("子线程3退出\n");
    pthread_join(pthid4,NULL);
    printf("子线程4退出\n");
    pthread_join(pthid5,NULL);
    printf("子线程5退出\n");

    return 0;
}

(4)线程资源回收

线程有两种状态:joinable(非分离)、unjoinble(分离)。

创建线程默认是joinable。

joinable状态的线程在主函数执行完后(自己退出或调用pthread_exit()退出)不会自己释放内存资源,这种线程称为“僵尸线程”。

回收方法:

  • 主进程pthread_join。(不常用)

    实际中一般不会这么用,因为主进程要干别的,不能阻塞在pthread_join这

  • 创建线程前,把将要创建的线程设为分离

    pthread_create的第2个参数。

    线程设置分离后就join不到了,返回22,不返回0。

    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));
    
  • 创建线程后在主线程中调用pthread_detach(pthid)设置分离

  • 在子线程中调用pthread_detach(pthread_self())设置分离

(5)线程返回状态

如果主线程需要使用子线程的返回值,需要等待子线程执行完毕。

子线程的主函数结束时,通过pthread_join第二个参数获取它的返回状态。

虽然第二个参数类型是void**,但还是指针,也就是8字节,存储据强转就行:

return 0因为在C中NULL就是0,所以不用强转。而return 10需要强转为return (void*)10

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

void* mainfunc1(void* arg);

int main()
{
    pthread_t pthid;
    int ival;
    pthread_create(&pthid,NULL,mainfunc1,NULL);

    pthread_join(pthid,(void**)&ival);
    printf("retval=%d\n",ival);

    return 0;
}

void* mainfunc1(void* arg)
{
    for(int i=0;i<3;i++)
    {
        sleep(1);
        printf("sleep %dsec ok\n",i);
    }
    //pthread_exit(0);
    //return 0;		
    return (void*)10;
}

(6)线程取消

  • 默认:PTHREAD_CANCEL_ENABLE状态,在主线程或其他线程中可调用pthread_cancel取消该线程

    线程被取消后,主线程中通过pthread_join获取的线程的返回值为宏PTHREAD_CANCEL即:-1

    pthread_cancel(pthread_t threadid);
    
  • 子线程中调用pthread_setcancelstate设置pthread_cancel有效与否

    pthread_cancel有效——PTHREAD_CANCEL_ENABLE

    pthread_cancel失效——PTHREAD_CANCEL_DISABLE

    pthread_setcancelstate(int state, int* oldstate);
    //state-要设置的状态
    //oldstate-要保留之前的状态,不想保留就传NULL
    
  • 子线程中调用pthread_setcanceltype设置线程取消的方式

    立即取消,调用pthread_cancel就立即取消——PTHREAD_CANCEL_ASYNCHRONOUS

    延迟取消,线程直到代码执行到取消点才取消——PTHREAD_CANCEL_DEFERRED(意义不大,因为很多函数都是取消点)

  • 取消点

    man pthreads
    
    查看cancellation points
    

    很多常用函数都是取消点,比如:printf()、sleep()fopen()

  • 可通过pthread_testcancel()设置取消点

    #include<stdlib.h>
    #include<stdio.h>
    #include<unistd.h>
    #include<pthread.h>
    
    void* mainfunc(void* arg);
    int main()
    {
        pthread_t pthid;
        if(pthread_create(&pthid,NULL,mainfunc,NULL)!=0)
            printf("create failed..\n");
        
        usleep(100);
        pthread_cancel(pthid);	//取消线程
        
        int iret;
        int ival = pthread_join(pthid,(void**)&iret);	//线程被取消,iret将为-1
        printf("thread(%d) is exit,ret=%d\n",ival,iret);
        
        return 0;
    }
    
    int val=0;
    
    //跑到printf取消,能够执行printf打印
    void* mainfunc(void* arg)
    {
        pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
        for(int i=0;i<5000000;i++)
        {
            val++;
        }
        printf("val=%d\n",val);	//printf是取消点,在这里就取消线程
        pthread_testcancel();
    	pthread_exit(0);
    }
    
    //跑100ms就取消,不会执行到printf语句
    void* mainfunc(void* arg)
    {
        pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);
        for(int i=0;i<5000000;i++)
        {
            val++;
        }
        printf("val=%d\n",val);
        pthread_testcancel();
    	pthread_exit(0);
    }
    

(7)线程清理

子线程退出时的善后工作,释放资源、回滚事务等。不适合放在主函数中,就放在清理函数中。

通过下面的函数,将清理函数压入、弹出堆栈:

pthread_cleanup_push(void(*routine)(void*),void* arg);	//注册清理函数(注意第一个参数函数指针:返回值void)
pthread_cleanup_pop(int execute);	//弹出并执行清理函数
  • 上面2个函数必须成对写在代码块{}里,否则报错。
  • 线程被取消时,按照压栈的相反顺序依次执行所有清理函数
  • 线程被pthread_exit()终止时,也按照上述顺序依次执行所有清理函数
  • 线程调用return返回时,不执行清理函数
  • 传入非0参数到pthread_cleanup_pop中,将会弹出栈顶函数并执行
  • 一般1个清理函数即可
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>


int ival=0;

void cleanfunc1(void* arg)
{
    printf("cleanfunc1..\n");
}

void cleanfunc2(void* arg)
{
    printf("cleanfunc2..\n");
}

void cleanfunc3(void* arg)
{
    printf("cleanfunc3..\n");
}


void* func1(void* arg)
{
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);
    pthread_cleanup_push(cleanfunc1,NULL);
    pthread_cleanup_push(cleanfunc2,NULL);
    pthread_cleanup_push(cleanfunc3,NULL);
    for(int i=0;i<100000000;i++)
    {
        ival++;
        printf("ival=%d\n",ival);
    }
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(1);
    pthread_exit(0);
}


int main(int argc,char** argv)
{
    pthread_t pthid;
    pthread_create(&pthid, NULL, func1, NULL);
    usleep(1000);
    pthread_cancel(pthid);
    pthread_join(pthid,NULL);

    return 0;
}

(8)向线程发信号

和多进程中的信号有区别,多线程中用的比较少,也就用下pthread_kill

  • 外部(其他进程或bash)向进程发送信号,信号到达的是进程,会调用handler,但主进程、任一子线程都不会中断。

  • 多线程程序中,信号捕获关联处理函数的signal()sigaction()的代码放在主线程、任一子线程都一样,都会改变所有线程的handler

    一般放在主进程的函数中。

  • 主进程向子线程发送信号:

    若设置了handler则调用,若没设置handler则整个进程都会退出。

    int pthread_kill(pthread_t threadid,int sig);
    

    和外部发信号不同,这会中断指定的子线程当前执行的代码转而去执行信号处理函数handler,handler执行完后再继续执行子线程。

  • 每个子线程调用pthread_sigmask()阻塞信号

posted @ 2024-07-09 18:53  徘徊彼岸花  阅读(1)  评论(0编辑  收藏  举报