C/C++基于线程的并发编程(二):线程安全和线程锁

线程安全

所谓线程安全不是指线程的安全,而是指内存的安全。线程是由进程所承载,所有线程均可访问进程的上下文,意味着所有线程均可访问在进程中的内存空间,这也是线程之间造成问题的潜在原因。当多个线程读取同一片内存空间(变量、对象等)时,不会引起线程安全问题。但是当多个线程对同一片内存空间进行写操作时,就需要考虑内存安全问题。

线程不安全例子

#include <iostream>
#include <thread>

using namespace std;
int counter = 0;

void func()
{
    for (int i = 0; i < 10000; ++i)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));	// 休眠1ms
        ++counter;		// 写入操作:counter自加
    }
    
}

int main()
{
    std::thread t1 = std::thread(func);
    std::thread t2 = std::thread(func);

    t1.join();
    t2.join();

    cout << "counter: " << counter << endl;

    return 0;
}

上述代码,预期结果是counter输出20000,因为两个线程均会进入循环,counter各自自加10000次。实际输出效果如下:

counter: 19989
counter: 19992

两次输出结果均不相同,且均不等于预期的20000。这就线程不安全导致的BUG。

线程锁

为了解决上述线程安全问题,就需要线程锁,C和C++最常用的锁包括互斥锁、条件锁、自旋锁和读写锁等。其中最基本的是互斥锁,其他的锁都是基于互斥锁实现。所以锁的功能越强大,性能就越低。

互斥锁

互斥锁(mutex),也成为互斥量,就是保证多线程共享同一个互斥量,然后让线程之间。当一个线程对共享数据操作时,利用互斥锁进行上锁,其他线程在尝试再次上锁时,发现已上锁就会睡眠等待。等到互斥锁解开,才能再次上锁进而继续操作,从而保护共享数据的安全。下面利用互斥锁,解决上述BUG。

Windows环境C++实现

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mutex;

void func()
{
    for (int i = 0; i < 10000; ++i)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        mutex.lock();
        ++counter;
        mutex.unlock();
    }
    
}

int main()
{
    std::thread t1 = std::thread(func);
    std::thread t2 = std::thread(func);

    t1.join();
    t2.join();

    std::cout << "counter: " << counter << std::endl;

    return 0;
}

输出结果如下:

counter: 20000
counter: 20000

两次运行输出均为20000,与预期结果相同,证明加入互斥锁后解决线程安全问题。

基于RAII优化C++实现

RAII(Resource Acquisition Is Initialization)是一种C++编程技术,它将必须在使用前请求的资源(例如:分配的堆内存、执行线程、打开的套接字、打开的文件、锁定的互斥体、磁盘空间、数据库连接等——任何存在受限供给中的事物)的生命周期与一个对象的生存周期相绑定。 RAII保证资源可用于任何会访问该对象的函数。它亦保证所有资源在其控制对象的生存期结束时,以获取顺序的逆序释放。类似地,若资源获取失败(构造函数以异常退出),则为已构造完成的对象和基类子对象所获取的所有资源,会以初始化顺序的逆序释放。这有效地利用了语言特性以消除内存泄漏并保证异常安全。

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mutex;

void func()
{
    for (int i = 0; i < 10000; ++i)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        std::lock_guard<std::mutex> lock(mutex);
        ++counter;
    }
    
}

int main()
{
    std::thread t1 = std::thread(func);
    std::thread t2 = std::thread(func);

    t1.join();
    t2.join();

    std::cout << "counter: " << counter << std::endl;

    return 0;
}

lock_guard使用的就是RAII编程技巧,在方法结束的时候,局部变量std::lock_guard<std::mutex> lock会被销毁,它对互斥体的锁定也就解除了。

Linux环境C语言实现

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

int counter = 0;
pthread_mutex_t mutex;

