【C/C++】多线程从入门到进阶

一、多线程基础

(一)创建多线程

1.函数接口

//函数原型:
int phread_create(pthread_t *thread, const phtread_attr_t *attr, 
		void *(*start_routine)(void *), void *arg)

//函数参数:
·thread: 线程ID,pthread_t

接口参数:
    thread:线程ID,通过 pthread_t 定义。
    attr:线程属性,可以为线程设置各种属性,详情见附录。默认设置为NULL,表示使用默认的属性,即主子线程之间是可接合的。
    start_routine:子线程函数,必须是 void *func(void *)
    arg:子线程函数的参数,必须是 void *,如果需要其他类型,在引入参数时强转类型即可。

代码实例:

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

void *child_thread(void *arg)
{
    pthread_t ccid = pthread_self();
    printf("child_thread ccid:%d\n", ccid);

    int vec[5] = {1, 2, 3, 4, 5};
    for(int i=0; i<5; i++)
        printf("%d\t", i);
    printf("\n");

    return arg;
}

int main()
{
	printf("main thread!\n");
	
    //1.创建子线程
    pthread_t cid;
    if(pthread_create(&cid, NULL, child_thread, (void *)"i am coming!") != 0)
        perror("create thread failed!\n");
        
    //2.打印父子线程的线程ID
    pthread_t pid = pthread_self();
    printf("pid:%d; cid = %d\n", pid, cid);

    //3.等待子线程退出并获取子线程的返回值
    void *cret = NULL;
    pthread_join(cid, &cret);
    printf("cret: %s\n", (char *)cret);

    printf("main thread over!\n");
    return 0;
}

输出结果:
在这里插入图片描述

2.代码分析

    第一步:代码创建了线程,第二个参数 attr 线程属性设置为 NULL,表示子线程与主线程是可接合的,子线程的资源由主线程回收。最后一个参数调用者使用的是 const char*类型所以要强制转换为void*类型。
    第三步:多线程一个最重要的问题就是 保证主线程的生存期一定要大于或等于子线程的生存期(以return为标志)
                  那么如何保证主线程的生存期大于子线程呢?大家最先想到的就是 sleep() 函数,通过让主线程睡眠几秒,然后在这几秒内让子线程运行完毕。那么在这里是否能用 sleep() 函数延迟主线程的生存期呢?答案是可以的。

int main()
{
    printf("main thread!\n");
    //1.创建子线程
    pthread_t cid;
    if(pthread_create(&cid, NULL, child_thread, (void *)"i am coming!") != 0)
        perror("create thread failed!\n");
    //2.打印父子线程的线程ID
    pthread_t pid = pthread_self();
    printf("pid:%d; cid = %d\n", pid, cid);

    //3.等待子线程退出或者说等待其运行完毕
    sleep(1);
    printf("main thread over!\n");

    return 0;
}

输出结果:
在这里插入图片描述
    可以看到,这里用sleep函数一样实现了子线程的任务。

3.pthread_join()的使用

(1)pthread_join()的第一个作用:阻塞等待

    除了sleep之外, #include <pthread.h> 中提供一种标准的,专业的用于阻塞等待子线程退出的函数接口,就是我们本文的重点之一,pthread_join() 函数。

int phtread_join(pthread_t thread, void **retval)

接口参数:
        pthread:子线程ID
        retval:存储线程退出值的内存的指针

接口解析:
    pthread_join()函数指定的线程如果还在运行,就会阻塞等待。这种功能就决定了其可以代替sleep()函数成为标准的线程等待函数。
    那么此时就有个疑问:sleep()既然能代替pthread_join()那还要这个接口干什么?
    由此推测:pthread_join()必定还有其他的作用。

(2)pthread_join()的第二个作用:获取子线程返回值

    pthread_join()函数第二个参数可以获取子线程的返回值,换句话说,可接合的主子线程之间是可以传递变量出来的。就如我们例子的第三步那样,通过pthread_join()函数将子线程的参数 void *arg 传递出来赋值给 void *cret
在这里插入图片描述
    注意,pthread_join()不可以获取子线程函数内的局部变量,换句话说,子线程不可以返回局部变量,因为子线程的局部变量在子线程结束后就失去了生存期,属于非法内存,访问会造成段错误。
    所以pthread_join()除了可以获取子线程函数child_thread()的参数外,仅只能获取子线程堆区的变量,也就是malloc出来的变量(因为malloc的生存期是由程序员自己控制的)。这里的void*arg参数是主线程提供的,虽然也是局部变量,但是其内存的生存期是要大于子线程的,故可以被返回。
    如果一条线程是可接合的,即默认属性,那么子线程在退出时不会释放自身的资源,从而成为僵尸进程,同时意味着该线程的返回值可以被其他线程获取

