Страницы

Поиск по вопросам

Показаны сообщения с ярлыком mutex. Показать все сообщения
Показаны сообщения с ярлыком mutex. Показать все сообщения

воскресенье, 29 марта 2020 г.

TryOpenExisting() и использование Discards (пустых переменных) на обьектах которые наследуют IDisposable

#c_sharp #c_sharp_faq #mutex #c_sharp_70


Мне нужно только результат функции, который записывается в переменную existing. 

Boolean existing = Mutex.TryOpenExisting(name: key, rights: MutexRights.ReadPermissions,
result: out Mutex _);


Могу ли я использовать "Пустую переменную" в этом коде? Вызовет ли она Dispose();
для этого обьекта автоматически?

Или правильным было бы обьявить переменную Mutex mutex и после вызова метода вызвать
mutex.Dispose(); ?

P.S. Кому не понятна суть вопроса, подробнее про пустые переменные тут: https://docs.microsoft.com/en-us/dotnet/csharp/discards
    


Ответы

Ответ 1



Сама документация описывает что "пустые переменные" просто ссылаются на некую "пустую" область памяти. То есть не занимают места. Что вряд ли означает что они будут вызывать Dispose() у IDisposable обьектов. Я провел личное расследование даного вопроса и написал прогу для тестирования. Простая винформс апликуха на старте которого вызывается метод: public bool TestMethod(out Image bmp) { Thread.Sleep(3000); bmp = Bitmap.FromFile(@"C:\Users\UKS\Desktop\2000x2000pixels.bmp"); Thread.Sleep(1000); return true; } Выкладываю результаты: public Form1() { InitializeComponent(); Image img; var a = TestMethod(out img); img.Dispose(); } Как видно из картинки - Память возросла и освободилась т.к. мы использовали диспоуз Теперь настал черед теста: public Form1() { InitializeComponent(); var a = TestMethod(out _); } Как видим из картинки - память НЕ освободилась когда я использовал Discard-переменную. Из чего делаем вывод что Дискарды не диспоузят данные. Теперь настал черед еще пары тестов: Конструкция: var a = TestMethod(out _.Dispose()); Не сработает. Говорит что _ не существует в даном контексте. Конструкция: var a = TestMethod(out _); _.Dispose(); Даст ровно тот же результат. Вывод: пустые переменные использовать на IDisposable обьектах категорически нельзя. Итак, самый коректный код в даном конкретном случае есть: Mutex mutex; Boolean existing = Mutex.TryOpenExisting(name: key, rights: MutexRights.ReadPermissions, result: mutex); mutex?.Dispose();//проверяем на null. Если не налл, то вызываем диспоуз.

воскресенье, 8 марта 2020 г.

Rust. Вызов метода у Arc<T>

#rust #mutex


Читаю Rustbook и не понимаю некоторые вещи.     

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let data = Arc::new(Mutex::new(vec![1u32, 2, 3]));
    for i in 0..3 {
        let data = data.clone(); //(1)
        thread::spawn(move || {
            let mut data = data.lock().unwrap(); //(2)
            data[i] += 1;
        });
    }
    thread::sleep_ms(50);
}


(1). Метод clone() возвращает тип self, т.е. Arc<_>. В этом можно убедиться, если
явно написать тип:

let data: Arc>> = data.clone();


В связи с этим возникает вопрос: почему в (2) мы вызываем метод lock() так, если
бы мы работали напрямую с Mutex? Все как в C++, но там у shared_ptr оператор -> перегружен
за нас, а покопавшись в исходниках arc.rs я не увидел подобного или пропустил.

UPD

Спасибо Кнопкатык за наводку на Deref coercions. Для закрепления написал такой искусственный
пример:

use std::ops::Deref;

#[derive(Debug)]
struct Field {
    x: i32,
}

impl Field{
    fn print(&self){
        println!("x = {}", self.x);
    }
}

#[derive(Debug)]
struct Example {
    x: Field,
}

impl Deref for Example{
    type Target = Field;

    fn deref(&self) -> &Field{
        &self.x
    }
}

fn main() {
    let s = Example { x: Field { x: 5} };
    s.print();
}


Структура Example не имеет имплементации метода print.
Но если вспомнить универсальный синтаксис вызова функций, то 

