为什么需要锁?
* 多线程编程有别于单线程编程。
* 当多个线程访问同一个全局变量时,线程之间会进行切换,需要保护和恢复现场。
* 一行普通的count++代码,可能对应多条汇编,如果上下文的切换发生在这几条汇编的中间,没有一次性执行完count++对应的那几行汇编代码,则会出现意想不到的结果。
* 下图是对以上的文字解释。
1. 上下文的切换没有发生在汇编代码的中间

2. 上下文的切换发生在汇编代码的中间

为了避免以上情况的发生,我们就需要锁!
锁的作用:确保同一时间只有一个线程访问数据
互斥量:互斥量从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完后解锁互斥量。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,说么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。

c语言中几种锁的类型:

  1. pthread_mutex_lock();
  2. pthread_mutex_trylock();
  3. pthread_spin_lock();
  4. pthread_wrlock();
    每种类型都要其适用的场景,下面依次介绍其用法。开十个线程对全局count变量进行++,直到1000000。
    (这里就不介绍未加锁的情形了)

1.pthread_mutex_lock();
这个函数对互斥量进行加锁。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock();
意外的收获!对例子进行编译的时候发生错误

错误的原因
huahuati@ubuntu:~/share$ gcc -o test.c -lpthread (编译的时候是这样的,没有填写目标文件名)
huahuati@ubuntu:~/share$ gcc -o test test.c -lpthread (ok)

pthread_mutex_lock()实例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_SIZE 10

int count = 0;
pthread_mutex_t mutex;

void *func(void *arg)
{
    int *pcount = (int *)arg;
    int i = 0;
    while(i++ < 100000)
    {
        pthread_mutex_lock(&mutex);
        (*pcount)++;
        pthread_mutex_unlock(&mutex);
        usleep(1);
    }
    pthread_exit(NULL);

}

int main()
{
    pthread_t threadid[THREAD_SIZE] = {0};
    pthread_mutex_init(&mutex, NULL);
    int i = 0;
    for(; i < THREAD_SIZE; i++)
    {
        int ret = pthread_create(&threadid[i], NULL, func, &count);
        if(ret)
        {
            perror("pthread_create()");
            break;
        }
    }

    for(i = 0; i < 100; i++)
    {
        printf("count --> %d\n",count);
        sleep(1);
    }
    return 0;
}

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

pthread_mutex_trylock()
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_SIZE 10

int count = 0;
pthread_mutex_t mutex;

void *func(void *arg)
{
    int *pcount = (int *)arg;
    int i = 0;
    while(i++ < 100000)
    {
        if (0 != pthread_mutex_trylock(&mutex)) 
        {
            i --;
            continue;
        }
        (*pcount) ++;
        pthread_mutex_unlock(&mutex);
        usleep(1);
    }
    pthread_exit(NULL);

}

int main()
{
    pthread_t threadid[THREAD_SIZE] = {0};
    pthread_mutex_init(&mutex, NULL);
    int i = 0;
    for(; i < THREAD_SIZE; i++)
    {
        int ret = pthread_create(&threadid[i], NULL, func, &count);
        if(ret)
        {
            perror("pthread_create()");
            break;
        }
    }

    for(i = 0; i < 100; i++)
    {
        printf("count --> %d\n",count);
        sleep(1);
    }
    return 0;
}

3.pthread_rwlock_rdlock()[这是读锁] pthread_rwlock_wrlock()[这是写锁]
读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有3种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时所有试图以读模式对它进行加锁的线程都可以得到访问权但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改。因为一次只有一个线程可以在写模式下拥有这个锁。当读写锁在读模式下时,只要线程先获取了读模式下的读写锁,该锁所保护的数据结构就可以被多个获得读模式锁的线程读取。
读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是以共享模式锁住的。当它是写模式锁住的时候,就可以说成是以互斥模式锁住的。
与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。

pthrea_rwlock_rd&wrlock
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_SIZE 10

int count = 0;
pthread_rwlock_t rwlock;
void *func(void *arg)
{
    int *pcount = (int *)arg;
    int i = 0;
    while(i++ < 100000)
    {
        pthread_rwlock_wrlock(&rwlock);
        (*pcount) ++;
        pthread_rwlock_unlock(&rwlock);
        usleep(1);
    }
    pthread_exit(NULL);

}

