Linux系统学习篇——多线程和互斥锁

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


下文转自:
https://www.cnblogs.com/xiehongfeng100/p/4620852.html#autoid-1-2-0

进程与线程

典型的UNIX/Linux进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。

进程是程序执行时的一个实例,是担当分配系统资源(CPU时间、内存等)的基本单位。在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。

线程是操作系统能够进行运算调度的最小单位它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程包含了表示进程内执行环境必须的信息,其中包括进程中表示线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno常量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本、程序的全局内存和堆内存、栈以及文件描述符。在Unix和类Unix操作系统中线程也被称为轻量级进程(lightweight processes),但轻量级进程更多指的是内核线程(kernel thread),而把用户线程(user thread)称为线程。

"进程——资源分配的最小单位,线程——程序执行的最小单位"

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

一、线程开发相关API

多线程开发在 Linux 平台上已经有成熟的 pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。详细请见下表:

在这里插入图片描述

1. 线程创建

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
// 返回:若成功返回0,否则返回错误编号

当pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性,暂可以把它设置为NULL,以创建默认属性的线程。

新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

2. 线程退出

单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:

1)线程只是从启动例程中返回,返回值是线程的退出码。

2)线程可以被同一进程中的其他线程取消。

3)线程调用pthread_exit:

#include <pthread.h>
int pthread_exit(void *rval_ptr);

rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

3. 线程等待

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回:若成功返回0,否则返回错误编号

调用这个函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果例程只是从它的启动例程返回i,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。

如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。

4. 线程脱离

一个线程或者是可汇合(joinable,默认值),或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关的资源都被释放,我们不能等待它们终止。如果一个线程需要知道另一线程什么时候终止,那就最好保持第二个线程的可汇合状态。

pthread_detach函数把指定的线程转变为脱离状态。

#include <pthread.h>
int pthread_detach(pthread_t thread);
// 返回:若成功返回0,否则返回错误编号

本函数通常由想让自己脱离的线程使用,就如以下语句:

pthread_detach(pthread_self());

5. 线程ID获取及比较

#include <pthread.h>
pthread_t pthread_self(void);
// 返回:调用线程的ID

对于线程ID比较,为了可移植操作,我们不能简单地把线程ID当作整数来处理,因为不同系统对线程ID的定义可能不一样。我们应该要用下边的函数:

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 返回:若相等则返回非0值,否则返回0
  

例子

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

#define unsigned int unit;

int g_Flag = 0;
void *thread1(void *);

int main(int argc, char **argv)
{
    printf("Enter main function.\n");

    pthread_t tid1;
    int err1 = 0;

    err1 = pthread_create(&tid1, NULL, thread1, NULL);
    if(err1 != 0)
        printf("%s: %d\n", __func__, strerror(err1));

    printf("This is main the thread, process ID is %u, thread ID is %u, and g_Flag is %d.\n", (uint)getpid(), (uint)pthread_self, g_Flag);

    printf("Leave main functiona.\n");
    exit(0);
}

void *thread1(void *arg)
{
    printf("Enter thread1.\n");
    printf("This is thread1, process ID is %u, thread ID is %u, and g_Flag is %d.\n", (uint)getpid(), (uint)pthread_self, g_Flag);
    g_Flag = 1;
    printf("This is thread1, process ID is %u, thread ID is %u, and g_Flag is %d.\n", (uint)getpid(), (uint)pthread_self, g_Flag);
    printf("Leave thread1.\n");
    pthread_exit(0);
}

程序运行结果:

qyg@qyg-virtual-machine:~/linux_dev/6_bokedemo$ ./demo3
Enter main function.
This is main the thread, process ID is 17423, thread ID is 599866768, and g_Flag is 0.
Leave main functiona.

**很明显,该程序的进程并没有执行线程thread1。**解决方法很简单,只需在return 0之前来个sleep,while(1)或 pthread_join即可。
调用这个pthread_join函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消

主线程与新线程存在竞争:主线程需要休眠,如果主线程不休眠,它就可能退出,这样在新线程有机会运行之前整个进程就已经终止了。这种行为依赖于操作系统的线程实现方法和调度方法。

对于多线程程序来说,我们往往需要对这些多线程进行同步。同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。在这里,我们暂不介绍读写锁。

二、互斥锁

互斥量(mutex)从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为可运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去等待它重新变为可用。在这种方式下,每次只有一个线程可以向前运行。

在设计时需要规定所有的线程必须遵守相同的数据访问规则。只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。

互斥变量用pthread_mutex_t数据类型表示。在使用互斥变量前必须对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。

1.创建及销毁互斥锁

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t mutex);
// 返回:若成功返回0,否则返回错误编号

要用默认的属性初始化互斥量,只需把attr设置为NULL。