s.print();


можно заменить на:

Field::print(&s);


Следовательно, так как у нас есть реализация типажа Deref для Example, возвращающая
&Field, то значения Example, если нужно, будут автоматически преобразовываться в &Field
при разыменовании:

Field::print(&Example) -> dereference -> Field::print(&Field);


Код, как и предполагается, выведет:

x = 5

    


Ответы

Ответ 1



Дело в механизме Deref coercions или "Преобразования при разыменовании". Arc реализует типаж Deref, значения &Arc будут автоматически преобразованы в &T, когда это необходимо. Метод lock(&self) Мютекса принимает &self, и когда мы пытаемся вызвать его с Arc: lock(&Arc) срабатывает механизм Deref coercions lock(&Mutex) а &Mutex это и есть &self для метода lock.

среда, 26 февраля 2020 г.

Почему критическая секция не работает как мьютекс?

#cpp #многопоточность #winapi #mutex


У меня есть глобальная переменная global равная 100, и есть 2 потока, каждый из которых
12 раз увеличивает значение этой глобальной переменной, т.е. в итоге её значение должно
быть равным 124. Сначала я использовал для синхронизации мьютекс и всё было отлично,
т.к. потоки выполнялись поочерёдно. После я решил заменить мьютекс на критическую секцию,
и вроде бы я в итоге получаю 124, но при этом потоки не выполняются поочередно чего
я никак не мог ожидать, может быть кто-нибудь знает в чём тут дело?

Собственно код:

#include "stdafx.h" 
#include  
#include  
#include 
using namespace std;

int global = 100;
HANDLE ht1, ht2; 
CRITICAL_SECTION cs;
DWORD WINAPI ThreadProc1(LPVOID lpParameter ) { 
    int i, j;   
    for (j=1; j <= 12; j++)    {        
        EnterCriticalSection(&cs);   
        i = global;      
        i++;     
        Sleep (1);      
        global = i; 
        printf_s( "%4s %4d \n", " 1 th", i );     
        LeaveCriticalSection(&cs);   
    } 
    return 0;  
}
DWORD WINAPI ThreadProc2 (LPVOID lpParameter) { 
    int i, j; 
    for (j=1; j <= 12; j++)    {    
        EnterCriticalSection(&cs);   
        i = global;      
        i++;     
        Sleep (1);     
        global = i;
        printf_s( "%4s %4d %4d \n", " 2 th", i, j );     
        LeaveCriticalSection(&cs);    
    } 
    return 0; 
} 
int _tmain(int argc, _TCHAR* argv[]) { 
    HANDLE msh[2]; 
    InitializeCriticalSection(&cs);
    ht1 = CreateThread(NULL, 0, &ThreadProc1, NULL, 0, NULL); 
    ht2 = CreateThread(NULL, 0, &ThreadProc2, NULL, 0, NULL); 
    msh[0] = ht1; 
    msh[1] = ht2;
    Sleep(500);
    DeleteCriticalSection(&cs);
    return 0; 
} 


Такой результат я получаю с использованием мьютекса:


А вот результат с использованием критической секции:

    


Ответы

Ответ 1



А с чего им выполняться поочередно? Выделено время до переключения - и пошел-пошел-пошел, сколько успеет... Поток переключился, пошел-пошел-пошел второй... Возможное переключение при Sleep(1) не сработает - оно в критическом разделе. Так что у второго потока шанса запуститься по сути нет... Вот один поток и гонит весь цикл, а потом второй. А мьютекс у вас (код вы не привели, будем ванговать...) пропускает один поток, второй ждет. Мьютекс отпущен, второй тут же включился, первый на своей итерации влетает в ожидание. Второй отпускает - захватывает первый... Ну, и так далее. Без вашего кода трудно точно сказать. А вот если вынести Sleep за пределы раздела - типа for (j=1; j <= 12; j++) { EnterCriticalSection(&cs); i = global; i++; global = i; printf_s( "%4s %4d \n", " 1 th", i ); LeaveCriticalSection(&cs); Sleep (1); } то результат уже не такой грустный :) - 1 th 101 2 th 102 1 1 th 103 2 th 104 2 1 th 105 2 th 106 3 1 th 107 2 th 108 4 1 th 109 2 th 110 5 1 th 111 2 th 112 6 1 th 113 2 th 114 7 1 th 115 2 th 116 8 1 th 117 2 th 118 9 1 th 119 2 th 120 10 1 th 121 2 th 122 11 1 th 123 2 th 124 12 Именно потому, что Sleep обеспечивает переключение на второй поток... Я более-менее доступно пояснил? Только вот учтите, что гарантии поочередного выполнения этим переносом Sleep() не получится - есть теоретическая вероятность, что оба потока уйдут в спячку одновременно... P.S. Убрал Sleep совсем, счетчик поднял до 1000 - получил до 123 первый поток, потом до 1122 второй, потом опять первый... Т.е. переключение есть - но именно такое, как я писал - по времени.

