首页 > 编程语言 >C#多线程(二)同步基础篇

C#多线程(二)同步基础篇

时间:2022-11-16 23:35:52浏览次数:69  
标签:同步 Thread C# lock testOutputHelper 线程 WriteLine 多线程 waitHandle

C#多线程(二)同步基础篇

 

回顾上节:

我们对多线程已经有了基础的认知,知道其工作原理和一些基本维护操作,并且引出了线程安全的概念。这一篇我们要讲的主题--同步,是解决线程安全问题的一个手段之一,线程安全是整个多线程的核心挑战,几乎所有的手段都是在与他对抗!整个系列将通篇围绕线程安全开展。

一、基本概念

线程安全

线程安全(thread safe):指的是被任意多的线程同时执行,都可以保证正确性。

除基本类型外,很少有类型是线程安全的,线程安全的责任基本落在开发者身上,System.Collections.Concurrent命名空间下的类型的除外。

  • 线程安全最常见的手段一般是使用【排它锁】,将大段代码甚至是访问的整个对象封装在一个排它锁内,从而保证在高层上能进行顺序访问。
    这种解决方案适用于对象的方法都能够快速执行的场景(否则会导致大量的阻塞)。
  • 还有一种手段很高明,即通过【最小化共享数据】来减少【线程交互】,web服务器就是最好的案例,由于多个客户端请求可以同时到达,服务端方法必须保证线程安全。类似的案例还有【无状态】设计,在本质上限制了
    数据交互的可能,具有良好的伸缩性(scalability)。
  • 还有一种手段,【自动锁机制(automatic locking)】如果继承 ContextBoundObject 类并使用 Synchronization 特性,.NET Framework 就可以实现这种机制,framework全系支持,但是netcore没有,类似java的synchronized。

尽管这样降低了开发者实现线程安全的负担,但范围过大的锁定作用域将制造出巨大的麻烦:死锁、非有意的重入以及降低并发度。这使得手动锁定在任何场景都显得更为合适,而并不仅仅只在简单的场景下(直到有更好用的自动锁机制出现)。

  • 其它手段,【信号构造】,【内存屏障】,【自旋构造】。。。

同步与阻塞

同步(synchronization):指对在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象 -- 为期望的结果协调多个线程的行为。

当多个线程访问同一个数据时,同步尤为重要,但这是一件非常容易G的事情。

同步对象(synchronized object):对所有参与同步的线程可见的任何对象都可以被当作同步对象使用,但有一个硬性规定:同步对象必须为引用类型。同步对象一般是私有的(因为这有助于封装锁逻辑)。

同步对象也可以就是其要保护的对象。

class ThreadSafe
{
  List <string> _list = new List <string>();