2.加锁及解锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t mutex);
int pthread_mutex_trylock(pthread_mutex_t mutex);
int pthread_mutex_unlock(pthread_mutex_t mutex);
// 返回:若成功返回0,否则返回错误编号

如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY

例子

简单的例子:

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

pthread_mutex_t mutex;

void *fun1()
{
    
    /* 加锁*/
    pthread_mutex_lock(&mutex);
    printf("this is fun1\n");
    printf("this is fun111\n");
    /* 解锁*/
    pthread_mutex_unlock(&mutex);
}

void *fun2()
{
    
    /* 加锁*/
    pthread_mutex_lock(&mutex);
    printf("this is fun2\n");
    printf("this is fun2\n");
    /* 解锁*/
    pthread_mutex_unlock(&mutex);
   
}
 int main()
 {
    pthread_mutex_init(&mutex, NULL);   /* 创建锁,并初始化*/
    pthread_t t1;
    pthread_t t2;

    pthread_create(&t1, NULL, fun1, NULL);
    pthread_create(&t2, NULL, fun2, NULL);

    printf("this main1 process\n");
    printf("this main2 process\n");

    pthread_mutex_destroy(&mutex);   /* 销毁锁*/

    while(1);
    return 0;
 }

改进的例子:

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

#define unsigned int unit;

int g_Flag = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void printids(const char *s);
void lock(const char *s);
void unlock(const char *s);

void *thread1(void *);
void *thread2(void *);
void *thread3(void *);

int main(int argc, char **argv)
{
    printf("Enter main function.\n");

    pthread_t tid1, tid2, tid3;
    int err1 = 0, err2 = 0, err3 = 0;
    void *rval1, *rval2, *rval3;

    err1 = pthread_create(&tid1, NULL, thread1, NULL);
    if(err1 != 0)
        printf("%s: %d\n", __func__, strerror(err1));

    err2 = pthread_create(&tid2, NULL, thread2, NULL);
    if(err2 != 0)
        printf("%s: %d\n", __func__, strerror(err2));

    err3 = pthread_create(&tid3, NULL, thread3, NULL);
    if(err3 != 0)
        printf("%s: %d\n", __func__, strerror(err3));

    err1 = pthread_join(tid1, &rval1);
    if(err1 != 0)
        printf("%s: %d\n", __func__, strerror(err1));
    printf("thread1 exit code is %d.\n", (int)rval1);

    err2 = pthread_join(tid2, &rval2);
    if(err2 != 0)
        printf("%s: %d\n", __func__, strerror(err2));
    printf("thread2 exit code is %d.\n", (int)rval2);

    err3 = pthread_join(tid3, &rval3);
    if(err3 != 0)
        printf("%s: %d\n", __func__, strerror(err3));
    printf("thread3 exit code is %d.\n", (int)rval3);

    printids("This is the main thread");
    printf("Leave main function.\n");
    exit(0);
}

void printids(const char *s)
{
    pid_t        pid;
    pthread_t    tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s, process ID is %u, thread ID is %u, and g_Flag is %d.\n", s, (uint)pid, (uint)tid, g_Flag);
}

void lock(const char *s)
{
    if(pthread_mutex_lock(&mutex) != 0)
    {
        printf("%s has lock error!", s);
        exit(0);
    }
}

void unlock(const char *s)
{
    if(pthread_mutex_unlock(&mutex) != 0)
    {
        printf("%s has unlock error!", s);
        exit(0);
    }
}

void *thread1(void *arg)
{
    lock("Thread1");
    printf("Enter thread1.\n");
    printids("This is thread1");
    int i;
    for( i = 0; i < 1000; i++)
        g_Flag++;
    printids("This is thread1");
    printf("Leave thread1.\n");
    unlock("Thread1");
    pthread_exit(0);
}

void *thread2(void *arg)
{
    lock("Thread2");
    printf("Enter thread2.\n");
    printids("This is thread2");
    int i;
    for( i = 0; i < 1000; i++)
        g_Flag++;
    printids("This is thread2");
    printf("Leave thread2.\n");
    unlock("Thread2");
    return (void *)2;
}

void *thread3(void *arg)
{
    lock("Thread3");
    printf("Enter thread3.\n");
    printids("This is thread3");
    int i;
    for( i = 0; i < 1000; i++)
        g_Flag++;
    printids("This is thread3");
    printf("Leave thread3.\n");
    unlock("Thread3");
    return (void *)3;
}

改进后的程序输出如下:
在这里插入图片描述
这下程序输出是正确的。想要更改线程的执行顺序,我们可以将pthread_join放在恰当的地方。另外,我们的锁主要是要保护数据操作的安全性,而不是printf函数输出的正确性。不过我们在程序中将printf函数同时保护起来,这样保证每个线程的输出完整性。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值