Ответ 2



Вся суть и назначение критической секции заключается в том, что захват свободной критической секции - исключительно легкая операция, выполняемая в контексте вызывающего процесса, без системных вызовов и переключения контекста. Попытка же захвата уже занятой критической секции - "тяжелая" операция, которая помещает поток в полноценное состояние системного ожидания - "спячки" (см. однако spin count ниже). По этой причине, если поток, владеющий критической секцией, делает LeaveCriticalSection и EnterCriticalSection без какой-либо паузы между ними, у ждущего ("спящего") потока нет никаких шансов успеть вовремя проснуться и тоже сделать попытку захвата секции. Уже владеющий секцией поток будет все время единолично успешно освобождать и перезахватывать ее. Именно это вы и наблюдаете в вашем эксперименте. Чтобы уравнять шансы потоков, вам надо Либо ввести какую-то задержку между LeaveCriticalSection и EnterCriticalSection, чтобы дать ждущему потоку время проснуться до выполнения EnterCriticalSection. Либо позаботиться о том, чтобы ждущий поток не уходил в состояние системной спячки, пока первый поток "держит" секцию. Этого можно достичь путем указания spin count для критической секции. Разумеется, для такой тяжелой итерации, как у вас, понадобится неразумно большой spin count. Тем не менее, ради эксперимента, в данном примере для облегчения итерации убираем Sleep и делаем InitializeCriticalSectionAndSpinCount(&cs, 10000000); Получаем уже 1 th 101 2 th 102 1 1 th 103 2 th 104 2 2 th 105 3 2 th 106 4 1 th 107 2 th 108 5 2 th 109 6 1 th 110 2 th 111 7 2 th 112 8 1 th 113 1 th 114 2 th 115 9 1 th 116 2 th 117 10 2 th 118 11 2 th 119 12 1 th 120 1 th 121 1 th 122 1 th 123 1 th 124 А если убрать такую "тяжесть", как printf и следить за потоками более эффективными способами, то сработает и существенно меньший spin count.

Предотвращение одновременного запуска нескольких AsyncTask

#android #многопоточность #android_asynctask #mutex


Есть некий AsyncTask. Нужно предотвратить одновременное выполнение нескольких его
экземпляров.

Есть идея использовать старый добрый Mutex, но насколько это правильно?
    


Ответы

Ответ 1



У AsyncTask есть метод executeOnExecutor (Executor exec, Params... params), с помощью которого можно организовать поведение Ваших тасков. Так же есть два заготовленных для этого Executor'а: SERIAL_EXECUTOR и THREAD_POOL_EXECUTOR Если я правильно понял вопрос Вам нужен SERIAL_EXECUTOR - все таски запущенные такой командой будут выполняться по очереди task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params...); Либо Вы можете создать свой, более подходящий экземпляр Executor и использовать его для организации задач.

среда, 25 декабря 2019 г.

C++ реализация call_once

#cpp #mutex #lock_free


Хочу разобраться в том, как работает std::call_once. И главное - lock-free ли он.
Здесь пытаются его реализовать с использованием мьютекса. Если call_once можно реализовать
только с использованием мьютекса, какие проблемы могут возникнуть с этим кодом?

#include 
#include 
#include 

using namespace std;
using my_once_flag = atomic;

