C++实战-Linux多线程(入门到精通)(二)

简介: C++实战-Linux多线程(入门到精通)(二)

线程属性

Linux下的线程属性是可以根据实际项目需求进行设置,之前我们讨论的是采用线程默认的属性。默认属性已经可以解决大多数问题。如果我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存使用从而增加最大线程数量。

主要属性:作用域、栈尺寸、栈地址、优先级、分离状态、调度策略

线程属性值不能直接设置,需要通过相关函数(可以理解为接口)进行操作:

int pthread_attr_init(pthread_attr_t *attr);            //初始化线程属性变量
int pthread_attr_destroy(pthread_attr_t  *attr);        //释放线程属性的资源
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate);  //获取线程分离的状态属性
int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);        //设置线程分离的状态属性

查看线程的属性方法: man  pthread_attr_XXX

案例:

       //创建一个线程属性变量

         pthread_attr_t  attr;

       //初始化属性变量

         pthread_attr_init(&attr);

       //设置属性

       pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);

       //设置线程栈的大小

       pthread_attr_setstacksize(&attr,size);

       .....

#include <stdio.h>
#include <pthread.h>
#include <string.h>
void* func(void *arg)
{
    printf("子线程:%lu\n",pthread_self());
    return NULL;
}
int main(void)
{
    pthread_t tid;
    //创建线程属性变量
    pthread_attr_t attr;
    //初始化
    pthread_attr_init(&attr);
    //设置线程分离
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    //设置栈大小
    int size = 256*1024;
    pthread_attr_setstacksize(&attr,size);
    pthread_create(&tid,&attr,func,NULL);
    while(1)
    {
        sleep(1);
        void* retval;
        int err = pthread_join(tid,&retval);
        if(err)
            printf("-------------err= %s\n", strerror(err));
        else
            printf("-----------%d\n",(int)retval);
    }
    return 0;
}

线程使用注意事项

1.主线程退出其他线程不退出,主线程调用 pthread_exit

2.避免僵尸线程

       pthread_join

       pthread_detach

       设置线程属性为分离,然后 pthread_create

3.malloc和mmap申请的内存可以被其他线程释放

4.应避免在多线程模型中调用fork,除非马上exec。子进程中只有调用frok的线程存在,其他线程在子进程中均pthread_exit

5.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

线程同步

先说同步的概念(不要觉得啰嗦,方便我们去理解线程同步):

       所谓同步,对于不同的研究对象而言是具有不同的含义的。例如:设备同步,是指在两个设备之间规定一个共同的时间参考。秦始皇的“书同文,车同轨”岂不也是一种同步。而在编程中的同步是指协同、协助、相互配合,主要是为了协调步骤,按预定的先后次序运行

线程同步

       同步即协同步骤,按预定的先后次序运行。大家有没有想过一个问题,为什么我们要强调按预定的先后次序,主要是因为同一个进程内的线程之间是资源共享的,加上并发的原因,假设一个线程想要修改每一个数据,还没修改完,另一个线程就把它取出,是不是就会产生问题。

       专业一点说:线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。(一个线程对某一共享的资源没有调用完,其它线程不能调用)

       

详细分析:

1.线程的主要优势在于能够通过全局变量来共享信息。不过,这种便捷(便捷是与进程间通信比较得出的)的共享是有代价的,必须确保多个线程不会同时修改同一变量或者某一线程不会读取正在由其他线程修改的变量(你会发现可以同时读)。

2.临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作。也就是同时访问同一共享资源的其他线程不应终断该片段的执行。

3.当有一个线程在对内存操作时,其他线程都不可以对这个内存地址进程操作,直到该线程完成操作,其他线程才能对该内存进行操作,而其他线程则处于等待状态

那么如何才能更好的保持这种原子操作呢?

互斥量、信号量、XXX锁....机制

不晓得有没有发现,很多时候的发展就是为了解决某一个问题。在任意条件下,很难做到十全十美,或者说很难画出一个完美的圆,我们一直在不断的创新不断的去接近这个完美的圆。好似我们永远没有算不完π一样。

互斥量(互斥锁)

先说一个通俗的理解:现在有一个房间,并且这个房间同一个时刻只能容纳一个人,防止两个人或以上的人进入,现在给这个房间置办一把锁,当有人进去时,看门的人就把房间锁上,当人出来,把锁打开。又有人进去时,把锁锁上,如此而已。其实很多计算机中解决问题的办法跟我们实际生活有很大的联系的,细细体会