void *child_cthread(void *arg)
{
    char *p = (char *)malloc(20);
    strcpy(p, "hello world!");
    return (void *)p;
}

int main()
{
    pthread_t cid;
    if(pthread_create(&cid, NULL, child_cthread, (void *)"i am coming!") != 0)
        perror("create thread failed!\n");
    //等待子线程退出并获取子线程的返回值
    void *cret = NULL;
    pthread_join(cid, &cret);
    printf("cret: %s\n", (char *)cret);
    
    free(cret);
    cret = NULL;

    printf("main thread over!\n");
    return 0;
}

    获取子线程的堆区变量时,主线程一定要记得释放其资源。当主线程不需要获取子线程的堆区变量时,子线程自己一定要记得释放其资源。
    当我们不需要获取子线程的返回值时,为了不让子线程变成僵尸进程,我们一般最好是使用线程分离属性。

(二)线程分离

//函数原型
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)

//函数参数:
attr: 线程属性变量
detachstate: PTHREAD_CREATE_DETACHED, 表示分离
			 PTHREAD_CREATE_JOINABLE, 表示结合

//返回值:

接口参数:
        
        detachstate:PTHREAD_CREATE_DETACHED 表示分离,PTHREAD_CREATE_JOINABLE 表示结合

接口功能:
        设置了线程分离属性,子线程在退出时会自动释放资源,也就是说子线程的返回值是无法被主线程捕捉的。子线程自动释放资源避免了成为僵尸进程。但是线程分离就需要程序员自己确保主线程与子线程的生存期,避免因生存期问题造成子线程无法运行

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

#include <pthread.h>

void *child_thread(void *arg)
{
    pthread_t ccid = pthread_self();
    printf("child_thread ccid:%d\n", ccid);

    int vec[5] = {1, 2, 3, 4, 5};
    for(int i=0; i<5; i++)
        printf("%d\t", i);
    printf("\n");

    return arg;
}

int main()
{
    printf("main thread!\n");
    //1.设置线程分离属性,让子线程自动回收内存
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    //2.创建线程
    pthread_t cid;
    int ret = pthread_create(&cid, &attr, child_thread, (void *)"i am come coming");
    if(ret)
        perror("create pthread error\n");
    
    sleep(1);//给子线程提供足够的生存期
    //pause();//暂停主线程,目的也是给子线程提供足够的生存期
    
    // void *cret;
    // pthread_join(cid, &cret);
    // printf("cret: %s\n", (char *)cret);//线程分离之后是无法捕获子线程的返回值的

    printf("main thread end !\n");

    return 0;
}

    当多线程线程分离的时候,如何确定子线程的生存期和线程安全是非常重要的。在更大的项目中,光靠sleep是无法满足需求的,有些项目甚至不能阻塞。所以还需要学习更加进阶的线程知识。

二、进阶之路

(一)死锁

    在多线程编程中,我们为了防止多线程竞争共享资源导致数据错乱,都会在操作共享资源前加上互斥锁。即只有成功获得锁的线程才能操作共享资源,其他线程只能等待,直到锁被释放。
    当两个线程为了保护两个不同的共享资源而使用了两个互斥锁。如果两个锁使用不当,就会造成两个线程都在等待对方释放锁,从而导致死锁
    因此,总结得出死锁产生的条件有四个:互斥条件、持有并等待条件,不可剥夺条件、环路等待条件。

1.死锁产生条件

(1) 互斥条件

    互斥条件是指多个线程不能同时使用同一资源
在这里插入图片描述
    即线程A加锁后操作共享资源时,线程B也想访问这块共享资源,它只能等待线程A操作完资源并主动释放锁之后才能访问这块资源。

(2)持有并等待条件

    持有并等待条件是指,线程A已经加锁并持有了共享资源1,但又想访问共享资源2,但资源2已被线程B加锁并持有,因此线程A只得等待线程B释放锁。所以这里线程A的情况就是持有并等待条件。
在这里插入图片描述

(3)不用剥夺条件

    不可剥夺条件是对锁的特性的表征,它表示了当一个线程加锁并访问一个共享资源时,其他也想访问这块共享资源的线程必须等待其使用完该资源并释放锁
    不可剥夺条件和互斥条件情况是一致的,不过前者的重心是在必须等待,强调锁的特性;后者的重心是在多个线程同时访问,强调锁的使用场景。
在这里插入图片描述

(4)环路等待条件

    环路等待条件才是死锁发生的最重要条件。指的是发生死锁时,两个线程获取资源的顺序构成了一个环形链路
在这里插入图片描述

2.死锁实例代码

在这里插入代码片
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值