void my_call_once(my_once_flag& flag, std::function foo) {
    bool expected = false;
    bool res = flag.compare_exchange_strong(expected, true,
                                            std::memory_order_release, std::memory_order_relaxed);
    if(res)
        foo();
}
my_once_flag flag;
void printOnce() {
    my_call_once(flag, [](){
       cout << "test" << endl;
    });
}
int main() {
    for(int i = 0; i< 50; ++i){
            thread([](){
                printOnce();
            }).detach();
    }
    return 0;
}

    


Ответы

Ответ 1



Стандарт не налагает ограничений на реализацию std::call_once поэтому, его реализация может быть как с блокировками, так и без(я не знаю, возможно ли такую реализацию придумать). Что касается Вашей реализации: она просто неверна. Пусть у нас будет 2 потока, которые одновременно заходят в функцию и попадают на строчку: flag.compare_exchange_strong один из них выставит флаг, а другой уйдёт с полной уверенностью, что функция уже была вызвана. Но до вызова функции дело ещё вообще не дошло! Поэтому, в правильной реализации, на входе в call_once все потоки должны выстроиться в очередь, если кто-то уже начал выполнение функции. Конечно, если бы функция была «чистой»(pure) можно было бы устроить спекулятивное выполнение, с принятием результата от того потока, что первым закончит её исполнение. Но стандарт не налагает никаких ограничений на функцию, которая может быть исполнена в call_once. Поэтому, в целом, я не вижу как можно её реализовать в неблокирующем виде.

воскресенье, 22 декабря 2019 г.

Как наиболее корректно реализовать mutex со счетчиком?

#cpp #многопоточность #cpp11 #mutex


Пусть, например, задача - ограничить количество одновременно работающих потоков.
Т.е. хочется иметь мьютекс, который бы могли захватывать одновременно N потоков, а
остальные бы ждали, пока один из потоков не освободит такой мьютекс.

Вижу несколько вариантов организации такой функциональности, например, с использованием
переменной для подсчета и обычных мьютексов, но что-то мне кажется, что я изобретаю
велосипед.

Как наиболее корректно реализовать такой семафор со счетчиком средствами стандартного
C++11? (Или там это уже есть, и я просто плохо читал литературу?..)
    


Ответы

Ответ 1



В стандартной библиотеке готового семафора нет, по этому его надо писать самому, делая ожидание на condition_variable. В простейшем варианте это class Semaphore { public: explicit Semaphore(int max_count) : max_count_(max_count) {} void acquire() { std::unique_lock lock(mut_); while (count_ == max_count_) cv_.wait(lock); ++count_; } void release() { std::lock_guard lock(mut_); assert(count_ > 0); --count_; cv_.notify_one(); } private: int max_count_; int count_ = 0; std::mutex mut_; std::condition_variable cv_; }; Вызов notify в release() можно вынести из под мьютекса. Но на некоторых реализациях это не нужно. Вместо мьютекса в release() можно использовать атомарную переменную count_, но это потребует CAS в acquire(). Код усложнится, а выигрыш в производительности надо еще доказать.

среда, 27 ноября 2019 г.

Как mutex определяет, какой именно объект нужно блокировать?

#c++ #многопоточность #c++11 #mutex


Как известно,


  Задача мьютекса — защита объекта от доступа к нему других потоков,
  отличных от того, который завладел мьютексом. В каждый конкретный
  момент только один поток может владеть объектом, защищённым мьютексом.
  Если другому потоку будет нужен доступ к переменной, защищённой
  мьютексом, то этот поток блокируется до тех пор, пока мьютекс не будет
  освобождён.


Но как мьютекс определяет, доступ к какому именно ресурсу/объекту/переменной нужно
блокировать? Ведь в него не передается никаких параметров, из которых он мог бы знать
что именно защищать. 

Если что, речь о std::mutex, с++11. 
    


Ответы

Ответ 1



Дополню ответ @alexis031182. Мьютекс или семафор, как его обобщение (конкретно, объект (переменная), расположенный по некоторому адресу) это точка синхронизации потоков кода одного или (в некоторых случаях) нескольких процессов. И ничего более. Он ничего не защищает. Ни объекты в памяти, ни участки кода. Поток не вызывающий операцию lock с этим мьютексом может делать что хочет с любыми переменными и участками кода. Защита производится на логическом уровне (в голове разработчика). Фактически поток кода, вызвавший операцию lock для указанного мьютекса увеличивает счетчик, связанный с этим мьютексом и продолжает выполнение если счетчик был равен нулю. В противном случае этот поток ожидает, пока счетчик не станет равен 0 (тогда он в свою очередь увеличит его и продолжит выполняться), что произойдет, когда кто-то вызовет unlock, уменьшающий счетчик на 1.