int main()
{
    pthread_t threadid[THREAD_SIZE] = {0};
    pthread_rwlock_init(&rwlock, NULL);
    int i = 0;
    for(; i < THREAD_SIZE; i++)
    {
        int ret = pthread_create(&threadid[i], NULL, func, &count);
        if(ret)
        {
            perror("pthread_create()");
            break;
        }
    }

    for(i = 0; i < 100; i++)
    {
        pthread_rwlock_rdlock(&rwlock); //读多写少的时候才使用,读时加读锁,写时加写锁
        printf("count --> %d\n",count);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return 0;
}

4.pthread_spin_lock()
自旋锁与互斥量类似。但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。自旋锁通常作为底层原语用于实现其他类型的锁。

pthread_spin_lock()
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_SIZE 10
pthread_spinlock_t spinlock;
int count = 0;
void *func(void *arg)
{
    int *pcount = (int *)arg;
    int i = 0;
    while(i++ < 100000)
    {
        pthread_spin_lock(&spinlock);
        (*pcount) ++;
        pthread_spin_unlock(&spinlock);
        usleep(1);
    }
    pthread_exit(NULL);

}

int main()
{
    pthread_t threadid[THREAD_SIZE] = {0};
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
    int i = 0;
    for(; i < THREAD_SIZE; i++)
    {
        int ret = pthread_create(&threadid[i], NULL, func, &count);
        if(ret)
        {
            perror("pthread_create()");
            break;
        }
    }

    for(i = 0; i < 100; i++)
    {
        printf("count --> %d\n",count);
        sleep(1);
    }
    return 0;
}

不加锁的原子操作
原子操作不会被打断,意味着线程的切换不会对原子操作有影响

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

#define THREAD_SIZE 10
int count = 0;
int inc(int *value, int add) {   //这里是原子操作

    int old;

    __asm__ volatile (     //要;指令系统中存在这样的指令
        "lock; xaddl %2, %1;" 
        : "=a" (old)
        : "m" (*value), "a" (add)
        : "cc", "memory"   //内存屏障
    );

    return old;    //这个返回值没什么用
}
void *func(void *arg)
{
    int *pcount = (int *)arg;
    int i = 0;
    while(i++ < 100000)
    {
        inc(pcount, 1);
        usleep(1);
    }
    pthread_exit(NULL);

}

int main()
{
    pthread_t threadid[THREAD_SIZE] = {0};
    int i = 0;
    for(; i < THREAD_SIZE; i++)
    {
        int ret = pthread_create(&threadid[i], NULL, func, &count);
        if(ret)
        {
            perror("pthread_create()");
            break;
        }
    }

    for(i = 0; i < 100; i++)
    {
        printf("count --> %d\n",count);
        sleep(1);
    }
    return 0;
}

上述都是使用多线程的方式,下面介绍多进程加原子操作。
多进程

多进程
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#include<sys/mman.h>   //注意头文件

#define PROCESS_SIZE 10
int count = 0;
int inc(int *value, int add) {   //这里是原子操作

    int old;

    __asm__ volatile (     //要;指令系统中存在这样的指令
        "lock; xaddl %2, %1;" 
        : "=a" (old)
        : "m" (*value), "a" (add)
        : "cc", "memory"   //内存屏障
    );

    return old;    //这个返回值没什么用
}


int main()
{
    int *pcount = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0); //这里共享一个int型变量

    int i = 0;
    pid_t pid = 0;
    for(i = 0; i < PROCESS_SIZE; i++)
    {
        pid = fork();
        if(pid <= 0)
        {
            usleep(1);   //这里的usleep()的作用是让子线程先走
            break;  //为什么break;  如果进程不break;那么子进程也会执行上述的for循环
                    //那么不break创建的子进程主将是 2 ^ PROCESS_SIZE 个
                    //  break掉的话,只会创建PROCESS_SIZE个进程
        }
    }

    if( pid > 0)  //父进程
    {
        for (i = 0;i < 100;i ++) 
        {
            printf("count --> %d\n",  (*pcount));
            sleep(1);
        }
    }
    else   //子进程
    {
        int i = 0;
        while(i++ < 100000)
        {
            inc(pcount, 1);
        }
    }
    return 0;
}

!!!以上要在linux环境中才可运行。对加锁函数的解释来自《unix环境编程(第三版)》

posted @ 2022-07-23 11:28  huahuati  阅读(95)  评论(0)    收藏  举报