  void Test()
  {
    lock (_list)
    {
      _list.Add ("Item 1");
      // ...

一个只被用来加锁的字段可以精确控制锁的作用域与粒度。

private static readonly object _locker = new object();

对象自己(this),甚至是类型,lambda 表达式或匿名方法所捕获的局部变量 都可以被当作同步对象来使用:

lock (this) { ... }
// 或者:
lock (typeof (Widget)) { ... }    // 保护对静态资源的访问

但这种方式的缺点在于并没有对锁逻辑进行封装,从而很难避免【死锁】或过多的【阻塞】。 同时类型上的锁也可能会跨越应用程序域(application domain)边界(在同一进程内)。

阻塞(block):当线程的执行由于某些原因被暂停,比如【信号构造】或【锁构造】时,比如调用Thread.Sleep,Task.Wait,或者通过Join方法等待其它线程结束时,则认为此线程被阻塞(blocked)。

阻塞会在以下 4 种情况下解除(电源按钮可不能算╮(╯▽╰)╭):

  • 阻塞条件被满足
  • 操作超时(如果指定了超时时间)
  • 通过Thread.Interrupt中断
  • 通过Thread.Abort中止

编译器将async Task转换为状态机,到达 await 时暂停执行等待后台作业完成时继续执行。从理论上讲,这是异步的承诺模型的实现。)

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    // do samething..
    var toast = await ToastBreadAsync(number);
    // do samething..
    return toast;
}

同步构造

锁构造(lock):锁能够限制同一时刻可以执行某些指令或是某段代码的线程数量。排他锁是最常见的,它只允许同一时刻至多有一个线程执行,从而可以使得参与竞争的线程在访问公共数据时不会彼此干扰。
一般的排他锁有lock(Monitor.Enter/Monitor.Exit)、Mutex、SpinLock,非排他锁有Semaphore、SemaphoreSlim以及reader/writer lock。

信号构造(signal):信号构造可以使一个线程【挂起】,直到接收到另一个线程的通知,避免了低效的轮询 。有两种经常使用的信号设施:事件等待句柄(event wait handle )和Monitor类的Wait / Pulse方法。
Framework 4.0 加入了CountdownEvent与Barrier类。

自旋(spinning):有时线程必须阻塞/暂停,直至条件被满足,【信号构造】或【锁构造】可以实现,但在等待条件能够在微秒级的时间被满足时,
自旋往往更加高效,因为它避免了上下文切换带来的昂贵开销。

while (!condition);

自旋往往与阻塞组合使用,防止cpu浪费

while (!condition) Thread.Sleep (10);

线程状态(thread state):Unstarted、Running、WaitSleepJoin、Stopped。。

原子性

指令原子性(instruction atomically):如果一组【指令】可以在 CPU 上不可分割地执行,那么它就是原子的

原子性(atomically):如果一组变量总是在相同的锁内进行读写,就可以称为原子性读写

lock (locker) { if (x != 0) y /= x; }

可以说x和y是被原子的访问的,因为上面的代码块无法被其它的线程分割或抢占(cpu悲观锁/总线锁)。如果被其它线程分割或抢占,x和y就可能被别的线程修改导致计算结果无效(cpu乐观锁/缓存锁)。而现在 x和y总是在相同的排它锁中进行访问,因此不会出现除数为零的错误。

如果lock代码块内发生异常,原子性将被打破

decimal _savingsBalance, _checkBalance;

void Transfer (decimal amount)
{
  lock (_locker)
  {
    _savingsBalance += amount;
    _checkBalance -= amount + GetBankFee();
  }
}

如果GetBankFee()方法内抛出异常,银行可能就要亏钱了。在这个例子中,我们可以通过更早的调用GetBankFee()来规避这个问题。对于更复杂情况,解决方案是在catch/finally中实现“回滚(rollback)”逻辑。

二、锁构造

Monitor

C# 的lock语句是一个语法糖,它其实就是使用了try / finally来调用Monitor.EnterMonitor.Exit方法:

bool taken = false;
try
{
    // JIT应该内联此方法,以便在典型情况下优化lockTaken参数的检查。
    Monitor.Enter(_locker,ref taken);
    num++;
}
finally
{
    // C# 4.0 解决锁泄露问题
    if (taken) Monitor.Exit(_locker);
}

Monitor是【可重入的(Reentrant)】,只有当最外层的lock语句退出或是执行了匹配数目的Monitor.Exit语句时,对象才会被解锁。

static void Main()
{
    lock (locker)  // 线程只会在第一个(最外层)lock处阻塞。
    {
        AnotherMethod();
        // 这里依然拥有锁,因为锁是可重入的
    }
}

static void AnotherMethod()
{
  lock (_locker) { Console.WriteLine ("Another method"); }
}

Monitor的性能:在一个 2010 时代的计算机上,没有竞争的情况下获取并释放锁一般只需 20 纳秒。如果存在竞争,产生的上下文切换会把开销增加到微秒的级别,并且线程被重新调度前可能还会等待更久的时间。如果需要锁定的时间很短,那么可以使用【自旋锁(SpinLock)】来避免上下文切换的开销。

Monitor还提供了一个TryEnter方法,允许以毫秒或是TimeSpan方式指定超时时间。如果获得了锁,该方法会返回true,而如果由于超时没有获得锁,则会返回false。TryEnter也可以不传递超时时间进行调用,这是对锁进行“测试”,如果不能立即获得锁就会立即返回false。

如果获取锁后保持的时间太长而不释放,就会降低并发度,同时也会加大【死锁】的风险。

Mutex

Mutex互斥体类似于Monitor,不同在于它是可以跨越进程工作。换句话说,Mutex可以是机器范围(computer-wide)的,也可以是程序范围(application-wide)的。

没有竞争的情况下,获取并释放Mutex需要几微秒的时间,大约比lock慢 50 倍。

使用Mutex类时,可以调用WaitOne方法来加锁,调用ReleaseMutex方法来解锁。关闭或销毁Mutex会自动释放锁。与lock语句一样,Mutex只能被获得该锁的线程释放。

跨进程Mutex的一种常见的应用就是确保只运行一个程序实例。

// 命名的 Mutex 是机器范围的,它的名称需要是唯一的
// 比如使用公司名+程序名,或者也可以用 URL
using (var mutex = new Mutex (false, "Global\oreilly.com OneAtATimeDemo"))
{
  // 可能其它程序实例正在关闭,所以可以等待几秒来让其它实例完成关闭
  if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false))
  {
    Console.WriteLine ("Another app instance is running. Bye!");
    return;
  }
  RunProgram();
}