1.为了避免线程更新(修改)共享变量时出现问题,可以使用互斥量(mutex)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。

2.互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。(我们回到上面的例子,我们管理人员每次只对一把锁打开,如果加了两把锁,那么屋子里的人出不来,屋子外的人进不去,而且管理员已经开了一把锁,他认为房间已经空了,等待人进去。导致永远阻塞在这了,呜呜...

3.一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一个共享资源会使用不同的互斥量,每一个线程在访问同一资源时将采用如下协议:

  •        针对共享资源锁定互斥量(加锁)
  •        访问共享资源(访问)
  •        对互斥量解锁(解锁)

4.如果有多个线程试图执行这一块代码(临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭遇阻塞),即同时只有一个线程能够进入这段代码区域

举个例子:

       通过"锁"将资源的访问变长了互斥操作,而后与时间有关的错误也不会产生(按预计的次序执行)。

说明:当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

所以,互斥锁实质上是一把"建议锁"(又称为”协同锁“),建议程序中有多线程访问共享资源的时候使用该机制,但不是强制使用。(什么意思呢,就是某一个线程在访问共享资源之前,不访问锁,直接去访问共享资源,也可以访问到。我们使用互斥量需要按照规定步骤来,防止数据混乱。我直接进房间,我不管是否有管理员)

相关函数

互斥量的类型:pthread_mutex_t

pthread_mutex_init函数        

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,

                                            const pthread_mutexattr_t *restrict attr);

       作用:初始化互斥量

       参数:mutex  需要初始化的互斥量变量

                  attr       互斥量相关属性,通常传 NULL

       restrict:C语言的修饰符,被修饰的指针不能由另外一个指针进行操作

pthread_mutex_destroy函数

       int pthread_mutex_destroy(pthread_mutex_t *mutex);

       作用:释放互斥量的资源

pthread_mutex_lock函数

       int pthread_mutex_lock(pthread_mutex_t *mutex);

       作用:加锁,阻塞的(如果一个线程加锁了,那么其他线程只能阻塞等待)

pthread_mutex_trylock函数

       int  pthread_mutex_trylock(pthread_mutex_t *mutex);

       作用:尝试加锁,非阻塞(如果加锁失败,不会阻塞,会直接返回)

pthread_mutex_unlock函数

      int pthread_mutex_unlock(pthread_mutex_t *mutex);

       作用:解锁