Ответ 2



По сути, никак. Он и не защищает конкретный объект. Мьютекс лишь гарантирует, что обращения к данным под мьютексом сериализованы. (То есть, что невозможны гонки данных.) Ответственность работать с нужными объектами возлагается на вас, программиста. То, что вы описали, говорит не о том, что делает мьютекс, а лишь о том, для чего он предназначен, и что вы можете при помощи него делать (сами).

Ответ 3



У любого мютекса есть функции lock и unlock (они могут называться по разному, но суть от этого не меняется. Некоторые мютексы могут использовать raii, автоматически вызывая нужные функции). А защищает он кусок кода, который не может быть исполнен, если мютекс в залоченном состоянии. То есть, один мютекс может защищать много различных мест и они не будут исполнены одновременно (актуально при многопоточном выполнении). С другой стороны, в системе может существовать много мютексов одновременно и они не будут мешать друг-дружке.

Ответ 4



Мьютекс по сути обеспечивает то, что называется «критической секцией» (критическим разделом) - т.е. код, который может выполняться одновременно только одним потоком. Но если данный мьютекс навесить на код, который обращается к некоторой переменной - то создается впечатление, что он защищает объект, а не фрагмент кода.

Ответ 5



Позвольте аллегорию... Кто раньше встал, того и тапки. Нет никакой необходимости отмечать сей факт на них самих. Первый надел - владей, ты победил. Соответственно, кто не успел, тот опоздал, и будет пребывать в унылом ожидании, пока столь полезный, но совершенно неразделяемый ресурс не будет освобождён. Конечно, в какой-то мере пример притянут за уши, поскольку в реальной жизни нельзя исключить вариант силового решения вопроса. Но речь о мьютексах, а потому безусловность контроля за процессом передела имущества само собой разумеется. Лишь добрая воля счастливчика, на этот раз успевшего первым, позволит другому воспользоваться предметом вожделенного комфорта. Сам себя дополню. С тапками аллегория не получилось потому, что многие могут представить себе рабочее существование без них. А суть не в тапках, а в контексте выполнения. Посредством мьютекса контекст выполнения одного потока блокирует/высвобождает контекст выполнения другого или других. А так - да, тапки ни при чём, дело в голове. — Кстати, д’Артаньян: я слышал, что некий Милон Кротонский проделывал удивительные вещи. Он, например, обматывал голову верёвкой и одним движением головных мускулов!!!… разрывал её. Он сбивал быка одним ударом кулака и уносил его на своих плечах… ну, и тому подобное. Я, когда узнал обо всём этом… Я проделал у себя в Пьерфоне… ну, всё то же самое, что и этот Милон. За исключением одного: я никак не мог разорвать головой верёвку. — Это оттого, что у Вас сила… не в голове.

понедельник, 8 июля 2019 г.

Как сделать массив экземпляров класса с полем-мьютексом?

Есть класс A. У него есть поле Mutex m_mutex; - вопрос: как создать глобальный массив экземпляров класса A, у него есть конструктор, который принимает одно число. Пишется ошибка - попытка доступа к удаленной функции. Подозреваю, что проблема с move-семантикой для mutex - это запрещено.


Ответ

Как-то так:
#include
class A { std::mutex m_mutex;
public: A(int x) {} };
A array[3] = {{1}, {2}, {7}};
int main() { return 0; }
То есть нужно явно указать аргумент конструктора каждого элемента массива.

вторник, 11 июня 2019 г.

Использование std::shared_mutex и проблема “обделения” писателя