如果在终端服务(Terminal Services)下运行,机器范围的Mutex默认仅对于运行在相同终端会话的应用程序可见。要使其对所有终端会话可见,需要在其名字前加上Global\。

死锁

当两个线程等待的资源都被对方占用时(A等B,B等A),它们都无法执行,这就产生了死锁。更复杂的死锁链可能由三个或更多的线程创建。

object locker1 = new object();
object locker2 = new object();

new Thread(() =>
{
    lock (locker1)
    {
        Thread.Sleep(1000);
        lock (locker2)  // 死锁
        {
            // do something..
        }
    }
}).Start();

lock (locker2)
{
    Thread.Sleep(1000);
    lock (locker1)  // 死锁
    {
        // do something..
    }
}

CLR 不会像SQL Server一样自动检测和解决死锁。除非你指定了锁定的超时时间,否则死锁会造成参与的线程无限阻塞。(在SQL CLR 集成宿主环境中,死锁能够被自动检测,并在其中一个线程上抛出可捕获的异常。)

死锁是多线程中最难解决的问题之一,尤其是在有很多关联对象的时候。这个困难在根本上在于无法确定调用方(caller)已经拥有了哪些锁。

你可能会锁定类x中的私有字段a,而并不知道调用方(或者调用方的调用方)已经锁住了类y中的字段b。同时,另一个线程正在执行顺序相反的操作,这样就创建了死锁。讽刺的是,这个问题会由于(良好的)面向对象的设计模式而加剧,因为这类模式建立的调用链直到运行时才能确定。

流行的建议:“以一致的顺序对对象加锁以避免死锁”,尽管它对于我们最初的例子有帮助,但是很难应用到刚才所描述的场景。更好的策略是:如果发现在锁区域中的对其它类的方法调用最终会引用回当前对象,就应该小心,同时考虑是否真的需要对其它类的方法调用加锁(往往是需要的,但是有时也会有其它选择)。更多的依靠声明方式(declarative)与数据并行(data parallelism)、不可变类型(immutable types)与非阻塞同步构造( nonblocking synchronization constructs),可以减少对锁的需要。

有另一种思路:当你在拥有锁的情况下访问其它类的代码,对于锁的封装就存在潜在的泄露。这不是 CLR 或 .NET Framework 的问题,而是因为锁本身的局限性。某些人认为造成这样问题的根因是可重入?

三、信号构造

SemaphoreSlim

信号量类似于一个通道:它具有一定的容量(capacity)房间,并且有保安把守。一旦满员,就不允许其他人进入,这些人将在外面排队。当有一个人离开时,排在最前头的人便可以进入。

容量为1的的信号量就是一把互斥锁,类似mutex,不同的是信号量没有【所有者】,它是线程无关(thread-agnostic)的。任何线程都可以在调用Semaphore上的Release方法,而对于mutex,只有获得锁的线程才可以释放该锁。

