如何使用 C 互斥锁示例进行 Linux 线程同步

举报
Tiamo_T 发表于 2022/06/15 16:35:29 2022/06/15
【摘要】 在本文中,我们将介绍一个重要的方面,即线程同步。

在 Linux 线程系列中,我们讨论了线程可以终止的方式以及返回状态如何从终止线程传递到其父线程。在本文中,我们将介绍一个重要的方面,即线程同步。

线程同步问题

让我们用一个示例代码来研究同步问题:

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

pthread_t tid[2];
int counter;

void* doSomeThing(void *arg)
{
    unsigned long i = 0;
    counter += 1;
    printf("\n Job %d started\n", counter);

    for(i=0; i<(0xFFFFFFFF);i++);
    printf("\n Job %d finished\n", counter);

    return NULL;
}

int main(void)
{
    int i = 0;
    int err;

    while(i < 2)
    {
        err = pthread_create(&(tid[i]), NULL, &doSomeThing, NULL);
        if (err != 0)
            printf("\ncan't create thread :[%s]", strerror(err));
        i++;
    }

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);

    return 0;
}

上面的代码是一个简单的代码,其中创建了两个线程(作业),并且在这些线程的启动函数中,维护了一个计数器,用户通过该计数器获取有关已启动的作业号以及何时完成的日志。代码和流程看起来不错,但是当我们看到输出时:

$ ./tgsthreads
Job 1 started
Job 2 started
Job 2 finished
Job 2 finished

如果您关注最后两个日志,您将看到日志“作业 2 完成”重复了两次,而没有看到“作业 1 完成”的日志。

现在,如果您返回代码并尝试找到任何逻辑缺陷,您可能不会轻易找到任何缺陷。但是,如果您仔细观察并可视化代码的执行,您会发现:

  • 日志“Job 2 started”在“Job 1 Started”之后打印,因此可以很容易地得出结论,当线程 1 正在处理调度程序时,调度程序调度了线程 2。
  • 如果上述假设为真,则“计数器”变量的值在作业 1 完成之前再次增加。
  • 因此,当作业 1 实际完成时,计数器的错误值会生成日志“作业 2 完成”,然后是实际作业 2 的“作业 2 完成”,反之亦然,因为它取决于调度程序。
  • 所以我们看到问题不是重复的日志,而是“计数器”变量的错误值。

实际问题是当第一个线程正在使用或即将使用它时,第二个线程对变量“counter”的使用。换句话说,我们可以说在使用共享资源“计数器”时线程之间缺乏同步导致了问题,或者总而言之,我们可以说这个问题是由于两个线程之间的“同步问题”而发生的。

互斥体

现在既然我们已经了解了基本问题,让我们讨论一下解决方案。实现线程同步的最流行方法是使用互斥锁。


互斥锁是我们在使用共享资源之前设置并在使用后释放的锁。设置锁后,没有其他线程可以访问锁定的代码区域。所以我们看到,即使线程 2 被调度,而线程 1 没有完成访问共享资源并且代码被线程 1 使用互斥锁锁定,那么线程 2 甚至无法访问该代码区域。所以这确保了代码中共享资源的同步访问。

在内部它的工作原理如下:

  • 假设一个线程使用互斥锁锁定了一个代码区域并正在执行该代码。
  • 现在,如果调度程序决定进行上下文切换,那么准备好执行同一区域的所有其他线程都将被解除阻塞。
  • 所有线程中只有一个会执行,但如果该线程尝试执行已锁定的同一代码区域,那么它将再次进入睡眠状态。
  • 上下文切换将一次又一次地发生,但在释放互斥锁之前,没有线程能够执行锁定的代码区域。
  • 互斥锁只会由锁定它的线程释放。
  • 所以这确保了一旦一个线程锁定了一段代码,那么在锁定它的线程解锁之前,没有其他线程可以执行相同的区域。
  • 因此,该系统在处理共享资源时确保线程之间的同步。

一个互斥体被初始化,然后通过调用以下两个函数来获得一个锁:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);

第一个函数初始化互斥体,通过第二个函数可以锁定代码中的任何关键区域。

可以通过调用以下函数来解锁和销毁互斥锁​​:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

上面的第一个函数释放锁,第二个函数销毁锁,这样以后就不能在任何地方使用它了。

一个实际的例子

让我们看一段使用互斥锁进行线程同步的代码

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

pthread_t tid[2];
int counter;
pthread_mutex_t lock;

void* doSomeThing(void *arg)
{
    pthread_mutex_lock(&lock);

    unsigned long i = 0;
    counter += 1;
    printf("\n Job %d started\n", counter);

    for(i=0; i<(0xFFFFFFFF);i++);

    printf("\n Job %d finished\n", counter);

    pthread_mutex_unlock(&lock);

    return NULL;
}

int main(void)
{
    int i = 0;
    int err;

    if (pthread_mutex_init(&lock, NULL) != 0)
    {
        printf("\n mutex init failed\n");
        return 1;
    }

    while(i < 2)
    {
        err = pthread_create(&(tid[i]), NULL, &doSomeThing, NULL);
        if (err != 0)
            printf("\ncan't create thread :[%s]", strerror(err));
        i++;
    }

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_mutex_destroy(&lock);

    return 0;
}

在上面的代码中:

  • 互斥锁在 main 函数的开头被初始化。
  • 在使用共享资源“计数器”时,相同的互斥锁在“doSomeThing()”函数中被锁定
  • 在函数“doSomeThing()”结束时,相同的互斥锁被解锁。
  • 在两个线程都完成的主函数结束时,互斥锁被销毁。

现在,如果我们查看输出,我们会发现:

$ ./threads
Job 1 started
Job 1 finished
Job 2 started
Job 2 finished

所以我们看到这次两个作业的开始和完成日志都存在。所以线程同步是通过使用互斥锁来实现的。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。