上个案例:(目标->能够完整打印HELLO WORLD或hello world)

      不加互斥锁:

               

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;      //定义为全局变量,不能定义为栈上的临时变量
void *func(void *arg)
{
    srand(time(NULL));
    while(1)
    {
        //pthread_mutex_lock(&mutex); //加锁
        printf("hello");
        sleep(rand()%3);
        printf("world\n");
        //pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    return NULL;
}
int main(void)
{
    int n = 5;
    pthread_t tid;
    srand(time(NULL));  //设置随机种子
    //初始化互斥量,在创建线程之前
    pthread_mutex_init(&mutex,NULL);
    //创建线程
    pthread_create(&tid,NULL,func,NULL);
    while(n--)
    {
        //pthread_mutex_lock(&mutex); //加锁
        printf("HELLO");
        sleep(rand()%3);
        printf("WORLD\n");
        //pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    //销毁锁
    pthread_mutex_destroy(&mutex);
    //关闭子线程
    pthread_cancel(tid);
    //回收子线程,或者设置线程分离
    pthread_join(tid,NULL);
    //pthread_detach(tid);
    return 0;
}

      加互斥锁

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;      //定义为全局变量,不能定义为栈上的临时变量
void *func(void *arg)
{
    srand(time(NULL));
    while(1)
    {
        pthread_mutex_lock(&mutex); //加锁
        printf("hello");
        sleep(rand()%3);
        printf("world\n");
        pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    return NULL;
}
int main(void)
{
    int n = 5;
    pthread_t tid;
    srand(time(NULL));  //设置随机种子
    //初始化互斥量,在创建线程之前
    pthread_mutex_init(&mutex,NULL);
    //创建线程
    pthread_create(&tid,NULL,func,NULL);
    while(n--)
    {
        pthread_mutex_lock(&mutex); //加锁
        printf("HELLO");
        sleep(rand()%3);
        printf("WORLD\n");
        pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }
    //销毁锁
    pthread_mutex_destroy(&mutex);
    //关闭子线程
    pthread_cancel(tid);
    //回收子线程,或者设置线程分离
    pthread_join(tid,NULL);
    //pthread_detach(tid);
    return 0;
}

我们的讨论到这里就结束了吗?

当然没有,我们来看一下特殊的情况

我现在把代码改成这个样子,会得到什么结果呢....

  进入了死循环,主线程竞争不到CPU了

线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁。这两个库函数本身不会阻塞。所以在这两行代码之间失去CPU的概率很小。因此,另一个线程很难得到加锁的机会。

我们再来修改代码:

发现子线程没有结束,父线程阻塞等待回收子线程

这个原因就很明显了,pthread_join会阻塞等待子线程结束,子线程进入死循环中..所以...

死锁

死锁产生的四个必要条件当时学习操作系统时(课本上定义):

1.互斥条件(我们互斥锁解决的就是互斥的共享资源,某一时刻只能有一个线程进入)

2.请求和保持条件(每一个进程都有保持现有状态)

3.不剥夺条件(没有外力的影响)

4.循环等待条件(等待其他进程释放资源)

这是课本中给我们的定义,后面加上了一些解释,方便理解。

那么在实际编程过程中的场景(主要有三种情况):

1.忘记释放锁

2.重复加锁

3.多线程多锁,抢占锁资源

(第一个种情况很好理解,这里就不过多的解释了。我们重点分析下第二种和第三种情况)

       

       有时,一个线程需要同时访问两个或更多不同的共享资源,而每一个共享资源都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就可能产生死锁。(对同一个互斥量加锁两次)

解决:访问完共享资源后立即解锁,等待步骤完成之后再加锁

       两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。(线程1拥有锁A,请求锁B,线程2拥有锁B,请求锁A)

解决:trylock替代lock函数并解锁(当不获取所有锁时主动放弃所有锁)      

上案例:

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1;
void* deadlock1(void *arg)
{
    pthread_mutex_lock(&mutex1);
    printf("hello");
    pthread_mutex_lock(&mutex1);
    printf("world1\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
int main(void)
{
    pthread_t tid1;
    //初始化
    pthread_mutex_init(&mutex1,NULL);
    //创建线程
    pthread_create(&tid1,NULL,deadlock1,NULL);
    //设置线程分离
    pthread_detach(tid1);
    //退出主线程
    pthread_exit(0);
    return 0;
}
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
void* deadlock1(void *arg)
{
    pthread_mutex_lock(&mutex1);
    printf("hello");
    sleep(4);
    pthread_mutex_lock(&mutex2);
    printf("world1\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
void* deadlock2(void *arg)
{
    // sleep(1);
    pthread_mutex_lock(&mutex2);
    printf("HELLOE");
    sleep(3);
    pthread_mutex_lock(&mutex1);
    printf("WORLD\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}
int main(void)
{
    pthread_t tid1,tid2;
    //初始化
    pthread_mutex_init(&mutex1,NULL);
    pthread_mutex_init(&mutex2,NULL);
    //创建线程
    pthread_create(&tid1,NULL,deadlock1,NULL);
    pthread_create(&tid2,NULL,deadlock2,NULL);
    //设置线程分离
    pthread_detach(tid1);
    pthread_detach(tid2);
    //退出主线程
    pthread_exit(0);
    return 0;
}
相关文章
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
847 60
【Java并发】【线程池】带你从0-1入门线程池
|
JavaScript Linux 网络安全
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
6226 77
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
521 67
|
关系型数据库 MySQL 应用服务中间件
Linux 手动安装快速部署 LNMP 环境实战
本文详细记录了在阿里云ECS上手动搭建LNMP环境的过程,系统选用Ubuntu 24.04。主要内容包括:1) 使用`apt`安装Nginx和MySQL,并更新软件源;2) 编译安装PHP 8.4.5,配置PHP-FPM及环境路径;3) 配置MySQL root用户密码;4) 调整Nginx支持PHP解析并测试整体环境。通过此过程,重现手动配置服务器的细节,帮助熟悉各组件的安装与协同工作。
1118 23
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
364 26
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
366 17
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
481 16
|
Unix Linux Shell
Linux新手入门手册
在Linux或类Unix系统中,掌握一系列基础命令和快捷键是提升工作效率的关键。这些工具和技巧不仅能帮助用户更高效地管理系统,还能在日常使用中带来极大的便利。以下是对这些基础操作与快捷键的详细解析大全。
389 9

热门文章

最新文章