下面是创建一个初始可授予数为3,最大并发授予数为5的信号量(默认是int.MaxValue

private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(3, 5);

SemaphoreSlim是 standard1.0 就支持的轻量级的信号量,功能与Semaphore相似,不同之处是它对于并行编程的低延迟需求做了优化;支持在等待时指定取消标记 (cancellation token)。但它不能跨进程使用,Semaphore可以。

在Semaphore上调用WaitOne或Release会产生大概 1 微秒的开销(无竞争情况下),而SemaphoreSlim产生的开销约是其四分之一。

我们开模拟5个线程并行执行:

[Theory]
[InlineData(5)]
void 循环测试SemaphoreSlim(int threadCnt)
{
    Parallel.For(0, threadCnt, 进来玩);
}

进来玩方法的实现:

private void 进来玩(int x)
{
	_testOutputHelper.WriteLine(x + "想进来");
	_semaphoreSlim.Wait(); // - 1 尝试进入房间   thread.sleep(-1)
	_testOutputHelper.WriteLine(x + "进来了");
	Thread.Sleep(1000);  // 业务逻辑
	_testOutputHelper.WriteLine(x + "溜了");
	_semaphoreSlim.Release(); // + 1 可用容量
}

输出:可以看到,"房间"中最多只会同时有三个线程,其它想进来的线程必须等“房间”里面的线程出来

3想进来
1想进来
2想进来
4想进来
0想进来
4进来了
3进来了
0进来了
4溜了
0溜了
3溜了
2进来了
1进来了
2溜了
1溜了

EventWaitHandle

EventWaitHandle允许线程通过信号相互通信。 通常,在解除阻塞的线程调用Set之前,调用WaitHandle.WaitOne()的线程会一直阻塞。

EventWaitHandle提供两个重要的方法:

  • Set:将事件的状态设置为已发出信号,允许一个或多个WaitOne线程继续(好比开“门”)
  • Reset:将事件的状态设置为无信号,导致WaitOne线程阻塞(好比关“门”)

EventWaitHandle的构造方法允许以命名的方式进行创建,这样它就可以跨进程使用。名称就是一个字符串,可以随意起名

EventWaitHandle wh = new EventWaitHandle (false,EventResetMode.AutoReset,"MyCompany.MyApp.SomeName");

注意命名冲突!如果名字在计算机上已存在,你就会获取一个它对应的EventWaitHandle的引用,否则操作系统会创建一个新的,createdNew会告诉你这个事情。

public EventWaitHandle(bool initialState, EventResetMode mode, string? name, out bool createdNew)

ManualResetEvent

ManualResetEvent是继承EventWaitHandle的密封类

public sealed class ManualResetEvent : EventWaitHandle

内部实现非常简单,只提供了一个构造函数,initialState用于标识初始状态下,“门”是开的还是关的

public ManualResetEvent(bool initialState) : base(initialState, EventResetMode.ManualReset) { }

ManualResetEvent调用WaitOne进入阻塞,任意可访问的线程都能调用Set方法来放行。

var waitHandle = new ManualResetEvent(false);
// var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
Task.Run(() =>
{
    _testOutputHelper.WriteLine(Thread.CurrentThread.ManagedThreadId + " 尝试进门...");
    waitHandle.WaitOne();
    _testOutputHelper.WriteLine(Thread.CurrentThread.ManagedThreadId + " 进去了");
    业务逻辑();
    _testOutputHelper.WriteLine("当前门的状态是开启的吗?"+waitHandle.WaitOne(0)); //true
});
Thread.Sleep(1000);
_testOutputHelper.WriteLine(Thread.CurrentThread.ManagedThreadId + " say:我来开门");
waitHandle.Set();

输出:

5 尝试进门...
15 say:我来开门
5 进去了
5 开始干活!
当前门的状态是开启的吗?True

从 Framework 4.0 开始,提供了另一个版本的ManualResetEvent,名为ManualResetEventSlim。 后者为短等待时间做了优化,它提供了进行一定次数迭代自旋的能力,也实现了一种更有效的管理机制,允许通过CancellationToken取消Wait等待。但它不能用于跨进程的信号同步。 ManualResetEventSlim不是WaitHandle的子类,但它提供一个WaitHandle的属性,会返回一个基于WaitHandle的对象(使用它的性能和一般的等待句柄相同)。

AutoResetEvent

AutoResetEvent如其命名,收到通知后他能自动复位(reset),而ManualResetEvent不能。

EventWaitHandle waitHandle = new AutoResetEvent(false);
// var waitHandle2 = new EventWaitHandle(false, EventResetMode.AutoReset);
Task.Factory.StartNew(() =>
{
    _testOutputHelper.WriteLine(Thread.CurrentThread.ManagedThreadId + " 尝试进门...");
    waitHandle.WaitOne();
    _testOutputHelper.WriteLine("当前门的状态是开启的吗?"+waitHandle.WaitOne(0));
    _testOutputHelper.WriteLine(Thread.CurrentThread.ManagedThreadId + " 进去了");
    业务逻辑();
    _testOutputHelper.WriteLine("当前门的状态是开启的吗?"+waitHandle.WaitOne(0));
    waitHandle.Set();
    Task.Run(() =>
    {
        waitHandle.WaitOne();
        _testOutputHelper.WriteLine(Thread.CurrentThread.ManagedThreadId + " 进去了");
        业务逻辑();
    });
});
Thread.Sleep(1000);
_testOutputHelper.WriteLine(Thread.CurrentThread.ManagedThreadId + " say:我来开门");
waitHandle.Set();
Thread.Sleep(1000);  // 等待worker

输出:

13 尝试进门...
17 say:我来开门
当前门的状态是开启的吗?False
13 进去了
13 开始干活!
当前门的状态是开启的吗?False
13 进去了
13 开始干活!

我们可以使用AutoResetEvent实现一个简易的生产消费队列:

public class PCQueue:IDisposable
{
    private EventWaitHandle _waitHandle = new AutoResetEvent(false);
    private Thread _worker;
    private readonly object _locker = new object();
    private Queue<string> _tasks = new Queue<string>();

    private readonly ITestOutputHelper _testOutputHelper;

    public PCQueue(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;

        _worker = new Thread(Work);
        _worker.Start();
    }

    public void AddTask(string task)
    {
        lock (_locker)
        {
            _tasks.Enqueue(task);
        }

        _waitHandle.Set();
    }

    private void Work()
    {
        while (true)
        {
            string task = null;
            lock (_locker)
            {
                if (_tasks.Count > 0)
                {
                    task = _tasks.Dequeue();
                    if (task == null) return;  // null为退出任务
                }
            }

            if (task == null)
            {
                _waitHandle.WaitOne();  // 没有任务,进入阻塞,等待新的任务
            }
            else
            {
                _testOutputHelper.WriteLine("执行任务:" +task);
                Thread.Sleep(1000);  // 模拟耗时任务
            }
        }
    }

    public void Dispose()
    {
        AddTask(null);  // 通知消费线程退出
        _worker.Join();  // 等待消费线程执行完成
        _waitHandle.Close();  // 释放事件句柄
    }
}

使用:

using (var queue = new PCQueue(_testOutputHelper))
{
    queue.AddTask("hello");
    for (int i = 1; i <= 5; i++)
    {
        queue.AddTask("say " + i);
    }
    queue.AddTask("bye");
}

输出:

执行任务:hello
执行任务:say 1
执行任务:say 2
执行任务:say 3
执行任务:say 4
执行任务:say 5
执行任务:bye

CountdownEvent

CountdownEvent可以用于多线程等待,这个类型是 Framework 4.0 加入的,是一个高效的纯托管实现。

构造CountdownEvent时,需要指定一个初始计数

public CountdownEvent(int initialCount)

常用的方法

public bool Signal()  // -1
public bool Signal(int signalCount)  // -signalCount
public void AddCount()  // +1
public void AddCount(int signalCount)  // +signalCount
public void Wait()  // 阻塞,直到计数==0
public void Wait(CancellationToken cancellationToken)
public bool Wait(TimeSpan timeout)

下面这个例子,初始一个计数为3的计数器,启动三个工作线程

CountdownEvent _countdown = new CountdownEvent (3);

void 测试CountdownEvent等待()
{
    Task.Run(工作);
    Task.Run(工作);
    Task.Run(工作);

    _countdown.Wait();
    _testOutputHelper.WriteLine("大家都干完了");
}

void 工作()
{
    _testOutputHelper.WriteLine("线程:"+Thread.CurrentThread.ManagedThreadId+" 正在干活");
    Thread.Sleep(1000);
    _countdown.Signal();
}

输出:

线程:7 正在干活
线程:8 正在干活
线程:5 正在干活
大家都干完了

CountdownEvent的所有公共成员和受保护成员都是线程安全的,可以在多个线程同时使用。但Dispose()Reset()除外,Dispose只能在CountdownEvent上的所有其他操作都完成时使用,Reset只能在没有其他线程访问事件时使用。

四、等待句柄

WaitHandle

WaitHandle类上还有一些静态方法用来解决更复杂的同步问题。

WaitAnyWaitAllSignalAndWait方法可以向多个等待句柄发信号和进行等待操作。等待句柄可以是不同的类型(包括 MutexSemaphoreCountdownEvent等,因为它们都派生自抽象类WaitHandle)。

  • waitAny 等待一组等待句柄中任意一个

  • WaitAll 等待给定的所有等待句柄。这个等待是原子的。

  • SignalAndWait会调用一个等待句柄的Set方法,然后调用另一个等待句柄的WaitOne方法。在向第一个句柄发信号后,会(让当前线程)跳到第二个句柄的等待队列的最前位置(插队)。

    WaitHandle.SignalAndWait (wh1, wh2);
    

等待句柄和线程池

如果你的应用有很多线程,这些线程大部分时间都在阻塞,那么可以通过调用ThreadPool.RegisterWaitForSingleObject来减少资源消耗。当向等待句柄waitObject发信号Set时(或者已超时millisecondsTimeOutInterval),委托callBack会在一个线程池线程运行。executeOnlyOnce设置请求是一次性的还是可重复的。

public static RegisteredWaitHandle RegisterWaitForSingleObject(
             WaitHandle waitObject,
             WaitOrTimerCallback callBack,
             object? state,
             int millisecondsTimeOutInterval,
             bool executeOnlyOnce    // NOTE: we do not allow other options that allow the callback to be queued as an APC
             )

下面这个例子,主线程在3s后向等待句柄发送信号,然后Work方法被一个线程池线程执行

[Fact]
void Show()
{
    var _waitHandle = new ManualResetEvent(false);
    _testOutputHelper.WriteLine ("thread id - " + Thread.CurrentThread.ManagedThreadId);
    var reg = ThreadPool.RegisterWaitForSingleObject(_waitHandle, Work, "hahah", -1, true);
    Thread.Sleep(3000);
    _testOutputHelper.WriteLine("发送复位信号");
    _testOutputHelper.WriteLine ("thread id - " + Thread.CurrentThread.ManagedThreadId);
    _waitHandle.Set();
    reg.Unregister(_waitHandle);
}
private void Work (object data, bool timedOut)
{
    _testOutputHelper.WriteLine ("Say - " + data);
    
    _testOutputHelper.WriteLine ("thread id - " + Thread.CurrentThread.ManagedThreadId);
    // 执行任务 ....
}

输出:

thread id - 22
发送复位信号
thread id - 22
Say - hahah
thread id - 28

可以发现,发送复位信号的和执行Work的不是同一个线程

标签:同步,Thread,C#,lock,testOutputHelper,线程,WriteLine,多线程,waitHandle
From: https://www.cnblogs.com/sexintercourse/p/16897909.html

相关文章

  • C#多线程(三)线程高级篇
    C#多线程(三)线程高级篇 前言抛开死锁不谈,只聊性能问题,尽管锁总能粗暴的满足同步需求,但一旦存在竞争关系,意味着一定会有线程被阻塞,竞争越激烈,被阻塞的线程越多,上下文切......
  • QtCreator常用快捷键
    快捷键功能Esc切换到代码编辑状态F1查看帮助(选中某一类或函数,按下F1,出现帮助文档)F2在光标选中对象的声明和定义之间切换(和Ctrl+鼠标左键一样的效果,选中......
  • plugin caching_sha2_password could not be loaded
    LineInternetThesameerrorintheupblog.ALTERUSER'root'@'localhost'IDENTIFIEDWITHmysql_native_passwordBY'yourselfpassword'.......
  • Linux环境下配置vscode的C/C++ 的make编译环境(编写makefile方式)代码Demo版
    以前写过同样话题下的图文版的,这里给出一个代码Demo版本,上一个图文版本参见:​​Linux环境下配置vscode的C/C++的make编译环境(编写makefile方式)​​  ===================......
  • tensorflow1.x——如何在C++多线程中调用同一个session会话tensorflow1.x
     =================================================  从前文​​tensorflow1.x——如何在python多线程中调用同一个session会话​​可以知道,使用python多线程调用同一......
  • NLP入门之——Word2Vec词向量Skip-Gram模型代码实现(Pytorch版)
    代码地址:https://github.com/liangyming/NLP-Word2Vec.git1.什么是Word2VecWord2vec是Google开源的将词表征为实数值向量的高效工具,其利用深度学习的思想,可以通过训练......
  • pytest文档82 - 用例收集钩子 pytest_collect_file 的使用
    前言pytest提供了一个收集用例的钩子,在用例收集阶段,默认会查找test_*.py文件或者*_test.py文件。如果我们想运行一个非python的文件,比如用yaml文件写用例,那么就需要......
  • P3469 [POI2008]BLO-Blockade
    P3469[POI2008]BLO-Blockade#include<bits/stdc++.h>usingnamespacestd;usingll=longlong;constintN=2e5+5;constintM=2e6+5;inth[N],ne[M],e[M],tot;v......
  • Codeforces Round #833 (Div. 2) D
    D.ConstructOR转化题意a|x=k1db|x=k2d我们考虑k1k2同样就只用让x包含a|b对于a|b的每一位我们用d的最后一位来填补然后在线的要是a|b这里有我们的x没有显然要让......
  • 多线程
    程序同时执行多个任务使用线程可以把占据长时间的程序中的任务放到后台去处理。程序的运行速度可能加快一、线程实现方法  线程是CPU分配资源的基本单位。当一......