C++多线程学习笔记(一)

互斥锁mutex

确保多线程时,所有的线程不在同一时间访问相同的资源。
API
lock()

(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

unlock()

解锁,释放对互斥量的所有权。

try_lock()

1.如果互斥锁当前未被任何线程锁定,则调用线程将其锁定(从此点开始,直到调用其成员解锁,该线程拥有互斥锁)。
2.如果互斥锁当前被另一个线程锁定,则该函数将失败并返回false,而不会阻塞(调用线程继续执行)。
3.如果互斥锁当前被调用此函数的同一线程锁定,则会产生死锁(具有未定义的行为)。
解锁,释放对互斥量的所有权。

lock_guard

lock_guard取代了mutex的lock()和unlock()。

lock_guard<mutex> lker(mtx)  //只要离开当前作用域,就会释放锁

unique_lock

unique_lock所有权的传递 mutex

unique_lock<mutex> sbguard1(my_mutex1);

example:leetcode1114.按序打印

class Foo {
    mutex mtx1, mtx2;
public:
    Foo() {
        mtx1.lock(), mtx2.lock();
    }

    void first(function<void()> printFirst) {
        printFirst();
        mtx1.unlock();
    }

    void second(function<void()> printSecond) {
        mtx1.lock();
        printSecond();
        mtx1.unlock();
        mtx2.unlock();
    }

    void third(function<void()> printThird) {
        mtx2.lock();
        printThird();
        mtx2.unlock();
    }
};

作者:zintrulcre
链接:https://leetcode-cn.com/problems/print-in-order/solution/c-hu-chi-suo-tiao-jian-bian-liang-xin-hao-liang-yi/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

原子操作atomic

  • C++11引入了原子操作的概念,提供了多种原子操作数据类型,如atomic ,在多线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的。
  • 能像mutex一样保证线程安全,但是atomic的操作是原子性的,不用使用锁,提高了效率。

example

#include <thread>
#include <vector>
#include <iostream>
#include <atomic>
 
std::atomic_flag lock = ATOMIC_FLAG_INIT;
 
void f(int n)
{
	for (int cnt = 0; cnt < 5; ++cnt) {
		while (lock.test_and_set(std::memory_order_acquire))  // 获得锁
			; // 自旋
		std::cout << "Thread " << n << " count:" << cnt << std::endl;
		lock.clear(std::memory_order_release);               // 释放锁
	}
}
 
int main(int argc, char* argv[])
{
	std::vector<std::thread> v;
	for (int n = 0; n < 4; ++n) {
		v.emplace_back(f, n); //使用参数进行初始化
	}
	for (auto& t : v) {
		t.join(); //等待线程结束
	}
 
	system("pause");
	return 0;
}
链接:https://blog.csdn.net/gongjianbo1992/article/details/102458841

条件变量CV

为什么要用条件变量(condition_variable):

当一个线程等待另一个线程完成任务时,等待线程需要持续检查互斥量,或者周期性地检查互斥量,这两种方法都会造成资源浪费,使用条件变量的话,当满足条件的时候会自动唤醒等待线程。

API:

条件变量的使用需要结合互斥量。当条件满足线程执行某种操作,条件不满足时,条件变量(wait)操作自动阻塞该线程。当有另外的线程修改了条件时,会激活阻塞的线程,阻塞线程重新评价条件。

wait()

  • 用来等一个事件或者条件满足。 如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行;
  • 如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行;
  • 阻塞到其他某个线程调用notify_one()成员函数为止;
  • 如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样;
  • wait()将解锁互斥量,并阻塞到本行,阻塞到其他某个线程调用**notify_one()**成员函数为止。

notify_one()

通知解除一个线程的阻塞状态,只有当另外一个线程正在执行wait()时notify_one()才会起效,如果有多个线程,notify_one随机选择线程唤醒

notify_all()

通知解除所有线程的阻塞状态,如果有多个线程,唤醒所有线程但线程之间争夺锁

example

std::mutex mymutex1;
std::unique_lock<std::mutex> sbguard1(mymutex1);
std::condition_variable condition;
condition.wait(sbguard1, [this] {if (!msgRecvQueue.empty())
                                    return true;
                                return false;
                                });
condition.wait(sbguard1);
链接:https://blog.csdn.net/weixin_42002291/article/details/113796630

这里要使用unique_lock而非lock_guard(因wait时会解除锁定,当获取信号后再锁定)

事件event

1.事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
3.事件可以解决线程间同步问题,因此也能解决互斥问题。
链接:https://blog.csdn.net/morewindows/article/details/7445233

API
CreateEvent

创建事件。

OpenEvent

根据名称获得一个事件句柄。

SetEvent

触发事件。
每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。

ResetEvent

将事件设为末触发。

信号量semaphore

Linux内核的信号量用来操作系统进程间同步访问共享资源。

原理

信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。

  • 一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。
  • 当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

转自:https://blog.csdn.net/langjian2012/article/details/39717903

PV操作

P操作

  • sem减1;
  • 若sem>=0,则P操作返回,该线程程可以“通过”并继续执行。
  • 若sem<0,则该线程被阻塞,进入操作系统的阻塞队列。

V操作

  • sem加1(释放)。
  • 若sem>0,则V操作返回,该线程继续执行。
  • 若sem<=0,则从阻塞队列中唤醒一个阻塞在该信号量上的线程,然后再返回原线程(调用ν操作的线程)继续执行。

源码:semaphore.h

#pragma once
#include<mutex>
#include<condition_variable>
class semaphore {
public:
	semaphore(long count = 0) :count(count) {}
	void wait() {
		std::unique_lock<std::mutex>lock(mx);
		cond.wait(lock, [&]() {return count > 0; });
		--count;
	}
	void signal() {
		std::unique_lock<std::mutex>lock(mx);
		++count;
		cond.notify_one();
	}

private:
	std::mutex mx;
	std::condition_variable cond;
	long count;
};

API
sem_init

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 初始化sem_t型变量,并设置初始信号量。
  • pshraed是semaphore需要跨越使用的进程数,设置0时只在一个进程中使用,设置1时可以在两个进程中使用,以此类推;
  • value是设置信号量设置的初始值,也是信号量的最大值;

sem_destroy

int sem_destroy(sem_t *sem);

sem_wait

int sem_wait(sem_t *sem);
  • 等待函数就是去打开锁的操作,在进入临界区前调用。
  • 如果semaphore的值为0,它将会阻塞;
  • 如果semaphore的值不为0,调用它后信号量的值将会减1;

sem_post

int sem_post(sem_t* sem);

在离开临界区后调用,每调用一次信号量的值就会加1。

example1

#include <stdio.h> // printf(),
#include <stdlib.h> // exit(), EXIT_SUCCESS
#include <pthread.h> // pthread_create(), pthread_join()
#include <semaphore.h> // sem_init()

sem_t binSem;

void* helloWorld(void* arg) {
    while(1) {
         // Wait semaphore
         sem_wait(&binSem);
         printf("Hello World\n");
     }
}

int main() {
     // Result for System call
    int res = 0;

     // Initialize semaphore
     res = sem_init(&binSem, 0, 0);
    if (res) {
         printf("Semaphore initialization failed!!\n");
         exit(EXIT_FAILURE);
     }

     // Create thread
     pthread_t thdHelloWorld;
	// 第一个参数是要绑定的线程对象,第二个参数是可以被用来设置线程属性,一般使用NULL,第三个参数需要绑定的函数名称,第四个参数是函数传入的参数
     res = pthread_create(&thdHelloWorld, NULL, helloWorld, NULL);
    if (res) {
         printf("Thread creation failed!!\n");
         exit(EXIT_FAILURE);
     }

    while(1) {
         // Post semaphore
         sem_post(&binSem);
         printf("In main, sleep several seconds.\n");
        sleep(1);
     }

     // Wait for thread synchronization
     void *threadResult;
     res = pthread_join(thdHelloWorld, &threadResult);
    if (res) {
         printf("Thread join failed!!\n");
         exit(EXIT_FAILURE);
     }

     exit(EXIT_SUCCESS);
}

链接:https://www.jianshu.com/p/4fdad407068b

example2:leetcode 1226. 哲学家进餐

#include <semaphore.h>

class DiningPhilosophers {
private:
    sem_t sem;
    std::mutex mutexs[5];
public:
    DiningPhilosophers() {
        sem_init(&sem, 0, 4); // 初始化信号量
    }

    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {
        int lhs = philosopher;
        int rhs = (philosopher+1)%5;

        sem_wait(&sem);
        {
            std::lock_guard<std::mutex> lock1(mutexs[lhs]);
            std::lock_guard<std::mutex> lock2(mutexs[rhs]);

            pickLeftFork();
            pickRightFork();
            eat();
            putLeftFork();
            putRightFork();
        }
        sem_post(&sem);
    }
};

作者:XYU5mz8u36
链接:https://leetcode-cn.com/problems/the-dining-philosophers/solution/csan-chong-si-lu-jie-jue-zhe-xue-jia-jiu-can-by-xy/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

待续。。。

[1]: C++ std::atomic_flag与sta::atomic布尔标志
[2]: condition_variable、wait、notify_one、notify_all
[3]: C++11中线程及信号量与条件变量
[4]: 信号量的实现
[5]: Linux信号(signal) 机制和Linux信号量(semaphore)机制的区别
[6]: 使用信号量semaphore进行多线程同步
[7]: 同步之PV操作
[8]: 经典线程同步 事件Event

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_mountainriver

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值