Допустим, есть несколько читателей и один писатель, которые имеют общий ресурс, контролируемый посредством std::shared_mutex. Как быть в ситуации, когда читатели читают ресурс не одновременно, а, скажем, по очереди, т.ч. мьютекс всегда удерживается через std::shared_lock<>(), и писатель не может записать новые данные в ресурс, т.е. не может пробиться через инструкцию std::unique_lock<>?
Тут приходит на ум логика решения, что попытка захвата мьютекса писателем должна иметь более высокий приоритет, чем у читателей, т.е. когда писатель хочет что-то записать - он оставляет "заявление", т.ч. никакие читатели не могут сделать shared_lock, а те, что уже сделали - доделывают свою работу и ожидают обновлений со стороны писателя. Когда писатель обновит данные - может сделать notify_all() через condition_variable, к примеру.
Не вполне понимаю, как реализовать "приоритет захвата" в коде + может имеются другие решения данной проблемы не через std::shared_mutex?
P.S. Натолкнулся на описание в стандартах алгоритма Терехова, в котором говорится о прохождении читателей/писателей через двое входов, на основе которых выставляется приоритет. Но не совсем его понял. А именно, пройдет ли писатель первый вход, когда на втором есть читатели и, если так, то не смогут ли новые читатели зайти на вход 1.


Ответ

Не знаю, как с приоритетами, а идея двух входов довольно очевидна.
Все делают эксклюзивный lock на E1. Получив его, читатели делают shared lock на E2, а писатели эксклюзивный. Получив lock на E2 и читатели и писатель тут же освобождают lock на E1.
Таким образом, писатель прошедший E1 будет блокировать всех пришедших после него читателей до тех пор, пока последний из совместно использующих E2 читатель не освободит E2.

четверг, 30 мая 2019 г.

Rust. Вызов метода у Arc