void *func(void *val)
{
    for (int i = 0; i < 10000; ++i)
    {
        usleep(1 * 1000);
        pthread_mutex_lock(&mutex);
        ++counter;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_mutex_init(&mutex, NULL);

    pthread_t t1;
    pthread_t t2;

    pthread_create(&t1, NULL, func, NULL);
    pthread_create(&t2, NULL, func, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_mutex_destroy(&mutex);

    printf("counter: %d\n", counter);

    return 0;
}

输出如下:

counter: 20000

输出也是预期的20000。

条件锁

条件锁,即条件变量,可以用于阻塞一个线程或多个线程,直到另一个线程修改共享变量(条件)后,通知条件锁。条件锁的实现的基于互斥锁和条件变量共同实现的,也可以实现线程同步。

Windows环境C++实现

C++条件锁依赖于std::unique_lock实现,条件锁需要满足某个条件才会进行加锁,否则将等待直到条件满足,下面举例无法等待成功情况。

条件锁成员函数wait
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mutex;
std::condition_variable condition;
std::string data;
bool ready = false;

void func()
{
    std::unique_lock<std::mutex> locker(mutex);
    condition.wait(locker, []{return ready;});      // 等待ready为true,再上锁

    std::cout << "condition locker finish waiting." << std::endl;
    data += " after waiting locker.";
}

int main()
{
    std::thread thread(func);

    data = "Example data";

	thread.join();
	
    std::cout << data << std::endl;

    return 0;
}

程序无法退出,且无法输出,因为thread在join后,一直没有等待ready置为true,所以停止在func函数的wait调用中无法退出。
若不想一直等待条件符合,可以使用wait_for函数,等一段时间后若条件达成则返回true,未达成则返回false。下面实现例子

条件锁成员函数wait_for
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>

using namespace std::chrono_literals;
std::mutex mutex;
std::condition_variable condition;
std::string data;
bool ready = false;

void func()
{
    std::unique_lock<std::mutex> locker(mutex);
    if( condition.wait_for(locker, 1s, []{return ready;})) {
        std::cout << "condition locker finish waiting." << std::endl;
        data += " after waiting locker.";
    }   
}

int main()
{
    std::thread thread(func);

    data = "Example data";

    thread.join();

    std::cout << data << std::endl;		// 输出: Example data

    return 0;
}

线程等待1s后,未收到ready置为true,输出“Example data”。

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <chrono>
#include <condition_variable>

using namespace std::chrono_literals;
std::mutex mutex;
std::condition_variable condition;
std::string data;
bool ready = false;

void func()
{
    std::unique_lock<std::mutex> locker(mutex);
    if( condition.wait_for(locker, 1s, []{return ready;})) {
        std::cout << "condition locker finish waiting." << std::endl;
        data += " after waiting locker.";
    }   
}

int main()
{
    std::thread thread(func);

    data = "Example data";
    {
        // 线程安全
        std::lock_guard<std::mutex> locker(mutex);
        ready = true;
    }
    condition.notify_one();

    thread.join();

    std::cout << data << std::endl;

    return 0;
}

输出如下:

condition locker finish waiting.
Example data after waiting locker.

当thread线程开启后,主线程将ready置为true,并通过notify_one通知线程。thread的条件锁等待成功,往下运行输出信息与预期相同。
notify_one和notify_all,区别在于notity_one只会唤醒一个线程,notify_all会唤醒所有等待的线程。

Linux环境C语言实现

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

char data[128] = "Example data";
pthread_mutex_t mutex;
pthread_cond_t condition;

void *func(void *val)
{
    pthread_cond_wait(&condition, &mutex);

    printf("func condition locker finish waiting.\n");

    char *temp = "after func waiting locker.";

    strcat(data, temp);
}

int main()
{
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&condition, NULL);

    pthread_t t1;
    pthread_create(&t1, NULL, func, NULL);

    usleep(100 * 1000);     // 等待100ms,防止线程未开始等待就发出条件信号
    pthread_cond_signal(&condition);

    pthread_join(t1, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&condition);
    
    printf("%s\n", data);

    return 0;
}

输出如下:

func condition locker finish waiting.
Example dataafter func waiting locker.

读写锁

读写锁就是区分线程中的读操作和写操作。由于线程中只执行读操作,对临界区时不构成影响,所以区分读操作和写操作,就可以程序的效率。当线程对读写锁加读操作锁时,其他线程也需要读时,可以直接获得独写锁并读取临界区资源;而当其他线程在读写锁加锁的情况下进行写操作锁的加锁,需要等待读写锁释放才能进行获得锁,且写操作锁只能由一个线程拥有。读写锁也被成为共享-独占锁,因为在读操作,读写锁时共享给读操作的,而在写操作时就是独占于一个线程。

Windows环境C++实现

在C++14起加入了std::shared_lockstd::shared_timed_mutex可共享锁的模板,结合std::unique_lock进行读写锁的实现。

#include <shared_mutex>
#include <iostream>
#include <chrono>
#include <thread>

int i = 100;
std::shared_timed_mutex mutex;

void funcRead() 
{
    std::shared_lock<std::shared_timed_mutex> locker(mutex);
    std::cout << std::this_thread::get_id() << "funcRead: " << i << std::endl;
    std::cout << std::this_thread::get_id() << "end funcRead" << std::endl;
}

void funcWrite()
{
    std::unique_lock<std::shared_timed_mutex> locker(mutex);
    std::cout << std::this_thread::get_id() << "funcWrite: " << ++i << std::endl;
}

int main()
{
    std::thread t1Read(funcRead);
    std::thread t2Read(funcRead);
    std::thread t3Write(funcWrite);
    
    t1Read.join();
    t2Read.join();
    t3Write.join();

    return 0;
}
3funcRead: 100
2funcRead: 100
2end funcRead
3end funcRead
4funcWrite: 101

由于线程的调度是内核实现,上述实现为简单实现,线程的开启可能有所不同。如果需要控制线程的开启时机可以加入条件锁进行线程函数的处理。上述输出能证明再两个funcRead线程中,互斥量是共享的,而写锁需要等到读锁释放才能获得。

Linux环境C语言实现

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

int i = 100;
pthread_rwlock_t rwlock;

void *funcRead(void *val)
{
    pthread_rwlock_rdlock(&rwlock);
    printf("thread 0x%x: %d\n", (unsigned int)pthread_self(), i);
    pthread_rwlock_unlock(&rwlock);
    printf("end funcRead\n");
}

void *funcWrite(void *val)
{
    pthread_rwlock_wrlock(&rwlock);
    printf("thread 0x%x: %d\n", (unsigned int)pthread_self(), ++i);
    pthread_rwlock_unlock(&rwlock);
}

int main()
{
    pthread_t t1read;
    pthread_t t2read;
    pthread_t twrite;

    pthread_rwlock_init(&rwlock, NULL);
    pthread_create(&t1read, NULL, funcRead, NULL);
    pthread_create(&t2read, NULL, funcRead, NULL);
    pthread_create(&twrite, NULL, funcWrite, NULL);

    pthread_join(t1read, NULL);
    pthread_join(t2read, NULL);
    pthread_join(twrite, NULL);

    pthread_rwlock_destroy(&rwlock);

    return 0;
}
thread 0xf7d9f700: 100
end funcRead
thread 0xf759e700: 100
end funcRead
thread 0xf6d9d700: 101

C语言实现主要利用POSIX标准的pthread_rwlock_t的读写锁API实现,输出与C++相类似。

自旋锁

自旋锁与互斥锁是类似的,实现方式也是基于互斥锁实现(上文提到,所有复杂的锁都是基于互斥锁实现的)。区别在于自旋锁是不断循环并测试锁的状态,这样会一直占用cpu资源。
C++没有提供自选锁的API,所以下面基于POSIX在C语言中实现自旋锁。

Linux环境C语言实现

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

int counter = 0;
pthread_spinlock_t spinlock;

void *func(void *val)
{
    for (int i = 0; i < 10000; ++i)
    {
        usleep(1 * 1000);
        pthread_spin_lock(&spinlock);
        ++counter;
        pthread_spin_unlock(&spinlock);
    }
}
int main()
{
    pthread_spin_init(&spinlock, 0);

    pthread_t t1;
    pthread_t t2;

    pthread_create(&t1, NULL, func, NULL);
    pthread_create(&t2, NULL, func, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_spin_destroy(&spinlock);

    printf("counter: %d\n", counter);

    return 0;
}

输出和互斥锁的实现一样:

counter: 20000
自旋锁和互斥锁等待锁的资源消耗情况

将互斥锁和自旋锁的线程函数修改为以下状况,让线程一直等待锁。

/* 互斥锁 */
void *func(void *val)
{
    for (int i = 0; i < 10000; ++i)
    {
        usleep(1 * 1000);
        pthread_mutex_lock(&mutex);
        ++counter;
        // pthread_mutex_unlock(&mutex);
    }
}

/* 自旋锁 */
void *func(void *val)
{
    for (int i = 0; i < 10000; ++i)
    {
        usleep(1 * 1000);
        pthread_spin_lock(&spinlock);
        ++counter;
        // pthread_spin_unlock(&spinlock);
    }
}

互斥锁资源消耗情况:
在这里插入图片描述
自旋锁资源消耗情况:在这里插入图片描述从两张图可以看出,当互斥锁等待锁时会将资源交还给CPU,然后等待下次内核调用在尝试获得锁。而自旋锁会一直占用CPU,也就是死循环。所以互斥锁是休眠等待(sleep-waiting),而自旋锁是忙等待(busy-waiting)

递归锁

递归锁就是在同一个线程中,不解锁的情况下,可以重复获得同一个递归锁,而不导致死锁。互斥锁为非递归锁。

Windows环境C++实现

#include <iostream>
#include <thread>
#include <mutex>

std::recursive_mutex mutex;
std::string shared;

void func1()
{
    std::lock_guard<std::recursive_mutex> locker(mutex);
    shared = "func1";
    std::cout << "thread id: " << std::this_thread::get_id() << " func1: shared variable: " << shared << std::endl; 
}

void func2()
{
    std::lock_guard<std::recursive_mutex> locker(mutex);
    shared = "func2";
    std::cout << "thread id: " << std::this_thread::get_id() << " func2: shared variable: " << shared << std::endl;
    func1();
    std::cout << "thread id: " << std::this_thread::get_id() << " back in func2, shared variable: " << shared << std::endl;
}

int main()
{
    std::thread t1(func1);
    std::thread t2(func2);

    t1.join();
    t2.join();
}

输出如下:

thread id: 2 func1: shared variable: func1
thread id: 3 func2: shared variable: func2
thread id: 3 func1: shared variable: func1
thread id: 3 back in func2, shared variable: func1

在func2中,在线程t2重新上锁,递归锁再次获得锁,这就是递归锁的特性。

非递归锁情况

如果将递归锁换成非递归锁,程序如下:

#include <iostream>
#include <thread>
#include <mutex>

// std::recursive_mutex mutex;
std::mutex mutex;
std::string shared;

void func1()
{
    // std::lock_guard<std::recursive_mutex> locker(mutex);
    std::lock_guard<std::mutex> locker(mutex);
    shared = "func1";
    std::cout << "thread id: " << std::this_thread::get_id() << " func1: shared variable: " << shared << std::endl; 
}

void func2()
{
    // std::lock_guard<std::recursive_mutex> locker(mutex);
    std::lock_guard<std::mutex> locker(mutex);
    shared = "func2";
    std::cout << "thread id: " << std::this_thread::get_id() << " func2: shared variable: " << shared << std::endl;
    func1();
    std::cout << "thread id: " << std::this_thread::get_id() << " back in func2, shared variable: " << shared << std::endl;
}

int main()
{
    std::thread t1(func1);
    std::thread t2(func2);

    t1.join();
    t2.join();
}

输出如下:

thread id: 2 func1: shared variable: func1
thread id: 3 func2: shared variable: func2

当线程t2运行到func1时无法获得锁,发生死锁的情况。这就是递归锁和非递归锁的区别。

Linux环境C语言实现

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

char shared[12] = {0};
pthread_mutex_t mutex;

void func1()
{
    pthread_mutex_lock(&mutex);
    strcpy(shared, "func1");
    printf("thread id: 0x%x, func1 shared variable: %s\n", (unsigned int)pthread_self(), shared);
    pthread_mutex_unlock(&mutex);
}

void func2()
{
    pthread_mutex_lock(&mutex);
    strcpy(shared, "func2");
    printf("thread id: 0x%x, func2 shared variable: %s\n", (unsigned int)pthread_self(), shared);
    func1();
    printf("thread id: 0x%x, back in func2, shared variable: %s\n", (unsigned int)pthread_self(), shared);
    pthread_mutex_unlock(&mutex);
}

int main()
{
    pthread_t t1;
    pthread_t t2;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
    pthread_mutex_init(&mutex, &attr);
    pthread_create(&t1, NULL, (void *)func1, NULL);
    pthread_create(&t2, NULL, (void *)func2, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}

输出如下:

thread id: 0xf7d9f700, func1 shared variable: func1
thread id: 0xf759e700, func2 shared variable: func2
thread id: 0xf759e700, func1 shared variable: func1
thread id: 0xf759e700, back in func2, shared variable: func1

总结

线程安全: 多线程同时操作同一片临界区时,可能会导致临界区资源改变。所以临界区资源在多线程操作时需要做保护,否则可能导致操作结果错误。解决线程安全的方法就是对资源进行加线程锁。
互斥锁: 互斥锁(mutex)是最基础的线程锁,其他的线程锁均在互斥锁的基础下实现。互斥锁是多个线程共享一个互斥量,当互斥量被设置时,其他线程只能等待互斥量被解放才能继续操作。互斥锁是睡眠等待机制,在等待过程不会占用系统资源。互斥锁是非递归锁。
条件锁: 条件锁(condition_variable)也就条件变量,当线程在条件不满足会进入休眠,一直等待条件变量满足再往下执行。条件锁需要通过notify_one和notify_all唤醒,再执行条件判断。条件锁可以实现线程同步。
读写锁: 读写锁是区分多线程中读操作和写操作的线程锁,也叫共享-独占锁,读锁共享,写锁独占。由于多线程间同时读临界区变量,不会造成线程安全问题,但要保证读操作时不会发生写操作。读写锁在上读锁时,其他线程也可以获取读锁,其他线程想获取写锁时就需要等待读锁解开。当读写锁上写锁时,其他线程无法获取写锁和读锁。
自旋锁: 自旋锁使用方式与互斥锁类似。自旋锁为忙等待机制,自旋锁在等待锁时,会占用CPU,因为自旋锁实际上是循环判断锁的状态。
递归锁: 递归锁的特点是在同一线程可以重复获得锁而不死锁。区分于非递归锁,非递归锁在同一线程重复尝试获得锁时,就会出现死锁。

以上均是个人愚见,如果有哪里说得不对的地方请大家指出。

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.项目代码均经过功能验证ok,确保稳定可靠运行。欢迎下载体验!下载完使用问题请私信沟通。 2.主要针对各个计算机相关专业,包括计算机科学、信息安全、数据科学与大数据技术、人工智能、通信、物联网等领域的在校学生、专业教师、企业员工。 3.项目具有丰富的拓展空间,不仅可作为入门进阶,也可直接作为毕设、课程设计、大作业、初期项目立项演示等用途。 4.当然也鼓励大家基于此进行次开发。在使用过程中,如有问题或建议,请及时沟通。 5.期待你能在项目中找到乐趣和灵感,也欢迎你的分享和反馈! 【资源说明】 基于C和C++开发的多线程高并发服务器源码+项目说明(课程设计).zip 基于Linux操作系统下由C语言编写的服务器引擎,该引擎由四个层次组成: | 层次 | 说明 | | -------------- | ------------------------------------------------- | | Process Layer | 处理层 数据的识别,处理与输出等数据加工接口 | | Network Layer | 网络层 基础的网络协议与网络数据传输等网络相关接口 | | Resource Layer | 资源层 相关文件操作和数据库操作等资源相关接口 | | Core Layer | 核心层 基础的数据结构和数学函数等基础函数接口 | 以上四个层次由低至高为 Core Layer -> Resource Layer -> Network Layer -> Process Layer,应基于"高层能调用低层的接口,而低层无法调用高层的接口"的原则来构建该引擎 用户应使用Linux操作系统运行以下代码(已用makefile文件集合),以在bin文件夹输出server.out可执行文件 ```shell make ``` 更多详细细节见项目说明!!!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值