Читаю Rustbook и не понимаю некоторые вещи.
use std::sync::{Arc, Mutex}; use std::thread;
fn main() { let data = Arc::new(Mutex::new(vec![1u32, 2, 3])); for i in 0..3 { let data = data.clone(); //(1) thread::spawn(move || { let mut data = data.lock().unwrap(); //(2) data[i] += 1; }); } thread::sleep_ms(50); }
(1). Метод clone() возвращает тип self, т.е. Arc<_>. В этом можно убедиться, если явно написать тип:
let data: Arc>> = data.clone();
В связи с этим возникает вопрос: почему в (2) мы вызываем метод lock() так, если бы мы работали напрямую с Mutex? Все как в C++, но там у shared_ptr оператор -> перегружен за нас, а покопавшись в исходниках arc.rs я не увидел подобного или пропустил.
UPD
Спасибо Кнопкатык за наводку на Deref coercions. Для закрепления написал такой искусственный пример:
use std::ops::Deref;
#[derive(Debug)] struct Field { x: i32, }
impl Field{ fn print(&self){ println!("x = {}", self.x); } }
#[derive(Debug)] struct Example { x: Field, }
impl Deref for Example{ type Target = Field;
fn deref(&self) -> &Field{ &self.x } }
fn main() { let s = Example { x: Field { x: 5} }; s.print(); }
Структура Example не имеет имплементации метода print. Но если вспомнить универсальный синтаксис вызова функций, то
s.print();
можно заменить на:
Field::print(&s);
Следовательно, так как у нас есть реализация типажа Deref для Example, возвращающая &Field, то значения Example, если нужно, будут автоматически преобразовываться в &Field при разыменовании:
Field::print(&Example) -> dereference -> Field::print(&Field);
Код, как и предполагается, выведет:
x = 5


Ответ

Дело в механизме Deref coercions или "Преобразования при разыменовании". Arc реализует типаж Deref, значения &Arc будут автоматически преобразованы в &T, когда это необходимо. Метод lock(&self) Мютекса принимает &self, и когда мы пытаемся вызвать его с Arc
lock(&Arc)
срабатывает механизм Deref coercions
lock(&Mutex)
а &Mutex это и есть &self для метода lock.

четверг, 29 ноября 2018 г.

C++ реализация call_once

Хочу разобраться в том, как работает std::call_once. И главное - lock-free ли он. Здесь пытаются его реализовать с использованием мьютекса. Если call_once можно реализовать только с использованием мьютекса, какие проблемы могут возникнуть с этим кодом?
#include #include #include
using namespace std; using my_once_flag = atomic;
void my_call_once(my_once_flag& flag, std::function foo) { bool expected = false; bool res = flag.compare_exchange_strong(expected, true, std::memory_order_release, std::memory_order_relaxed); if(res) foo(); } my_once_flag flag; void printOnce() { my_call_once(flag, [](){ cout << "test" << endl; }); } int main() { for(int i = 0; i< 50; ++i){ thread([](){ printOnce(); }).detach(); } return 0; }


Ответ

Стандарт не налагает ограничений на реализацию std::call_once поэтому, его реализация может быть как с блокировками, так и без(я не знаю, возможно ли такую реализацию придумать).
Что касается Вашей реализации: она просто неверна. Пусть у нас будет 2 потока, которые одновременно заходят в функцию и попадают на строчку: flag.compare_exchange_strong один из них выставит флаг, а другой уйдёт с полной уверенностью, что функция уже была вызвана. Но до вызова функции дело ещё вообще не дошло! Поэтому, в правильной реализации, на входе в call_once все потоки должны выстроиться в очередь, если кто-то уже начал выполнение функции.
Конечно, если бы функция была «чистой»(pure) можно было бы устроить спекулятивное выполнение, с принятием результата от того потока, что первым закончит её исполнение. Но стандарт не налагает никаких ограничений на функцию, которая может быть исполнена в call_once. Поэтому, в целом, я не вижу как можно её реализовать в неблокирующем виде.

четверг, 15 ноября 2018 г.

Как наиболее корректно реализовать mutex со счетчиком?

Пусть, например, задача - ограничить количество одновременно работающих потоков. Т.е. хочется иметь мьютекс, который бы могли захватывать одновременно N потоков, а остальные бы ждали, пока один из потоков не освободит такой мьютекс.
Вижу несколько вариантов организации такой функциональности, например, с использованием переменной для подсчета и обычных мьютексов, но что-то мне кажется, что я изобретаю велосипед.
Как наиболее корректно реализовать такой семафор со счетчиком средствами стандартного C++11? (Или там это уже есть, и я просто плохо читал литературу?..)


Ответ

В стандартной библиотеке готового семафора нет, по этому его надо писать самому, делая ожидание на condition_variable
В простейшем варианте это
class Semaphore { public: explicit Semaphore(int max_count) : max_count_(max_count) {}
void acquire() { std::unique_lock lock(mut_); while (count_ == max_count_) cv_.wait(lock); ++count_; }
void release() { std::lock_guard lock(mut_); assert(count_ > 0); --count_; cv_.notify_one(); }
private: int max_count_; int count_ = 0; std::mutex mut_; std::condition_variable cv_; };
Вызов notify в release() можно вынести из под мьютекса. Но на некоторых реализациях это не нужно
Вместо мьютекса в release() можно использовать атомарную переменную count_, но это потребует CAS в acquire(). Код усложнится, а выигрыш в производительности надо еще доказать.

вторник, 2 октября 2018 г.

Как mutex определяет, какой именно объект нужно блокировать?

Как известно,
Задача мьютекса — защита объекта от доступа к нему других потоков, отличных от того, который завладел мьютексом. В каждый конкретный момент только один поток может владеть объектом, защищённым мьютексом. Если другому потоку будет нужен доступ к переменной, защищённой мьютексом, то этот поток блокируется до тех пор, пока мьютекс не будет освобождён.
Но как мьютекс определяет, доступ к какому именно ресурсу/объекту/переменной нужно блокировать? Ведь в него не передается никаких параметров, из которых он мог бы знать что именно защищать.
Если что, речь о std::mutex, с++11.


Ответ

Дополню ответ @alexis031182.
Мьютекс или семафор, как его обобщение (конкретно, объект (переменная), расположенный по некоторому адресу) это точка синхронизации потоков кода одного или (в некоторых случаях) нескольких процессов.
И ничего более. Он ничего не защищает. Ни объекты в памяти, ни участки кода. Поток не вызывающий операцию lock с этим мьютексом может делать что хочет с любыми переменными и участками кода. Защита производится на логическом уровне (в голове разработчика).
Фактически поток кода, вызвавший операцию lock для указанного мьютекса увеличивает счетчик, связанный с этим мьютексом и продолжает выполнение если счетчик был равен нулю.
В противном случае этот поток ожидает, пока счетчик не станет равен 0 (тогда он в свою очередь увеличит его и продолжит выполняться), что произойдет, когда кто-то вызовет unlock, уменьшающий счетчик на 1.